When did people favor composition over inheritance?

The phrase “prioritizing composition over inheritance” has become one of those thought-expiring clichés in software design, and I always like to take a deeper look at them to understand where they come from and what ideas we’re missing if we take that phrase at face value without engaging in the discussion that led to it.

This is one of those aphorisms with a definite origin story (compare for an example to aphorisms considered harmful where parts clear origin but whole Does not): This “Gang of Four” is the second object-oriented design principle in the design patterns book of Gamma, Helm, Johnson, and Vlissides. Well, like this:

Prefer object composition over class inheritance.

This comes at the end of a page and a half of justification, and actually just before a page and a half on delegation (an extreme example of object structure), so actually comes in the middle of a three-page discussion. This compares to inheritance as a “white box” form of reuse, because the inherited 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 component object’s interface.

This is certainly true of Smalltalk objects, and Smalltalk is one of the gang’s example implementation languages. But even a moderately recent language like Java has visibility features that let a class control what its subtypes can see or change, meaning that any modifications to the subclass can be designed before we even know a subtype is needed. Looking at it another way, we can also say that languages ​​like Smalltalk and Python that have advanced runtime introspection allow a composing object to access the internal state of its component. This part of the argument is then contextually and historically situated, and relies on designers playing well within object design intentions, even where they are not enforced by a language.

The second part is more compelling: inheritance is defined statically at compile time and comes with language support, which makes it easy to use by composition and change is hardComposition is organized manually by a programmer, who assigns the component object to a member field and calls its methods in the composing object’s implementation; Which is more work but easy to change at runtime, Assign a different object to the field, and get new behavior,

Furthermore, classes (assuming the above polite agreement is respected) only belong to the public interface of the component object, so there is no implementation dependency. The design of the system depends on the relationships between objects at runtime rather than on an inheritance tree defined at compile time. This is presented as a benefit that 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 making decisions at runtime that could be bad.

Keep in mind that a year earlier, Barbara Liskov and Janet Wing proposed this theory:

What does it mean for one type to be a subtype of another type? We argue that this is a semantic question concerning the behavior of two types of objects: objects of the subtype should behave the same as objects of the supertype, as far as anyone or any program using a supertype object can tell.

In this context, the priority of composition is free to the designer: their type is not morally a subtype of the thing they are extending, so they do not need to limit themselves to a compatible interface. And in fact, Liskov already said this in Data Abstraction and Hierarchies in 1987 in the context of polymorphic types:

Using hierarchy to support polymorphism means that a polymorphic module is conceived as using a supertype, and each type used by that module is made a subtype of the supertype. When the supertype is introduced before the subtype, it is a good way to capture hierarchical relationships. When a supertype is invented, it is added to the type universe, and later subtypes are added below it.

If types exist before the relation, hierarchy doesn’t work as well[…] An alternative approach is to allow polymorphic modules to use any type that supplies the required operations. In this case no attempt has been made to combine types. Instead, objects belonging to any related type can be passed as arguments to the polymorphic module. Thus we get the same effect, but without the need to complicate the universe of types. We will refer to this approach as the grouping approach.

Liskov further adds that when the relationship is identified early in the design, “hierarchy is a good way to express the relationship. Otherwise, either a grouping approach…or logically processes may be better”.

This points to a flaw in the “building on legacy” aphorism: these aren’t the only two games in town. If you have first-class type procedures (like blocks in Smalltalk, or lambdas in many languages), you can prefer them over composition. Or Heritage.



Leave a Comment