Why class inheritance should be considered harmful

Why class inheritance should be considered harmful

Or; how to eliminate huge amount of tech debt by stop misusing class inheritance

OK, do I have your attention now?

Good... because misuse of class inheritance, in my experience, has wreaked havoc across many object-oriented systems that I have seen. Inheritance is fundamental mechanism of object-oriented design but it's improper usage is very harmful to your system and it could lead to considerable amount of tech debt that is rarely ever paid.

Read this article to learn how and why.

Now, before I start - let us remember one of the features that made object-oriented software design so popular and important.

Encapsulation:

In-fact, encapsulation is so important that is one of the fundamentals of object-oriented design.

It is very simple idea and yet very powerful: by simply keeping the public interface (or public API) and actual implementation strictly separated - thus making actual implementation encapsulated by its public interface, as shown on picture above - we can have other regions of code relay only on that public interface and therefor completely unaware of implementation.

That means that we are now free to change private implementation as we see fit. To fix a bug, to change a way it works or to replace it completely - without having even to touch all other places (which could be considerable) that uses that public interface.

Simple and effective means to protect entire regions of code from future changes. Make no mistake about it - if you are designing software, changes will come.

Being able to protect code from future changes is directly translated to maintainability as one of the most important system quality attributes in world of enterprise software and maintainability directly maps to agility (being able to change something efficiently) - "the" quality.

So that means, by simply applying basic tenets of object oriented design, we can make our team and our project more agile. In other words, if you pay more attention to design, you can achieve greater agility.

The irony is that, companies and development teams, in relentless chase for more and more agility are totally and completely neglecting design aspect of software. Which as we can see here clearly - and by the means of the one of the design fundamentals - is actually bringing them agility from get-go. So, by ignoring design aspect, in search for agility, teams are actually losing agility. Isn't that ironic? Teams all over the world are actually discouraged to do any upfront design and yet, they have professional courtesy to call themselves engineers. But I digress ...

Critics and skeptics might say: yeah, that's all fine, but, I don't need some access modifiers or some special syntax or naming convention to do something that is basically a common sense.

That is wrong on many levels. First of all, it is highly unlikely that you will work alone, outside of the team. And it is also unlikely that everyone will be so common sensed as you are.

The other thing, as have my experience thought me: people are so hypnotically laser-focused on solving actual problem from an actual domain they do forget some of those design fundamentals and principles, and even more so, they are usually pressured by deadlines and need to be agile and deliver - that sometimes, in-fact very often - do these design and architectural loopholes and shortcuts.

Or, as Niel Ford once put it very nicely:

There are gremlins living in your architecture.

And that applies to design as well.

So, yes, we do need special syntax, some access modifiers, even some naming conventions too, to help us maintain proper encapsulation. Why? To be more agile, of course, isn't it obvious?

So, let's get back to inheritance now.

Let's say you have some class, and that class implements some feature. Let's call it Class A and that class implements feature called Feature 1:

Fine. Seems reasonable.

Now, let's say now we need completely new feature. Let's call it Feature 2 and we shall implement Class B. So far so good.

However, Feature 1 and Feature 2 might have something in common. Maybe not much, but they will have to share something. Maybe some part of the interface or maybe some part of implementation. Or maybe nothing at all, just, Feature 2 will have to use Feature 1.

In all of these cases above, what would most developers do? Certainly those inexperienced with object-oriented design? Well, inheritance of course, what else ...

Inheritance will be used in many cases, even if Class B and Class A have nothing in common at all, and Class B doesn't even use Class A.

We simply need to expose interface of Class A trough Class B for some reason and it seems convenient and simple approach.

It is not, it is harmful and it creates tech debt that is hardly ever paid. Yet, I've seen many implementations like this.

Why do people use this? There are many reasons to do so. For example, if there something to be shared and reused like interface or implementation, it seems like good idea.

People naturally, when they learn new design technique, just to be generally fascinated and overwhelmed by possibilities and later they tend to use that same technique where is possible, being harmful or not.

Moreover, usually when someone starts using new language - he or she will begin to see inheritance used almost everywhere. It is because in every truly object-oriented language every class inherits something, all the way down to lowest object in hierarchy, usually named simply - object. For example, if you are building typical modern web application you will have to inherit every controller or view that you might use in you project. Someone would might wrongfully conclude that just might be best way to design your object-oriented system. It is not. What they usually neglect to see that those base controllers and views are abstract classes intended only for inheritance only, they can't be instantiated.

So, what exactly is wrong in inheritance?

For a start - it breaks encapsulation. In our example Feature 1 will expose its private parts to Feature 2 and thus your encapsulation will be rendered useless. It will have no meaningful purpose any more.

But you might say - it doesn't matter - my access modifiers are finely grained, I have private and protected members. Maybe so, but I would argue that protected access modifiers are intended for same feature implementation within class hierarchy. In example here we still have different features (that might or might not share some piece of interface or logic or whatever). So, Feature 2 will still have access to encapsulated parts of Feature 1.

