Composition in Object-Oriented vs Functional Programming
In a previous post, I laid out a framework for code quality that divides the qualities of code into a few categories like fitness, correctness, clarity, performance, maintainability and beauty. However, let's forget all that for the moment and talk about composition, since that is key to discussing object-oriented vs. functional programming.
compositionnoun 1. the act of combining parts or elements to form a whole.
In programming terms, composition is about making more complex programs out of simpler programs, without modifying the simpler pieces being composed--where the program could be anything from a single CPU instruction to an operating system with apps.
Do Objects Compose?
In object-oriented programming (OOP), the unit of composition seems to be an object (or class). Do objects compose? The answer to that is complicated, as there isn’t even a commonly accepted definition of object orientation. It’s mostly a “I know it when I see it” type of thing. Objects that represent data structures with little behavior usually do compose. This is also what we can model in class diagrams: one object owns or contains another. This kind of composition doesn't really have much to do with being object-oriented: struct or tuple types compose just as well as data objects.
For objects with complex behavior, there aren't any well-defined compositions. Even UML diagrams give up on specifying the nature of such composition and simply allow you to say that “A depends on B”. The nature of the dependency is completely ad-hoc. B might be used internally in a method, passed as a constructor argument or method parameter or might be returned from a method.
OOP puts code and data close together, so we want to know if data and behavior taken together can compose. There's no answer for the general case, because objects can have very complex and incompatible behavior patterns. Especially if an object is observably mutable (which seems to be the usual case with OO code), it has a potentially complex life cycle. And objects with different life cycles can easily become non-composable.
I think the real composability and reusability in object-oriented code doesn't come from object-oriented design at all: it comes from abstraction and encapsulation. If a library manages to hide a potentially complex implementation behind a simple interface, and you only have to know the simple interface to use it, then that doesn't come from some inherent property of object-orientation, but a basic module system that limits which symbols are exported to other modules, whether the modules are classes, namespaces, packages or something else. Such module systems are present in both OO and non-OO languages.
In summary, I think objects do not compose very well in general – only if the specific objects are designed to compose. Immutability helps to make objects composable by eliminating complex life-cycles and essentially turning the objects into values. Encapsulation also improves composability, and is put to good use in well-designed OO code, but is not unique to OO.
Do Functions Compose?
Functional programming (FP), on the other hand, is at its very core based on the mathematical notion of function composition. Composing functions f and g means g(f(x)) – f's output becomes g's input. And in pure FP, the inputs and outputs are values without life cycles. It's so simple to understand compared to the numerous and perhaps even indescribable ad hoc compositions possible in OOP. If you have two functions with matching input and output types, they always compose! More complicated forms of composition can be achieved through higher-order functions: by passing functions as inputs to functions or outputting functions from functions. Functions are treated as values just like everything else. In summary, functions almost always compose, because they deal with values that have no life cycles.
What Does Composition Give Us?
Having simple core rules for composition gives us a better ability to take existing code apart and put it together in a different way, and thus become reusable. Objects were once hailed as something that finally brings reusability, but it's actually functions that can achieve this much more easily. The real key, I think, is that the composability afforded by the functional design approach means that the same approach can be used for both the highest levels of abstraction and the lowest level–behavior is described by functions all the way down (many machine instructions can also be represented as functions).
However, I think most programmers today (including me) don't really understand how to make this approach work for complete programs. I would love for the functional programming advocates to put more effort into explaining how to build complete programs (that contain GUIs, database interactions and whatnot) in a functional way. I would really like to learn that rather than new abstractions from category theory -- even if the latter can help with the former, show us OOP guys the big strategy before the the small tactics of getting there.
Even if we can't do whole programs as functions, we can certainly do isolated parts of them. On the other hand, it seems that objects that put code close to the data help people make sense of a system. And if the objects are really designed to be composable, it works out quite nicely. I think a mix of object-oriented and functional programming such as in Scala, F# or even Java 8 is the way to go forward. So, thanks for tuning in, and reach out to me on Twitter @t4ffer.
Looking for additional insights into Object-Oriented design?
Our article on SOLID object-oriented design principles covers everything you need to know about SOLID.