Composition vs Inheritance
We've all been told that we should prefer composition over inheritance, but do we all know why?
In generic terms we say that composition is more versatile and more testable, but we're going to get to the bottom of this, because I am having my doubts.
Let's discuss this with a simple example of composition in Python:
Technically this is composition, because the class Composition is composed of an Element. But that on its own doesn't make it useful: it just makes it a different class that happens to take an Element. What makes it useful is that this Composition can be called as though it were an Element. That's because the Composition and Element classes both have the same interface.
The interface is obviously a single function called my_func taking only the self argument and returning a string.
In that sense an instance of Element is substitutable by an instance of Composition, as hinted by the commented line,
and thus follows the Liskov Substitution Principle.
So that's a rather interesting observation, that in software engineering, the term composition implies not only composition in the regular sense, but also that the object implements the contract of the elemental types it consists of. That's what makes them substitutible.
In fact, composition in the regular sense without the substitutability also exists in software engineering, but that's a different kind of composition. It more commonly goes under the name of tuple/data structure/wrapper/..., depending on context.
Let's be a bit more technical and get our definitions straight: what are the requirements to qualify for "composition"?
- The behavior of the composing object should be able to differ from the elemental object and other kinds of compositions.
- That's a rather trivial requirement. Its abstract expression might make it seem like it's quite something but it really isn't.
- Just to be clear, "kind" here doesn't refer to "type" but something more abstract: any objects groupable by some distinguishing feature, i.e. its "kind".
- Objects abide by the Liskov Substitution Principle (both at runtime and design-time).
- That implies that the behavioral difference should be encapsulated: a caller shouldn't need to care about what kind of object it is to get the object to run the appropriate code: that should be abstracted away.
I would say that the above example satisfies these requirements and therefore is composition. ✅. Let's go home, we're done... Or are we?
The type system
I'm going to change the example only ever so slightly:
We've made the code make use of Python's type system, and suddenly at design-time the type checker mypy is complaining:
error: Incompatible types in assignment (expression has type "Composition", variable has type "Element")
That's clearly unacceptable. You could alleviate this with typing the parameter to be Element | Composition, but that's no tenable strategy as the full type could be undeterminable.
So although at runtime nothing has changed, this code - merely by adding type hints! - is not abiding by the LSP at design-time anymore. What would we need to do to make mypy happy? What do we need to get composition both at runtime and design-time? Or perhaps the most pressing question is: why does mypy insist that this is wrong even though the type-unannotated version is absolutely fine?
That is the central question of this essay.
The remainder of this article can be found here.