“Use inheritance to extend the behavior of your classes”. This concept is one of the most widespread, yet wrong and dangerous in OOP. Do yourself a favor and stop using it right now.
Are you using inheritance in your code? Stop right now. You’re only hurting yourself.
I know, this is not what everyone has been teaching for decades. Using inheritance for behavioral composition and polymorphism is a common piece of knowledge you find in every OOP 101 book or blog post.
Sadly, it’s wrong.
Using inheritance is not the only way to extend a class behavior, but definitely is the most dangerous and harmful one.
Imagine you write a class with several methods and properties. Now imagine you or someone else extends that class. Do you realize what just happened by doing that?
Coupling has been created between two concrete classes.
One change in the base class could affect the behavior of the child.
But that’s fine — you could say — I can handle it.
Let’s continue then. Imagine now that the base class is extended by a second class, and this class is in turn extended by another one. Can you still handle it?
What happens when you have to change the behavior of the base class or another class in the middle of the inheritance tree? Are you confident enough you are not breaking anything?
Difficult, isn’t it?
What about testing each class? Do you think you can do it without involving anything but the System Under Test? I don’t think so.
Well, let me tell you one thing: this is exactly how bad monoliths are born. One inherited class after another.
Using inheritance is not the only way to extend a class behavior. But definitely is the most dangerous and harmful one.
Is there a better alternative to inheritance?
Of course there is.
It’s about mastering a precise design philosophy that automatically avoids the complexity inheritance brings in.
Believe or not, I do not use inheritance in the 99% of my code. And I really mean 99%.
How do I achieve that number, you may ask? I apply the following rules to my code:
- I use only two type of class-level declarations: interfaces and final classes;
- I inject interfaces in dependent classes’ constructors;
- I don’t allow any class dependency to be injected other than interfaces;
- I use a Dependency Injection Container (or an equivalent method, depending on which language I’m coding with) to handle the creation of my instances;
- If I feel I’m injecting too many dependencies in a class, I rethink my design in terms of class responsibilities and using the interface segregation principle;
- When required, I split complex behavior in multiple final classes implementing the same interface;
- I use inheritance only, and I mean only, when it makes sense on a semantical level and only for extension purposes, without any base behavior change;
- I allow myself to break the previous rules only in the 1% of my code.
Being able to apply these rules doesn’t happen over night. You need practice and discipline before you internalize them. Your brain will be busy trying to comply with them, leaving few space for the design of your code.
Allow yourself some days to get used to the new coding style, because it’s only when you get more familiar that the magic happens. And this is what you will start seeing in your codebase:
- clear contracts; using interfaces will force you to think in term of communication between objects;
- thinking “communication first” will give your brain free resources by splitting its job in two steps: design time and implementation time; our brain doesn’t like to work on two different things at the same time, especially switching continuously between the two;
- isolated, side effect free code units; injecting interfaces only as dependencies will remove every nasty side effect around the code you are working on; changing an interface implementation won’t affect other concrete classes, since they depend only on the interface abstraction;
- testability; mocking dependencies is extremely easy when they are interfaces; no more “I forgot to disable the constructor / mock that concrete method” troubles in your System Under Test;
- low, manageable complexity; as everything is isolated, you won’t need to worry about rippling changes; this dramatically decreases the complexity of your code;
- low cognitive load; with decreased complexity, your brain will be free to focus on what matters;
- code fluidity; removing any unnecessary coupling, you will be able to move things around way more easily than before;
- confidence in yourself; being able to test your code in isolation so well will give you a wonderful sense of confidence in changing it; no more hours of manual clicking around for testing, finger-crossed releases and Friday afternoon hot fixes.
You should stop using inheritance right now. You don’t need it. You don’t want it in your codebase.
What you should start doing instead:
- use interfaces to define the contracts between your classes;
- use final classes to implement behavior for those interfaces;
- use composition (through constructor dependency injection) to put things together and prevent complexity.
Your codebase will thank you.
Your manager will thank you (actually she will probably say “wow, you were so fast! And no bugs appeared!”).
Your team members will thank you.
And your future self will thank you too.