The phrase “favor composition over inheritance” has become one of those thought-terminating cliches in software design, and I always like to take a deeper look at those to understand where they come from and what ideas we’re missing if we just take the phrase at face value without engaging with the discussion that led to it.
This is one of those aphorisms with a definite origin story (compare with Aphorism Considered Harmful for an example where the parts have clear origins but the whole does not): it’s the second object-oriented design principle in the Design Patterns book from the “Gang of Four” Gamma, Helm, Johnson, and Vlissides. Well, sort of:
Favor object composition over class inheritance.
It comes at the end of a page and a half of justification, and actually just before another page and a half on delegation (an extreme example of object composition), so really in the middle of a three-page discussion. This contrasts inheritance as a “white box” form of reuse, because the inheriting class has full visibility over the implementation details of the inherited class; with composition as a “black box” form of reuse, because the composing object only has access to the interface of the constituent object.
That’s certainly true of Smalltalk objects, and Smalltalk is one of the Gang’s example implementation languages. But even a modestly more recent language like Java has visibility attributes that let a class control what its subtypes can view or change, meaning that any modification in a subclass can be designed before we even know that a subtype is needed. Looking at this the other way, we could also say that languages like Smalltalk and Python that have advanced runtime introspection let a composing object access internal state of its constituent. This part of the argument then is contextually and historically situated, and depends on designers playing nicely within the intent of object designs, even where those aren’t enforced by a language.
Another part is more compelling: inheritance is defined statically at compile time and comes with language support, which makes it easier to use than composition and harder to change. Composition is manually arranged by a programmer assigning the constituent object to a member field and calling its methods in the composing object’s implementation; which is more work but easier to change at runtime. Assign a different object to the field, and get new behavior.
Further, the classes are (assuming the polite agreement above is honored) related only by the public interface of the constituent object, so there’s no implementation dependency. The system’s design depends on the relationships between objects at runtime, rather than the inheritance tree defined at compile time. This is presented as an advantage, which we need to consider in the context of the modern preference to rely more on the compiler as a correctness checker and static analysis tool, and to avoid deferring decisions to runtime that might turn out to be buggy.
Bear in mind that a year earlier, Barbara Liskov and Jeanette Wing proposed this principle:
What does it mean for one type to be a subtype of another? We argue that this is a semantic question having to do with the behavior of the objects of the two types: the objects of the subtype ought to behave the same as those of the supertype as far as anyone or any program using supertype objects can tell.
Within this context, the preference for composition is liberating to the designer: their type isn’t morally a subtype of the thing they’re extending, so they don’t need to restrict themselves to a compatible interface. And indeed, Liskov had already made this point in 1987 in Data Abstraction and Hierarchy, with reference to polymorphic types:
Using hierarchy to support polymorphism means that a polymorphic module is conceived of as using a supertype, and every type that is intended to be used by that module is made a subtype of the supertype. When supertypes are introduced before subtypes, hierarchy is a good way to capture the relationships. The supertype is added to the type universe when it is invented, and subtypes are added below it later.
If the types exist before the relationship, hierarchy does not work as well[…] An alternative approach is to simply allow the polymorphic module to use any type that supplies the needed operations. In this case no attempt is made to relate the types. Instead, an object belonging to any of the related types can be passed as an argument to the polymorphic module. Thus we get the same effect, but without the need to complicate the type universe. We will refer to this approach as the grouping approach.
Liskov goes on to say that when the relationship is identified early in design, “hierarchy is a good way to express the relationship. Otherwise, either the grouping approach…or procedures as arguments may be better”.
That points to a deficiency in the “composition over inheritance” aphorism: those aren’t the only two games in town. If you have procedures as first-class types (like blocks in Smalltalk, or lambdas in many languages), then you might prefer those over composition or inheritance.
