I have been refactoring a number of APIs at work. A common pattern is to split a large API class into smaller ones. The desired smaller interfaces usually become abstracted classes with a single method. For example, there is an interface to collect data items and persisted them in a data store.
This is in C++, our programming language of choice. Simply put, Collector
is an interface with a single abstract method Collect
that takes a single data
and returns no value.
If you are an enthusiast of functional programming, you might ask, “why couldn’t this be simply be a function interface?”. Well, of ourse it could! Then it will look like this:
This simply says CollectorFn
is a function type that takes a single data
and returns no value, exactly the same as the class interface above! We can use function interfaces instead of class interfaces, because functions are also a first-class type in various programming languages. They can be passed around just like a regular object.
NOTE: in C++, the use of std::function
is usually a bad idea as its a copyable type, the alternative is to use the move-only absl::AnyInvocable or std::move_only_function from C++ 23.
The main benefit of using a function interface instead of a class interface is that the interface cannot be expanded beyond its signature. If the interface is a class, someone might add a new method, some members, type aliases, constructors / destructors, etc. None of that is possible with a function interface, keeping the interface stable and intact.
This might all sounds well, but the systems you work with might already be be filled with class interfaces, is it still possible to adopt function interfaces?
Converting a Function to a Class
Function can be converted to classes, and this technique can be used to plug the simpler function interface into an existing system with class interfaces.
Take the Collector
and CollectorFn
above as an example, a FunctionCollector
can be implemented to convert a CollectorFn
into a Collector
class.
That’s it! Now you can implement a bunch of CollectorFn
s and adapt them into the existing class interfaces whenever and wherever needed.
Converting a Class to a Function
Sometimes implementing something using functions only might be really inconvenient, and class features could really come in handy. Say for a particular Collector
implementation, we really want to buffer all the data items and perform the writes when the Collector
fall out of scope in the current stack. With a class, this can be trivially implemented in the class' destructor. So let's do that, but adapt the class back into a function to fit the function interface.
Here, we leverage closure in a lambda expression to make a CollectorFn
that keeps around the Sink
class with desired lifetime behavior. The usage of class and class features become entirely implementation details. e.g. in C++, the Sink
class can be put into an anonymous namespace in the .cc
file with only MakeDbCollectorFn
exposed in the header file.
Conclusion
When it comes to describing and implementing interfaces, classes and functions really work out the same. One can be converted to the other without much efforts. Generally using functions should keep the surface areas of your interfaces and modules small, thus making code easier to maintain.