Designing Entity Hierarchies in iOS: Class Inheritance v Composition
A poorly designed entity hierarchy can be as unstable as this tower. Inflexible designs can be unaccepting of new entities or characteristics, and can lead to lots of copying and pasting of code. Entities further down the hierarchy can end up with unnecessary behaviours, or unintended implementations, and lots of overriding can lead to poor dynamic dispatch. In short, it’s important to get your design right!
There are two common approaches to designing such a hierarchy: Class Inheritance and Composition, and it might surprise you to learn that one is significantly better than the other. In this article we will design a simple hierarchy using both of these approaches in order to discuss their benefits and shortcomings. Let’s get started!
Approach 1: Class Inheritance
Class inheritance is the classic approach to designing an entity hierarchy in OOP, and even a beginner in swift will be familiar with the concepts in this section. As a tiny bit of review, let’s remind ourselves that inheritance is a fundamental behaviour that is baked into classes, but not found in structs.
For the purposes of our article we are going to build a code representation of the following hierarchy:
And in code, it looks like this:
Notice that Bird inherits from Animal (line 9), and then each of our four specific birds inherits from Bird (lines 17, 19, 21, 23). I have also marked them with the final keyword to ensure that the compiler knows not to allow further subclassing, which in turn assists with dynamic dispatch. The methods that are defined in Animal and Bird are now available to our four subclasses of bird.
So far everything looks fine, and that is because the only methods that we have added happen to fit nicely in this hierarchy. After all, every type of bird will consume food and lay eggs. But what happens if we try to add methods for swim(), walk() and fly()? If we visualize which of these abilities belong to our four subclasses of Bird, we get something like this:
In this situation, none of the abilities can be placed in the Bird superclass, as none of them are universal to all four of the bird subclasses. Instead we have varying combinations of them. In order to have these abilities available where they need to be, we are forced to do something like this:
Even a quick glance tells us this is a poor solution for several reasons. First there is a lot of repetition, with the same method implementation copied into different classes. Just imagine if we needed to make a change to one of these methods, we would now need to make that same change in numerous locations.
Next, it also breaks the Single-Responsibility Principle. In the case of Puffin, we have all three methods implemented in a single class. For the sake of code maintainability, we would ideally want to see these different responsibilities encapsulated in different entities.
One might argue that the amount of copying could be abated with some ‘creative’ subclassing, but this inevitably leads to an inheritance juggling act that only gets more complicated with the introduction of new methods, or new classes that require only some of the inherited methods. We end up with a very fragile hierarchy where a single change in a superclass can have unpredicted results further down the inheritance chain. All of that aside, when we look at the hierarchy diagram that we started with, we are reminded that our ideal is to have all of our bird subclasses on the same level which is just not possible with this approach.
So why was class inheritance unable to give us the result we wanted? One useful way of framing this discussion is to think of ‘is-a’ versus ‘has-a’ relationships. Class Inheritance is limited to representing ‘is-a’ relationships: It was perfectly good at describing that a Puffin ‘is-a’ Bird, and that a Bird ‘is-a(n)’ Animal. However when we are trying to represent ‘has-a’ relationships it typically falls short. In this context, we can think of ‘has-a’ as expressing a property (‘has a beak’) or a method (‘has the ability to fly’). We saw that the abilities that we wanted to add swim(), walk() and run(), are not consistently applied across our bird subclasses and this is not unusual. The more that you observe the world with ‘is-a’ and ‘has-a’ at the front of your mind, the more you realize that the natural world just isn’t so easy to box.
Approach 2: Composition Through Protocols
So now let’s tackle the same problem through composition. This approach is elegantly described in the following phrase which was coined by the Gang of Four:
Program to an interface, not an implementation.
In iOS we define interfaces through the use of protocols. Any entity that adopts a protocol must contractually conform to the required interface, which is essentially what the phrase above is asking us to do. Our basic setup now looks like this:
Notice that we have two protocols Animal and Bird, the latter inherits the former (line 5). Then we have our four entities, each of which adopts and conforms to the Bird protocol. Now, whatever characteristics are found in Animal and Bird must be implemented in our structs.
In composition it is common to create protocols for individual behaviours and not just for entities. For example, in our Bird protocol we have the behaviour layEgg(). When we stop to think about it, we can quickly see that other animals lay eggs too. This would be a prime candidate for its own protocol. Maybe something like:
Notice that the layEgg() method we had in Bird has now been moved into a new protocol, EggLayable (lines 5–7). Unlike class inheritance, which only allows a class to inherit from a single superclass, we can see that protocols allow for multiple inheritance. In our case, Bird can inherit from both Animal and EggLayable (line 9). This is one of the most powerful features of composition as you can pick and choose the characteristics that you want your entities to have (and not have!). Side note: Repackaging your protocols in various combinations can be a good way around using messy protocol optionals.
Let’s now add our walk(), swim(), and fly() methods into this hierarchy. Because we want these behaviours to be available to other animals that are not birds, we will give them their own protocols much like we did with EggLayable. This time however, I want to implement the methods only once and have that single implementation shared across all of my bird entities. To do this, we use protocol extensions.
The three new protocols are declared at the top of the file, and their methods placed in extensions as default implementations. Each of the four structs now adopt their required Walkable, Swimmable, and Flyable protocols, and you will notice that no implementation is required in the struct itself.
If we want to make an exception to the default implementation in the protocol extension, we can simply do this:
The swim() method now appears in the entity (line 12) which means that the default implementation in the Swimmable protocol extension is never accessed for this entity.
Hands down, composition beats class inheritance when it comes to designing stable and flexible hierarchies. Our protocols can use multiple inheritance to acquire whatever they want, and our entities can similarly adopt and conform to multiple protocols. We can easily provide default implementation of methods and create behaviour specific to a single entity wherever we need to. Our example entity hierarchy now works not only for any future bird entities we might want to create, but also any object that might need to lay eggs, swim, fly or walk.
I want to point out one final advantage of composition. When we use class inheritance we never get to ask ourselves if we want our entity type to pass by reference or value, because inheritance is only available in classes. Composition on the other hand, provides us a real choice about whether to make our entities structs or classes.
As usual, thank you for reading!
Postscript: This topic got me thinking about polymorphism — the way that we work with different types at various levels in our hierarchy through a shared interface. I wrote an article about polymorphism which you can find here.