Inheritance vs Composition
There is a frequently encountered trade-off in software engineering: code reuse vs extensibility.1 Wikipedia: Extensibility. Extensibility is a topic that we will revisit in subsequent posts, but, put simply, extensibility correlates with how easy it is for a developer to modify the behaviour of some software. From Wikipedia,
Extensibility imposes fewer and cleaner dependencies during development, as well as reduced coupling and more cohesive abstractions, plus well defined interfaces.[3]
It is trivial to have an extensible system: have everything be singleton - every function, class etc. only has a single call or instance. It is also fairly easy to have a system with maximal reuse. 2To be clear, maximal code reuse, as we intend it here, and fewest lines of code are distinct concepts. Perhaps we ought to be calling it logic reuse instead. Doing both simultaneously, however, poses some challenges and over the years it has become apparent that some approaches for code reuse work better than others. In what follows below, we shall restrict our scope to object-oriented programming and present two alternatives: object composition 3 Wikipedia: Object composition. and inheritance.4 Wikipedia: Inheritance., 5Not to be confused with subtyping, which is a related, but orthogonal concept.
The crux of the idea is that there is more than one way to achieve code reuse in object-oriented programming,6Specifically, in addition to inheriting from a base or a parent class, one can compose classes together. and that object composition ought to be favoured over inheritance when we care about extensibility. This isn’t a novel idea.7Wikipedia: Composition over Inheritance., 8Wikipedia: Timeline of Composition. In fact, it could be considered a fairly well-known idea.9In addition to the Wikipedia [[https://en.wikipedia.org/wiki/Object-oriented_programming#Composition\][_inheritance,_and_delegation, entries]], this idea was also discussed in the influential Design Patterns book.
Practically speaking…
When, instead of reusing the code for def inputs
and def compute
(as in
Listing 1 10Example taken from
wikipedia. below), we do something like Listing 2,
the resulting code is more extensible.
1: class SumComputer: 2: def __init__(self, a, b): 3: self.a = a 4: self.b = b 5: 6: def transform(self, x): 7: raise NotImplementedError 8: 9: def inputs(self): 10: return range(self.a, self.b) 11: 12: def compute(self): 13: return sum(self.transform(value) for value in self.inputs()) 14: 15: class SquareSumComputer(SumComputer): 16: def transform(self, x): 17: return x * x 18: 19: class CubeSumComputer(SumComputer): 20: def transform(self, x): 21: return x * x * x
1: class SumComputer: 2: def __init__(self, a, b): 3: self.a = a 4: self.b = b 5: 6: def inputs(self): 7: return range(self.a, self.b) 8: 9: def compute(self, transform): 10: return sum(transform(value) for value in self.inputs()) 11: 12: class SquareSumComputer: 13: def __init__(self, a, b): 14: self.sumcomputer = SumComputer(a, b) 15: 16: def compute(self): 17: self.sumcomputer.compute(lambda x: x * x) 18: 19: class CubeSumComputer: 20: def __init__(self, a, b): 21: self.sumcomputer = SumComputer(a, b) 22: 23: def compute(self): 24: self.sumcomputer.compute(lambda x: x * x * x)
One reason the above code is more extensible is because object composition
allows us to reuse code from multiple classes without using multiple
inheritance11Note that object composition doesn’t forbid the use of
inheritance and CubeSquareSumComputer
could have been defined with Listing
1 as well. To truly see the limitation, the examples
have to be more complex. (see Listing 3 below). Thus,
we are free of associated constraints, such as requiring that all ancestors of a
derived class be linearizable in a consistent (i.e., monotonic) manner.
25: class CubeSquareSumComputer: 26: def __init__(self, a, b): 27: self.squaresum = SquareSumComputer(a, b) 28: self.cubesum = CubeSumComputer(a, b)
That’s it. That’s the idea.
Background
Inheritance
Inheritance imposes what may be unnecessary design constraints12A future post may elaborate on these constraints further. and its use has been controversial at least since the 1990s.13 Wikipedia: Inheritance issues. From Wikipedia:
Reportedly, Java inventor James Gosling has spoken against implementation inheritance, stating that he would not include it if he were to redesign Java.[19] Language designs that decouple inheritance from subtyping (interface inheritance) appeared as early as 1990;[21] a modern example of this is the Go programming language.
Some languages, notably Go [4] and Rust, [5] use type composition exclusively.
It seems, over the years, some of the drawbacks of inheritance have become apparent and the industry has taken steps to move away from it. Increasingly, the industry is moving towards object composition.
Object composition
Ironically, object composition as a concept predates inheritance.14The first widespread programming language to support object composition was COBOL in 1959 whereas Ole-Johan Dahl and Kristen Nygaard’s design of superclasses and subclasses didn’t take shape till 1967. Whereas inheritance defines an Is-a subsumption relation between a derived/child class and a base/parent class (with the derived class being able to refer to the base class), object composition defines a Has-a containment relation between the whole and the part (with the whole being able to refer to the part).
By allowing one thing to refer to another, both inheritance and object
composition permit reuse (with or without modification). It’s the nature of the
cost that one has to pay (for said reuse) that differs. When using
inheritance,
the cost limits extensibility. When using object composition,
the cost results in
boilerplate code
in the form of
forwarding methods. For instance in Listing 2 above,
we had to redefine def compute
in SquareSumComputer
and CubeSumComputer
.
Interestingly, when using object composition, there is more than one sense in
which a corresponding method (or property) of the contained value (i.e., the
object or field or member) may be referenced depending on what self
(in the
invoked method) is bound to:
forwarding15Wikipedia:
Forwarding. vs
delegation.16Wikipedia:
Delegation. Contrasting them, and debating their relative merits, is
something for a future post to tackle.
Comments
Comments can be left on twitter, mastodon, as well as below, so have at it.
New Post!
— The Weary Travelers blog (@wearyTravlrsBlg) May 14, 2023
Idea: What really is the difference between Is-A and Has-A inheritance?https://t.co/766bf7yU2W
Reply here if you have comments.
Footnotes:
To be clear, maximal code reuse, as we intend it here, and fewest lines of code are distinct concepts. Perhaps we ought to be calling it logic reuse instead.
Not to be confused with subtyping, which is a related, but orthogonal concept.
Specifically, in addition to inheriting from a base or a parent class, one can compose classes together.
In addition to the Wikipedia [[https://en.wikipedia.org/wiki/Object-oriented_programming#Composition\][_inheritance,_and_delegation, entries]], this idea was also discussed in the influential Design Patterns book.
Note that object composition doesn’t forbid the use of
inheritance and CubeSquareSumComputer
could have been defined with Listing
1 as well. To truly see the limitation, the examples
have to be more complex.
A future post may elaborate on these constraints further.
The first widespread programming language to support object composition was COBOL in 1959 whereas Ole-Johan Dahl and Kristen Nygaard’s design of superclasses and subclasses didn’t take shape till 1967.