Stop Doing Inheritance Wrong!
Object-oriented programming (OOP) can be hard, I admit it. I remember long ago being taught OOP in an undergrad class and not really getting it. I also remember the first C++ book I read and how confusing I found it. But over the years I finally figured it out. In grad school I studied programming languages and implemented a few languages of my own. I have taught OOP at the university level and I’m a professional software engineer. I use OOP every day.
Everyone who’s familiar with OOP understands classes, methods, and to a lesser degree, encapsulation. Those are the first things you learn in your undergrad OOP class. Then there are more big words like overloading, inheritance, overriding, and polymorphism where if you have class A and class B and B inherits from A and there are methods in A and methods in B, and well, things start to get confusing.
Again, OOP can be hard.
Just recently I looked up the syntax for how to run a concurrent thread in Python because, well, even professional software engineers Google things. However, many of the examples I saw left me in dismay: they used inheritance for a simple thread example (which by itself isn’t necessarily bad) but they used inheritance improperly. Before I go into that more, let’s have a look at what inheritance is.
1. What is inheritance?
Inheritance in an object-oriented program is about identity. It specifies a relationship between two classes that declares one of them to be the parent class or superclass, and the other one to be the child class or subclass.
An effect of that is that the subclass inherits the properties of the parent class. The subclass is also allowed to add its own properties, thereby extending the parent class.
What does inheritance actually mean?
Inheritance declares a subclass to be a more specialized version of the superclass.
Most of you who learned OOP probably read that statement in a book or heard it in lecture, and you might even have it in your class notes. That’s the fundamental essence of inheritance and it’s extremely important, yet many programmers get it wrong.
Say I have a class called Computer with the two public methods powerOn and compute. Then I create a subclass of Computer called Laptop and add the public methods openLid and closeLid to it. This UML diagram describes that relationship:
There are important things to know about a UML inheritance diagram:
- The arrow points from subclass to superclass: in this diagram Laptop is a subclass of Computer, and Computer is a superclass of Laptop.
- It shows an is-a (or is-an) relationship, whereby an instance of the subclass automatically is-an instance of the superclass. A Laptop is-a Computer.
- The flow of information in the diagram goes against the arrow. In this diagram it goes from Computer to Laptop (more on this later).
- No information flows in the direction of the arrow, from Laptop to Computer (more on this later).
Therefore this specific Computer/Laptop diagram shows a few things:
- Every instance of Computer can use the two methods powerOn and compute.
- Every instance of Laptop can also use the two methods powerOn and compute because the Laptop class is a subclass of the Computer class and it inherits those two public methods from the Computer class.
- Every instance of Laptop can also use the two methods openLid and closeLid.
- The Computer class doesn’t know anything about the Laptop class and therefore can’t call its openLid and closeLid methods.
- Every Laptop is a Computer but not every Computer is a Laptop.
These are some consequences of using inheritance:
- A subclass automatically inherits all the non-private methods declared in the superclass. This is what I’ll call the most visible consequence.
- Any variable that can hold an instance of the superclass can also hold an instance of the subclass. If you call one of the superclass methods on that variable, it will get dispatched to the appropriate method in either the superclass or the subclass, depending on the actual runtime type of the instance. This effect is called polymorphism, and it’s often an important motivation for using inheritance in the first place. I won’t discuss this any more here.
- I’ve saved the most important consequence for last. I mentioned it already earlier in this section, and even put it on its own line and in bold. I’m going to call it the most important consequence and it has to do with identity. Here I make it a little more specific than the previous way I wrote it:
An instance of the subclass is a special kind of whatever the superclass is.
As an example, a Laptop isn’t just a Computer, it’s a special kind of Computer — in this case it’s one that can open and close its lid.
That last point, the most important consequence, has nothing to do with C++ or Python or Java or compiled code or UML. It’s a statement about the model you’re creating, about how you’re choosing to represent some part of the real world in software. If the most important consequence doesn’t apply to your model as a natural consequence of the inheritance relationship you’ve created, you need to find out why and fix it.
Making a model consistent with reality is helpful for a number of reasons:
- Others who read your program will surely have the same mental model of the class relationship and will understand the program model easier.
- Augmenting the model will work the same in code as it does in the real world. For example, with my Computer & Laptop class relationship I don’t have to think too hard about how to add a TabletLaptop class or a MainframeComputer class.
Does a model absolutely need to be consistent with reality? No, it doesn’t. A program doesn’t ever refuse to run just because it represents a bad model. This is one of the difficult aspects of programming: sometimes it can be hard to get the model right. And of course sometimes there is no real-world analog of what you’re modeling in software. Is ClassB extends ClassA a relationship that’s consistent with the real world? The question is invalid. And finally, sometimes you may not know the proper way to model something — but here is one of the many joys of programming and where you find some of the science in computer science: just experiment with it, and if it doesn’t work then change your experiment and try again.
So far, though, this seems pretty easy, right? Of course every laptop is a computer. Good inheritance designs should make sense. Not everyone gets it right, though.
Summary: Inheritance declares a subclass to be a more specialized version of the superclass.
2. Points and Circles
Several years ago I was reviewing textbooks for a Java course I was to be teaching and I found a book by a popular group of authors that used the following example of inheritance, or something very close to it. Don’t worry much about the constructors or methods, but check out the inheritance relationship between these two classes:
The authors went on to discuss how the Circle class inherits the Point class’s translate method, and so translating a Circle is no different from translating a Point. Inheritance at its finest! The authors were clearly exploiting the most visible consequence of inheritance in order to inherit the Point class’s translate method into the Circle class. It is a good example to show how that specific mechanism of method inheritance works, BUT IT’S A TERRIBLE EXAMPLE OF INHERITANCE.
Right here, this line:
means: In this program universe, all Circles are Points.
Anyone who knows anything about geometry can see the problem immediately: circles aren’t points. If we remember the most important consequence of inheritance, then according to this model if you’re a Circle you are a special kind of Point, and what makes you special is that you have a radius. A radius isn’t just a number, it implies a special meaning: by having a radius it means that you have an infinite number of points all located the same distance from your center point. So by being a Circle not only are you a special kind of Point, but you’re one that’s made up of an infinite number of points (and I guess those points could be circles, too). It requires a lot of mental gymnastics to even begin to reconcile the inconsistencies between this model and reality. The coding might be “correct” — the program runs just as the programmer wanted — but the model is very wrong.
What’s the proper way to relate Point and Circle? Composition. A Circle should contain a Point (it’s the center point). This is the common has-a relationship. That’s how an actual circle is defined in the real world, after all.
And I’ve written the class in case you want to see it.
There is added complexity here because now the Circle class needs a centerPoint member variable and it needs to delegate its translate method to the centerPoint, but that added complexity is warranted because now the model makes sense.
Still not convinced? What if the Point class were a subclass of a Shape class that has a surfaceArea method in it. Ok, that’s doable: we can override that method so that a Circle has surface area πr2 and a Point has surface area 0. Now I add a LineSegment extends Shape class that contains two Point instances as its endpoints (the class below might make it easier to understand what I’m talking about). What should the surface area of a LineSegment be? Geometrically speaking it should be 0. But what if I use two Circle instances as the end points of the LineSegment? Those don’t have a surface area of 0. What do I do now, just ignore the surface area of the Circles and return 0? Or return the sum of the areas of the circles?
This poorly designed model is forcing us to ask questions that we really shouldn’t need to ask and causing unnecessary problems. Unnecessary problems cause unnecessary development delays and also lead to bugs that would otherwise be avoidable.
In this section I’ve said a lot about something that was just one example in a voluminous textbook. I worry, though, that too many programmers came away from that example with a poor understanding of inheritance (even just one programmer would be too many programmers). My motivation here is to help you understand that good modeling is crucial to good programming.
Summary: Why do I dislike the Circle extends Point example? Because it’s a bad model.
3. Threads and ToyRobot
As I read through a number of Python threading examples online I saw that each was a little different, but many of them shared one characteristic: each created a class that inherited from the Thread class for the sole purpose of inheriting one of its methods. This is exploitation of the most visible consequence of inheritance. That ended up making the model wrong in each of the examples.
I’ll distill the multiple examples into this one ToyRobot class in Python:
By extending the Thread class the ToyRobot class inherits the Thread class’s start method. Ok, so it works. The thread runs and the ToyRobot instance does something concurrently.
But this inheritance relationship is inherently wrong.
Much like saying that every Circle is a special kind of Point, here in this program universe every ToyRobot is a special kind of Thread.
This is the wrong way to use inheritance because the model is wrong. The only time you should extend the Thread class is when you’re creating a new and special kind of Thread.
- A WatchdogThread is a special kind of Thread.
- A WeaklyReferencedThread is a special kind of Thread.
- A BackgroundThread is a special kind of Thread.
A ToyRobot , though, is not a special kind of Thread. It’s a special kind of Toy. Or it’s a special kind of Robot. Or it’s a special kind of ArtificialHumanoid. There are many things a ToyRobot is a special kind of, but Thread is not one of them.
How should you create a concurrently running ToyRobot instance? Again, use composition. A ToyRobot contains a Thread:
The only criticism one can level against this version of the program is that, similar to the corrected Circle class I discussed earlier, it’s more complex than the previous ToyRobot (Thread) example — but you shouldn’t strive to make your program as simple as possible in the first place. Some consider that to be a form of optimization, and it’s something you shouldn’t address until after you’ve determined that it’s a problem. Instead, make your program readable. Make it debuggable. Make it extendable. Make it testable. Make it understandable. And sometimes you have to make it a little longer in order to do that.
Summary: Why do I dislike the ToyRobot (Thread) example? Because readers will get the impression that it’s the way threading should be done.
What would I like you to take away from this article?
- Don’t use inheritance just to get access to another class’s methods if it results in an incorrect model.
- Do use inheritance to specify a fundamental and invariant identity relationship between classes.
- Do create good, logical, clean, readable, maintainable classes and class hierarchies. It shows that you understand the problem domain, and it shows that you understand software development.
Other articles in this section
Will Developers Lose Their jobs to ChatGPT? + An AI Expert’s Opinion
ChatGPT, the advanced language model developed by OpenAI, has gained significant attention for its ability to generate human-like text with high accuracy.
How to Optimize & Accelerate GPUs for Graphical & Machine Vision Processing
Machine learning (ML) systems analyze tremendous amounts of data to identify hidden patterns and make predictions based on those patterns. This requires a very high level of parallel processing.
5 Tips for Combatting Developer Burnout!
5 Tips for Combatting Developer Burnout!
Do you remember when you first learned to code? It was fun, magical even, and you were obsessed.
Over a 30-year career, Jeff has done software development in the aerospace, automotive, and restaurant industries. He spent time in grad school researching programming languages and computer science education, and even worked at Yale University in the Haskell group. He has taught computer science and engineering at several universities in the US and overseas, and now works at GSI writing software for robotics and embedded systems.