Exploring the Rationale Behind 'Favor Composition Over Inheritance'

software engineering

Delve into the origins and underlying principles of the 'favor composition over inheritance' design aphorism. This article examines its justification, historical context, connection to the Liskov Substitution Principle, and modern relevance in software design.

The phrase "favor composition over inheritance" has evolved into a common, often unexamined, aphorism in software design. This analysis delves into its origins and the underlying discussions to uncover the nuances often overlooked when the principle is accepted without deeper engagement.

Unlike some aphorisms whose origins are diffuse (such as "Aphorism Considered Harmful"), this one has a distinct source: it is the second object-oriented design principle presented in the seminal Design Patterns book by the "Gang of Four" (Gamma, Helm, Johnson, and Vlissides). Their precise wording is: "Favor object composition over class inheritance."

This principle appears within a three-page discussion, preceded by justification and followed by an exploration of delegation—an advanced form of object composition. The authors distinguish inheritance as a "white-box" form of reuse, where the inheriting class gains full visibility into the inherited class's implementation details. In contrast, composition is described as "black-box" reuse, as the composing object interacts solely with the interface of its constituent object.

While this distinction holds true for Smalltalk, one of the Gang of Four's primary example languages, more recent languages like Java offer visibility attributes that enable a class to control what its subtypes can access or modify. This allows for designed modifications in a subclass even before the need for a subtype is explicitly known. Conversely, languages such as Smalltalk and Python, with their advanced runtime introspection capabilities, permit a composing object to access the internal state of its constituent. This aspect of the argument is thus contextually and historically sensitive, relying on designers adhering to the intended object design principles, even when language features don't strictly enforce them.

A more compelling aspect of the argument highlights that inheritance is statically defined at compile time and inherently supported by language constructs, making it simpler to implement initially but more rigid to alter later. Composition, conversely, requires manual arrangement: a programmer assigns a constituent object to a member field and invokes its methods within the composing object’s implementation. This demands more upfront effort but offers greater flexibility for runtime changes; simply assigning a different object to the field can yield new behavior.

Furthermore, assuming adherence to good design practices, classes in a compositional relationship are linked only by the public interface of the constituent object, eliminating direct implementation dependencies. The system’s design then hinges on runtime object relationships rather than a compile-time inheritance hierarchy. While presented as an advantage, this perspective must be weighed against the modern preference for leveraging compilers as correctness checkers and static analysis tools, thereby minimizing the deferral of critical decisions to runtime, where they might introduce bugs.

It's crucial to recall that just a year prior to the Design Patterns book, Barbara Liskov and Jeanette Wing introduced the Liskov Substitution Principle (LSP), articulating:

"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."

In this light, embracing composition offers designers significant liberation: their new type doesn't need to conform to a strict, moral subtype relationship with what it extends, thus freeing them from restrictive interface compatibility. Liskov herself articulated this concept earlier, in her 1987 paper, Data Abstraction and Hierarchy, regarding 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 further noted that when relationships are identified early in the design process, "hierarchy is a good way to express the relationship. Otherwise, either the grouping approach…or procedures as arguments may be better."

This highlights a potential limitation in the "composition over inheritance" aphorism: it frames the choice as a binary one. When first-class procedures (such as Smalltalk blocks or lambdas in various modern languages) are available, they can offer a compelling alternative, potentially being preferred over both composition and inheritance.