If someone is not yet convinced that this is harmful practice, and that inheritance breaks encapsulation - I will now pull the biggest argument of them all - the call from authority - Gang of Four, Design Patterns (Chapter 1):

Because inheritance exposes a subclass to details of its parent's implementation, it's often said that " inheritance breaks encapsulation"

OK, so it does breaks encapsulation after all, is that all?

No, not even close.

Consider following:

In our example Class B that implements Feature 2 uses something from Feature 1 that has inherited from Class A.

What I'd like to do, perhaps on some other part of the system, is be able to use Feature 2, but this time that will be version that doesn't uses something from Feature 1, but rather something else, something which I will provide. Let's call it customized Feature 2.

Will I be able to use that? Hardly... And why not? That is because those our feature has are in established inheritance relationship are tightly coupled together.

In other words inheritance establishes tightly coupled relation between classes.

Now, we all know, that a lot of has been changed in realm of software engineering trough years, best practices of today are becoming anti patterns of tomorrow, yes, we all know that.

But some things remain unchanged. And one of those things is tight coupling. It is bad practice no matter how you put it. Except maybe if you building operating system, but most of us aren't so lucky (or unlucky) - most of us will be stuck in tightly coupled software tech-debt hell.

OK, so, why is tight coupling between features such a bad thing you might ask?

Well, first of it adds to code inflexibility. It is harder (if not impossible) to make, for example, different version of Class B that uses some other functionality rather than the one that uses here in our example (Feature 1).

But, real ramifications of tight coupling are much bigger. Much more accurate visual representation of our example would probably look more like this:

Now, obviously, this is pretty inflexible. Not only that, can you, for example properly analyze and understand Class B without having to analyze Class A? No, not really. How much is there a chance that change in Class A implementation will cascade trough Class B and cause trouble? Pretty high I would say. Is there a chance that if you have need to change one class have to change them both? Can you even use one class without having to use another? And so on... there is really an range of issues to consider here.

So, if things are that bad with inheritance, what should we do? Nothing, just use composition. Inheritance is one of the fundamentals of object-oriented design, but, so is composition. Composition will help to preserve encapsulation, it will make relation between classes more flexible (can be switched with another instance at run-time easily) and it will also make you more wary of you interfaces. You'll much better off with composition in vast majority of cases. Overusing inheritance is just looking for trouble.

If someone is still not convinced, I'll pull out again mother-of-all-arguments - call for authority. Gang of Four, Design Patterns (Chapter 1, page 20):

Favor 'object composition' over 'class inheritance'

OK, so, class inheritance breaks encapsulation and creates tight coupling, is that all?

Actually ... no. I am just getting started ...

Ask yourself this question - what motivates people to use inheritance in first place?

Most of will say: well, I have one class that shares something in common with some other class that I already have, and since I've been thought that inheritance is "is" type relation and one class is also that another class it seemed like reasonable idea.

Well, if you dig little deeper motivation behind this thinking is assumption that there will be some region of your code that will use objects from these two classes without really knowing or caring that those objects are actually instantiated from different classes.

Meaning this: car is vehicle. Bicycle is also vehicle. But, car is not bicycle and vice versa. So if you have code that assumes that those objects are instantiated from same class and uses them as such - you'll have to compensate at some point for real differences between car and bicycle. And that compensation will have to come in form of "if" or some other conditional statements.

Conditional statement is what makes your code less structured. Unstructured code is code more prone to bugs. Your unstructured logic is then highly likely to be duplicated on multiple places. And as more differences appear in future between you classes inheriting one other, more compensations for differences in form of conditional statements will appear and more and more bugs will your program contain.

Take a good look at your code base and tell me that it isn't so. You know they say - before every bug, first there was a change...

Let me back that up with some facts:

In realm of software design there are a lot of things that is pretty hard to prove, so most of times we must relay on some sort of heuristics or some guiding principles of some sort. There were many of those so called principles over the years but very few of them stood test of time. One of those that is still around and not only that - it just gaining more and more popularity is know as SOLID principles of object-oriented design. That is actually an acronym for Single responsibility, Open-closed, Liskov substitution, Interface segregation and Dependency inversion principles.

That is certainly mouthful. As I said, those the ones that stood test of time and proven as very valuable guidelines in design of any object-oriented system. In-fact they are considered as "first five principles" or just "basic principles". According to wikipedia:

The intention is that these principles, when applied together, will make it more likely that a programmer will create a system that is easy to maintain and extend over time.

And of course "easy to maintain and extend" translates directly to agility that we aiming for.

Now let's see does our inheritance example from above - actually breaks any of those design principles?

As it turns out, there is actually principles that directly addressees inheritance. It is called LSP or Liskov Substitution Principle, named after famous computer scientist Barbara Liskov, and it goes something like this:

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

