Composition vs Inheritance
I can shoot a hoop, but I’m not a basketball player.
Oct 31, 2018
The Basketball Player
Imagine if I told you that you can’t throw a basketball into a hoop, because you’re not 7 feet tall. Doesn’t seem fair right?
What if I gave you a compromise, and told you that you can throw that basketball, but only if you make sure that you tell everyone that you’re definitely not 7 feet tall – is that more acceptable?
As contrived as this scenario is, that is one of the biggest downfalls of using an inheritance model: Trying to define something based on what it is, rather than what it can do requires that exceptions to the rules are explicitly stated, and can lead to undesired behaviour – what if there’s something about a basketball player which actually requires them to be 7 feet tall, and fails otherwise?
As your inheritance hierarchy grows or your base functionality increases, you’ll find that you need to declare more and more of these exceptions, and handle those edge cases further up the hierarchy in your base/shared functionality.
Let’s take a look at a slightly less contrived example to explain this concept further.
The Data Service
Let’s say I have two services which are used to create and delete different types of data to and from a database – people and dogs. Seeing as these services share some functionality, I decide to create an abstract base class for them to inherit from. My solution might look something like this:
All well and good, everything works, and there is no undesired behaviour.
But now let’s say I want to introduce a new service to handle another type of data – cats – except that this service only needs to provide deletion functionality.
DataService
, instead copying and pasting the code to perform the deletion. DataService
, but when overriding the methods that are not required by this new cat service, provide ‘empty’ implementations.
Because I hate copying and pasting code (for multiple reasons, but most notably because I would be creating multiple places for the same bugs to occur), let’s say I decide to go with the latter option. It would look something like this:
Due to my inheritance model, I’ve had to create an ‘empty’ implementation for the buildModel
method – in this case, throwing an exception. I might think: “Well, it’s a bit weird I guess, but adding a little bit of unnecessary code is a small price to pay to achieve this shared functionality.”
That’s a somewhat understandable initial conclusion, but there may be something crucial that I’ve overlooked: To the outside world, the CatService
still has a createModel
method which can be called, and would probably be expected to work. Only the CatService
really knows that an exception will inevitably be thrown if that method is called.
One way to solve our issue might be to break up our DataService
into separate classes: DataDeleterService
and DataCreatorAndDeleterService
, with the former being a parent of the latter – and then adjust our concrete services to inherit from one or the other – but of course that inheritance hierarchy is extremely rigid and brittle, and would be likely to cause more issues down the road.
Enter Composition
We were on to something when we thought about breaking up our DataService
, but the key is to break it up and remove the inheritance at the same time. The most obvious way to do this for our scenario is with interfaces. A simple solution to our problem using composition by way of interfaces looks like this:
As can be seen, using composition rather than inheritance has allowed us to define our CatService
without having to add unnecessary method overrides or deal with a rigid, complex or nonsensical inheritance hierarchy. To the outside, the DogService
and PersonService
both provide the createModel
and deleteModel
methods, whereas the CatService
only provides the latter. It’s easy to reason about what each service does, because we’ve defined them based precisely on what they can do.
As a bonus, we could also introduce new functionality to a particular service, such as updating, without affecting the other services – for example, we could create a new interface DataUpdaterService
and implement it in just the PersonService
.
The Lesson
Splitting up behaviour into separate, composable parts allows us to define something by what it can do rather than what it is, which generally leads to simpler, more concise code that is easier to understand and will be less likely to contain hidden bugs and unknown or undesired behaviour.
Inheritance can of course still be useful for a number of different cases – namely when it does make sense to define something based on what it is, which can often be the case when modelling real-world objects and their properties. However, when dealing with the behaviour of a system it’s almost always better to utilise compositional patterns rather than inheritance.
Thanks for reading!
Read more posts like this