One of the topics that frequently comes up in software design is the difference between composition and inheritance. At its core, it’s a question of how you organize your types/classes when you have several of them and share functionality across them.
This video about composition and inheritance by the YouTuber mpj is an excellent illustration of the topic and explains the benefits of composition. After watching it, I felt like I had a good understanding of the differences and tradeoffs. But something felt like it hadn’t clicked — whenever someone would mention composition over inheritance in the context of a code review or an interview, I’d have to spend a few minutes thinking through mpj’s arguments to remember exactly why one was preferred over the other. I felt like I hadn’t fully internalized the concept.
Given this, I wanted to develop a visual representation of the differences that I could instantly recall, to feel more confident in my understanding. In this post I’ll first explain the example that mpj talks about in the video, and then I’ll describe the visual model I came to.
Cats, Dogs, and Robots
In the example, mpj begins with the class Dog
, which has a bark()
method, and a class Cat
that has a meow()
method. Both classes share a common poop()
method, and he encapsulates this by creating a parent Animal
class:
Animal
.poop() Dog
.bark() Cat
.meow()
We also have two kinds of robots: a CleaningRobot
that can drive()
and clean()
, and a MurderRobot
that can drive()
and kill()
; we again capture the shared drive()
functionality in a parent Robot
class:
Robot
.drive() MurderRobot
.kill() CleaningRobot
.clean()
He then points out the trouble that arises when, a few months later, changing project requirements result in the need for a MurderRobotDog
, that has the ability to bark()
, drive()
, and kill()
, but does not have the ability to poop()
(it has no digestive system). mpj's expression in this frame tells us all we need to know about the dire state of affairs:
The only way to do this with inheritance is to move the bark()
functionality up into a common parent class of Animal
and Robot
. But this creates a redundancy where classes like Cat
and CleaningRobot
now have this bark()
function that they don't really need. This is bad design.
The solution is to design our types with composition — each datatype is created by combining the functions it needs:
dog = pooper + barker
cat = pooper + meower
cleaningRobot = driver + cleaner
murderRobot = driver + killer
murderRobotDog = driver + killer + barker
The way this is actually implemented in JavaScript is with object factories (see mpj’s video for more detail).
Inheritance, in Pictures
mpj’s explanation is awesome, but here’s the problem: in order to recall it, we have to go through all the steps of thinking about a bunch of classes, and the functionalities they share, and what exact configuration of classes results in a bad state with inheritance. Is there a simpler way to express the difference between composition and inheritance?
In any problem, we have a set of functionalities that we care about. In the dogs and cats example, the collection of all the functionalities looks like the following:
When we create a class or a datatype, we’re really just grouping some of these functions together. A class is just a subset of the universe of functions:
You add more classes by creating more groups of functionality. Going off of mpj’s video, we’ll create a slightly simpler world — let’s say we have just one kind of Robot
, and it has the ability to drive
, clean
, and kill
:
Now, what do you do if you want to share functionality between classes? If you’re creating objects via inheritance, you do this by creating a parent class. You can create a parent Animal
class that implements poop
, and Cat
and Dog
can inherit from that class:
In other words, to share behavior via inheritance, the child class must visually contain the parent class. The same parent class can be contained (i.e. inherited) by multiple children, but the constraint with inheritance is that a given child can only have one parent¹.
Put together, we have the following two rules in the world of inheritance:
- If two classes share functionality (i.e. their sets intersect), there must be a wrapper around all of the functions they share.
- A class can contain (i.e. be a child of) at most one other class.
Now let’s go back to the problem of the MurderRobotDog
—let's say that in addition to cleaning, killing, and driving, the robot also needs to bark. We can start by expanding the Robot class we created:
But now we have an intersection between Robot
and Dog
, which means we need to draw a circle around bark()
, i.e. create a parent Barking
class that just barks:
But now we’re in an invalid state! Dog
contains two circles, which means it's inheriting from two parent classes. This is the problem with inheritance².
Composition, in Pictures
The picture with composition is much simpler. You just start with your pieces of functionality…
…and then you group the elements (compose them) into whatever arrangement you want. Any set of circles that you draw is valid:
This is analogous to the equations we had above where we just said that dog = pooper + barker
, cat = pooper + meower
, and so on.
So to put it simply, with inheritance, we are constrained about which sets of functionalities we can group together, whereas with composition, we are not.
Of course, this model doesn’t take into account the implementation of the two design patterns, and ignores a number of nuances³. But it does help to visually demonstrate the flexibility of one pattern over the other.
The Power of Diagrams
Perhaps the diagrams only served to complicate a concept you felt like you already understood; in that case, feel free to disregard them. But for me, the diagrams give a sense that I understand the problem better. I’m able to strip down the problem to its core: it’s ultimately a question of how a collection of items is organized into intersecting groups.
This is what makes visual diagrams especially helpful — they articulate a complex topic into a picture, which you can then recall much more quickly than trying to build out the entire argument in words. Hopefully, next time someone points out composition and inheritance to you, you’ll be able to more quickly remember why composition is the more flexible option.
Footnotes
- Some languages do support multiple inheritance, but that adds an additional layer of complexity that we’ll leave out for this discussion.
- Just going off the diagram, one could technically create multiple copies of a certain piece of functionality like
bark
, to create the configuration we want. But that would be analogous to implementing the same function in two different classes! - In particular, a technical mentor at work pointed out that another distinction is that inheritance enables coupling of functionalities, e.g. between methods of the same class or between methods/properties across the parent and child class. The
Dog
class can override thepoop
method from its parent; themeow
method inCat
can call thepoop
method in its body if desired. This coupling can introduce all kinds of complexity or side effects if not handled carefully.