Or to rephrase it little bit differently:

Objects in a program should be replaceable with instances of their sub-types without altering the correctness of that program.

Or more precisely:

Object of derived class should be able to replace any object of base class without bringing any errors into the system or any modification to the client code.

And that is the exactly same situation described in car, bicycle and vehicle example shown earlier.

If you designing object-oriented system, and you are introducing some derived type, and those types are not mutually replaceable - you'll have to be aware that you'll have also to modify some other code that uses those types to compensate for those differences. And those modification can easily cascade trough dependent code and cause undesired behavior. Before every bug, first there was a change.

So, in accordance with this principle, old inheritance relation definition is no longer valid. Before, inheritance relation was defined as "is" relation. With LSP principle applied inheritance is now "is substitution for" relation.

As we can see, real ramifications of breaking LSP principle is such that you'll have to change your code. Why is that such bad thing? Well, understandably, every change can cascade trough depended code, since no code lives in complete isolation no matter how decoupled or micro-services is. And that is what causes bugs.

Actually, there is another SOLID principle that addresses just that issue. It is called OCP or Open-closed principle and it states:

Software entities should be open for extension, but closed for modification.

So the actual consequences of violating LSP principle is actually OCP violation. OCP, on the other hand, states that you should construct your code in a way that, when change is needed you would have to than extend your code, rather than to modify it. It is a principle that consist of two parts:

a) closure or protection of modifications

b) opened for extensions

We want to prevent modifications not because we are lazy, but because we can acknowledge that modification can cause undesired behavior.

That's why we use different abstraction techniques - we simply wrap up pieces of code with abstraction, we surround them with abstraction - so that we can protect entire regions of code that might be using it. Later we can extend what is abstracted without having to change what might be using it. Of course, no code can ever be completely closed for modifications, that's why we choose our project closures strategically and that's why those closures are called strategic closures.

If it sounds complicated, just think of encapsulation. Because encapsulation is using basically same exact principle: using public interface we have abstracted our implementation and thus protected regions of code that might be using it so that we can extend our implementation without having to modify calling code.

But, we have already broken encapsulation and thus violated OCP in the first place by simply using inheritance over different features. And now, we have broken OCP again, simply by violating LSP.

Doesn't look good so far - 2 principles, 3 violations. One principle twice. Let's see are there any SOLID principle violations...

To iterate again - in our example Class B implements Feature 2 and inherits Class A that implements Feature 1.

So, there is a lot of public interfaces, is there any principle that concerns with interfaces. Yes there is actually. It is called ISP or Interface segregation principle and it states:

Clients should NOT be forced to depend upon interfaces that they do not use

We have already demonstrated how and why is Class B (and its implemented Feature 2) tightly coupled to Class A (and its implemented Feature 1) . Nasty thing about tight coupling is that when you use one thing that is tightly coupled to another, you will inevitably be depended on both of those things. Such is a nature of tight coupling.

So, yes, we have another violation. 3 principles, 4 violations so far. Let's continue.

Next and last of the principles would be DIP or Depenency Inversion Principle. This principle also as well as OCP consists of two part and it states:

A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend on details. Details should depend on abstractions.

In our example with Class A and B - high-level module would be Class B and it actually depends on lower level module which is Class A. None of them depend on any abstraction. So there is no abstraction in the first place to not be depended on details. Details, however, which is our implementation of features - do not depend on any abstraction because there isn't any.

To be honest, I've lost count of how many violations would that be. Let's just call it one violation for simplicity. All in all a lot violations. One is too many. But this much. This is way too much. There is still one SOLID principle that I haven't mention and this one isn't actually violated ... surprisingly. Nevertheless it is time for conclusion.

Conclusion

Object-oriented design is very much like learning to play drums. Easy to understand, easy to start at first, just surround your code with class and you're good to go. But, it is pretty hard perfect. Well, actually, let's put it this way: it depends what is your aimed system quality attribute, and you can't have them all and some of them are even in contradiction. However, most people are aiming for things things like maintainability, agility, changeability and test-ability. Those are all the things that good design can deliver, so, ignoring it, in sake of agility would be foolish mistake.

As far as inheritance goes, it is and it always will be one of the fundamentals of OOP and it plays very important role. However, there are some rules of good design. First thing that people learn about software architecture is that there are no clear answers and everything is trade-off. Well, that kind of thinking, in my opinion has lead to fear of analysis paralysis and subsequently abandonment of design process which has actually hurt agility in software development claiming that it brings agility. So, having said that, I'd like to propose simple rule on how and when use inheritance properly:

Does that covers all cases and are there going to be exceptions? No doubt, in software architecture there are never clear answers and there is always some trade-off. But that doesn't mean we shouldn't design software before start coding.

TLDR;

Inheritance. Just, don't. Except if it is from abstract or substitutable class.

To view or add a comment, sign in

More articles by Vedran B.

Insights from the community

Others also viewed

Explore topics