When to prefer inheritance to composition
Hint: it involves the typechecker
Hi everyone!
First of all, new blog post: Somehow AutoHotKey is kinda good now. AHK's been a core part of my toolkit for years now and the new, backwards incompatible version is a whole lot better. But most of the article is about how much v1 sucked, which is more entertaining and more useful to non-AHKers. You think Javascript is bad? Javascript's nothing compared to v1.
I wanted to write a newsletter that was topical to the post, but none of my ideas really gelled, so instead here's something else on my mind: when is OOP inheritance better than composition? I'll assume you've all heard "prefer composition to inheritance", which is generally good advice, and I'm interested in where the advice doesn't apply.
(Caveat this is mostly theory crafting. I've run into a lot of cases where this approach seems correct but I wouldn't be surprised if someone found a "better way" without using inheritance.)
Subclassing is subtyping
Let's take a really simple model case that looks like Python but is really pseudocode:1
@dataclass
class TextElement:
text: str
# Inheritance
@dataclass
class LinkElementI(TextElement):
url: str
# Composition
@dataclass
class LinkElementC:
element: TextElement
url: str
Most of the advantages and drawbacks of composition have been discussed, in detail, for 30 years. It's more flexible, it doesn't break if someone updates TextElement
, but it means implementing a lot of delegation logic to call TextElement
methods.
One argument I haven't seen very much is this:
def render(t: TextElement) -> html:
...
render(LinkElementI())
typechecks. render(LinkElementC())
does not.
Inheritance is a form of subtyping. Any function that accepts the parent class can also accept any inheriting classes. The same isn't true for composition, which is an entirely new type. So if you want to define functions over the parent type, you need to use inheritance.
Now the obvious problem with this: LinkElementI
can override TextElement
's methods, which means that you can't safely pass it into a function expecting a TextElement
. This leads to the Liskov Substitution Principle: to safely use inheritance, you should be able to pass the child object into methods expecting the parent object. IE subclasses should extend but not change the observable behavior of the parent class.
This'll come up later.
Aside: LSP
LSP as a design principle comes from Robert Martin. He got it from A Behavioral Notion of Subtyping, which tries to find sets of properties that satisfy the Subtype Requirement:
Let ϕ(x) be a property provable about objects x of type T. Then ϕ(y) should be true for objects y of type S where S is a subtype of T.
If you read the paper, you'll see that it's about formalizing subtypes, not finding good design principles. Barbara Liskov never said that inherited classes should be subtypes, just the conditions where they are subtypes. The LSP comes later.
(Also, SOLID papers usually leave out the "history rule", which is that state changes on the subtype have to compatible with state changes on the supertype. That's why Square
isn't a subtype of Rect
, if they're both mutable.)
Aside: testing LSP
Elisa Baniassad and Alexander Summers have this great paper Reframing the Liskov Substitution Principle through the Lens of Testing, where they teach LSP as "the superclasses test suite should automatically be runnable, and pass, on the child class." Go read it, it's great.
What about interfaces?
Okay, back composition vs inheritance. There's an approach that gets type-safety without inheritance: interfaces!
interface Renderable:
...
class TextNode implements Renderable:
text: str
class LinkElementC implements Renderable:
element: TextElement
url: str
def render(r: Renderable) -> html:
...
Funnily enough, interfaces are a variant of abstract data types, which were also invented by Liskov. It won her a Turing Award.
So are there any advantages of inheritance over composition + interfaces? There is one: interfaces aren't implementations. They're just type signatures. With inheritance, you get all of the superclass's implementation, which you can then override.
But then you run into LSP violations and all the other reasons you'd want to use composition instead. Hm.
When to use Inheritance
So here are the benefits of inheritance:
- Unlike composition, you can pass the subclass into functions expecting the parent class.
- Unlike interfaces, you can reuse code from the parent class in the child class.
So, here's when you want to use inheritance: when you need to instantiate both the parent and child classes and pass them to the same functions. That's it. That's the use case.
I really like this condition. For one, in most online examples of "prefer composition to inheritance", the condition doesn't apply. Here's the diagram from the wiki page:
It makes sense that Duck
compose with Flyable
and Quackable
and not inherit from them. Are you ever going to have a free-floating Flyable
in your codebase? Of course not! I'd even say that MallardDuck
shouldn't inherit from Duck
if you can cleanly avoid that, because you're never expecting to use a free-floating Duck
in your code. Compose it and use a delegator.
What's a case where the condition does hold? How about TextElement
and LinkElement
! That's in fact how docutils represents the restructuredText node tree, and it works very, very well. It's really easy to add custom nodes with custom logic to your documentation and have it integrate with the rest of the toolchain without any boilerplate or adaptors.
There's also a broad category of uses, one where I don't have a good word for it, so I'll just call it
Different Execution Purposes
If we do inheritance "properly", then we can swap every instance of the parent class with the subclass at runtime. So we can do something like:
class MallardDuck:
...
class TestingMallardDuck(MallardDuck):
...
class ProfilingMallardDuck(MallardDuck):
...
The base class of MallardDuck
is the "default" class we use in production. When we run our test suite, we swap it for TestingMallardDuck
, which has extra testing methods and assertions and stuff. When we run the profiler, we use ProfilingMallardDuck
, which stores extra calling information and adds extra hooks for our profiler. All of these substitutions produce the same observable behavior for all of our "production" methods, but give us extended behavior for our new execution purpose.
Inheritance is a useful tool if we need to run our program under the supervision of other code, for the purpose of querying or analysing the original program. That's probably the way I use it the most, and the situation in which I miss having inheritance the most.
I don't think it'll forever be the right tool for doing this. Someone will eventually come up with a better way, and we'll generally prefer that to inheritance. The same thing happened with code reuse, subtyping, namespacing, and specialization. Inheritance was the first feature that did any of these things, all of the better approaches we now use came after.
-
Why are composition and inheritance always taught with physical objects, when the vast majority of OOP classes are computational abstractions? Nobody actually makes a
Dog
class, but they do makeGUIWidget
andLogger
classes. I think it's because physical objects are universally known while computational abstractions aren't. If you useTextElement
in a class you risk alienating half the class. ↩
If you're reading this on the web, you can subscribe here. Updates are once a week. My main website is here.
My new book, Logic for Programmers, is now in early access! Get it here.