Inheritance is an Object-Oriented Programming concept near and dear to my heart. Early on I recognized it as a godsend to programmers. Over time, I found myself realizing its destructive nature in my projects. In programs where I began to crave extensibility, I noticed that inheritance started limiting me to due to its tightly coupled nature. In services I had to maintain, I found inheritance hindering readability causing headaches figuring out where functions were defined.
I want everyone to realize that composition should be used instead of implementation inheritance all the time because inheritance creates code that is neither extensible or readable which are the foundational properties of clean code.
What is Inheritance?
Inheritance is the concept of an “is-a” relationship. We say “a cat is a mammal” and “a mammal is an Animal.” When we say something is an animal, it means it has the properties of an animal (shares member variables), and it does things that animals do (shares methods).
There are two types of inheritance:
- Interface inheritance – Derived classes share the public method interface of the class and override its implementation. Java/C# makes this clear by having “Interfaces” where the methods in the Interface has no implementation. In C++, you’ll need to make an abstract base class and force there to be no default implementation. Interface inheritance is the good type of inheritance, required for polymorphism – the ultimate tool for creating extensible code in Object-Oriented Programming.
- Implementation inheritance – Derived classes share the data and implementation of methods in the base class. This leads to inflexible systems where derived classes couple tightly to their base classes. From now on, when I say inheritance, this is the type of inheritance I’m referencing.
When I first started programming, I saw inheritance as a way to share code between different classes. It was beautiful. You could write functions in a base class, and all of the derived classes would automatically have those functions, reducing redundant code. It took me a long time to understand that implementation inheritance was inherently evil, and we can achieve the benefits of polymorphism without the disadvantages of inheritance by using composition to reduce redundancy.
Problems with implementation inheritance:
- Inheritance relationships reduce extensibility. Derived classes are tightly coupled to your base class keeping you from trading out behaviors from your base classes with different ones and requiring you to make different classes to mix and match different base classes. Additionally, in most languages, this tightly coupled relationship can’t be changed at runtime, which composition would allow you to do.
- Every class that inherits from another class diminishes readability. It’s hard to know where functions are declared and what classes override certain methods.
- You can’t test the derived class separate from the base class. This impact on testability is a dead giveaway that your code is tightly coupled. Remember, testable code is extensible code.
I’ll walk you through the thought process I took years to understand. Think about the above problems as I go through my learnings.
Hierarchies are Horrible
I used inheritance to create a large hierarchy of classes in a game I was making. Here’s a simple version of the hierarchy:
Polymorphic containers of Players and Snowballs would be iterated through to render, handle collision detection, and move every object without needing to know the derived type. You automatically have the methods for rendering, colliding, and moving with no extra lines of code. It seems awesome! Let’s see how bad this really is.
How would you add an InvisibleWall class? What about adding a ParticleEffect that moves but would annoy the player to death if they collided with it?
One way to add these classes could be to create them at the bottom of the hierarchy and override the methods to be blank. For example, make ParticleEffect inherit from Moveable but override the collide() method with an empty body. The problem: now you’re lying. InvisibleWall is-a Renderable, but it doesn’t render. The lie damages the readability of your code because you’ll have to dig into the implementation of the derived classes to understand which objects are doing what. Also, notice how this would add redundancy because you would end up with classes that override the same methods with no-op behavior (InvisibleWall and InvisibleSnowball would both override render with the same behavior). Seems silly because the whole point of using inheritance was to share code 😛
Another way to add classes like InvisibleWall or ParticleEffect would be to create classes like RenderableCollidableMovers and InvisibleCollidableMovers. You can quickly see here that this would cause a combinatorial explosion with so many redundant classes and code.
It’s a mess! The core problem that makes hierarchies difficult is how inherently tightly coupled derived classes are to their base classes. In this example, you can’t make a Movable that isn’t a Renderable or a Collidable without lying by overriding their methods. That’s a huge problem. How would we fix this while still using inheritance? Remove the hierarchies!
The Madness of Mix-ins
Now that we understand that hierarchies are a tightly-coupled mess, we can use flat hierarchies to make this better. This is called Mix-ins. The name comes from an ice cream shop that allowed customers to mix and match different “mix-ins” like sprinkles and caramels together. Let’s see our example modified to use Mix-ins:
In this example, the Renderable class still contains the code for rendering, removing redundancy. We get the benefit over hierarchies because Movable, Collidable, and Renderable are no longer coupled to each other.
So now how would we add an InvisibleWall or a ParticleEffect class? Well, it’s pretty easy. We can have InvisibleWall only inherit from Collidable (in fact, you might just use Collidable instead), and ParticleEffect would inherit from Moveable and Renderable. Not bad.
Wait a minute… Isn’t this multiple inheritance, and isn’t that bad!? Yes, it is. Multiple inheritance, in this case, isn’t so bad because we will avoid the dreaded diamond by only allowing two layers of classes.
Still, there’s a huge code smell here. We can notice it when we try to test Snowball or Player and find that we can’t isolate the snowball code from its base classes to test, resulting in redundant tests across classes or just more complicated tests in general.
Again, the problem is the tight coupling between the derived class to the base class. Let’s say in the beginning, Collidable just assumes all objects are rectangles and uses that to calculate collisions. As our physics gets more sophisticated, we find Snowballs shouldn’t really be rectangles. Playtesters think the snowballs look strange when they collide. To fix this, we want to add circular colliders. How could we do this?
- Override the Snowball collide method with circular collision logic.
- This damages readability because Snowball would be lying. It’s not Collidable in the same way that other Collidables are.
- It’s not extensible. What if we add basketballs later and need the same code? We wouldn’t be able to share it.
- Rename the Collidable class RectangleCollidable and create a new one called CircleCollidable. Each derived class can inherit from the one we need. This allows us to share code between classes.
- Now we lose the power of polymorphism – you can’t use all collidable objects with the same interface. We’d be tempted to create a new base class collidable to gain this back, but now we’d be creating a hierarchy… Not good.
- In most languages, we can’t change the behavior at runtime. Imagine a new object that changes between rectangle and circle colliders. Not easy to do.
- Add a flag inside the Collidable class that says if it’s rectangular or circular. Snowballs can set the flag to Circular on construction and Walls can be rectangular.
- Now the Collidable class begins to do too much, weakening cohesion, which makes testing the class more complicated.
- In the future, if we want to add new types of collision, we’d have to update the code inside this class violating the open-closed principle. Updating this class could cause bugs that impact every class that inherits from Collidable!
- The derived class is still coupled to the base class making testing in isolation hard. This testability predicament is a smell that our code isn’t as extensible as we believe.
What can we do to fix this? Let’s abolish the is-a relationship. Let’s make it so we can pass in different types of collidable components into our classes to make them super flexible.
If you loved this post, you would enjoy reading most of the books I recommend. Check them out!