In my early years as a software developer the moment I have learned something new I was eager to use it all around my projects. Right about that time the concept of inheritance hit me stronger than before and I was like “That’s cool! Let’s use it everywhere :tada:”.
Remember the first time you have read the book “Design Patterns: Elements of Reusable Object-Oriented Software” and afterwards trying to use design patterns everywhere in your code being overly convinced that you are doing things “the right way”. Same thing but instead of patterns it was inheritance.
And so it began. Inheritance invaded all the code that showed any signs of shared logic. I was extracting common methods into base classes to achieve reusable design. At first all was working out well: the code was DRY and reuse was flourishing all over it. Some doubts about that approach began to appear after a few weeks or so. I have succeeded to build a hierarchy of classes that served a single purpose - group common methods into base classes. As you all might have heard or experienced personally software changes. The moment I needed to make a change in any of the base classes I had to go to all specializations down the hierarchy and adapt to that change. The first time that I did that, I was: “Ok, I guess that’s the dues one has to pay to write reusable code”. That OOP book said inheritance is good. The book cannot be wrong. So I continued to do what the book says. By the fifth time I changed something in my base classes and had to propagate that change throughout my tree of objects, I said to myself: “Enough is enough. Inheritance cannot solve my duplicate logic issues. There has to be a better way to write reusable object-oriented software.” As the years passed by and my experience as a developer grew, it turned out that there are many ways to achieve that reusable design and inheritance have proved to be causing more pains than gains most of the times.
The thing I was doing back then actually has a real name - Refused Bequest:
Someone was motivated to create inheritance between classes only by the desire to reuse the code in a superclass but the superclass and subclass are completely different.
It is a good design practice is to eliminate any unnecessary duplication in your code base. I want to stress on the word “unnecessary” as some times it is OK to have some code duplications in order to keep your abstractions clean. As duplication is far cheaper than having the wrong abstraction. But ultimately you have to make sure that each concept in your design is expressed only once as code.
Designing object-oriented software is hard, and designing reusable object-oriented software is even harder.
That was stated in the book “Design Patterns: Elements of Reusable Object-Oriented Software” in 1994 and today it still holds true.
An example of duplication
class Order def total_amount line_items.reduce(0) do |total, item| total + (item.count * item.price) end end end class Invoice def total_amount line_items.reduce(0) do |total, item| total + (item.count * item.price) end end end
Here we have duplication in two classes that have nothing in common semantically but have the same repeating logic for calculating the total amount. A head first idea would be to pull up a method into a superclass and call it a day.
Reuse through inheritance
class Base def total_amount line_items.reduce(0) do |total, item| total + (item.count * item.price) end end end class Order < Base end class Invoice < Base end
Ok, that code is bad and smells. I cannot even think of a good name for the
superclass. Now every time I had to change something in either
Invoice line item calculation, I had to make sure that the change does not
break the other class. I can create some sort of type check inside the base
class but that would break the Liskov substitution
principle and our
code will no longer be SOLID. Inheritance creates tight coupling among all the
classes in the hierarchy chain. Specializations have access to methods and state
of their ancestors. If you change a method in a base class you have to adapt to
that change in all the specializations. Child objects inherit responsibilities
from their parents and combine them with their own responsibility. Objects have
many responsibilities violating the Single Responsibility Principle (SRP). There
is no encapsulation between children and parents. Your hierarchy of objects may
explode if you have many independent pieces depending on a base class. For
example, let’s assume we have a ConnectionBase class abstracting database
connectivity. On top of that you have multiple independent functionalities such
as: caching, pooling, multiple engines, etc. If you try to solve such a puzzle
with inheritance by splitting functionalities into base classes and creating a
new specialization for each need, then you end up in a combinatorial explosion.
That combinatorial hell is much more clearly expressed in some frameworks than
others. Take for example React.js. If you try to share code through inheritance
in React you are shooting yourself in the foot as you will end up with infinite
number of components. That’s why those guys reach out to patterns like
render props and
Reuse through mixins
In Ruby you can solve that puzzle and share code among your abstractions using mixins. But the principles discussed here do not apply solely to the Ruby language. The same guidelines are relevant for other languages and frameworks, for example - React.js mixins.
module Base def total_amount line_items.reduce(0) do |total, item| total + (item.count * item.price) end end end class Order include Base end class Invoice include Base end
Now you do not have to create a new specialization for each combination that you need. Still this solution does not provide good encapsulation. Code is DRY but logic is still mixed up. Everything is still tightly coupled together. If you decide to change a module, then you have to go to all the classes that include it and change them. If the module depends on the object internal state then things can get even uglier. You are building one giant footgun, my friend. Implicit dependencies, name clashes, snowballing complexity, you name it. The issues with this approach go on and on.
In Ruby including a module is inheritance.
I would not advise anyone to extract methods out of a class into mixins solely to DRY out shared logic. I may reach out to mixins only when I have some cross-cutting concerns, like logging, auditing and security. Still as a rule of thumb: I would avoid them.
Reuse through inheritance in Rails
In Rails things may get really really messy when abusing inheritance. First, because of Rails magic. Second, because of Rails global state. It’s a fact that within your controllers, views and helpers you have access to session, to params, to cookies, to request and response objects and to any instance variable you set in any controller. Normally one request hits one controller action. But for the Rails magic to happen that controller needs to inherit from ApplicationController. So if you happen to assign an instance variable inside the ApplicationController then it will magically appear in all your controllers, views and helpers throughout your whole application. People also tend to abuse inheritance among Rails controllers by abstracting common code into their own base controllers. For example, if you are building a user management application with Rails you may be tempted to do something like this: UsersController inherits from ResourcesController inherits from ApplicationController. Setting an instance variable anywhere in the base of this chain will make it show up exponentially in the number of controllers you derive from the base ones. When the application gets bigger you will end up in the constant pain of “Where did that variable come from?”. And that’s what we call an unmaintainable situation.
For example, one guy could do this to render the impersonated account user in the header of the application layout:
class ApplicationController < ActionController::Base def impersonate_user @user = current_account.user end end
Or do the assignment in a callback:
class ApplicationController < ActionController::Base before_action :impersonate_user def impersonate_user @user = current_account.user end end
Another guy could do this to get the user to manage from the request:
class UsersController < ApplicationController def assign_user @user = User.find params[:id] end end
or even worse:
class UsersController < ApplicationController def assign_user @user ||= User.find params[:id] end end
because why not doing some premature optimizations via memoization.
Declaring an instance variable in a base controller will make you live in a constant fear that someone may re-assign that variable in a subcontroller and use it for something else, like in my example above. There would be no errors in production. But I can assure you that you will have really weird behavior and some serious security holes within your application. In short: avoid inheritance in Rails. Have in mind that including modules or concerns is also inheritance so you will be better off avoiding those guys as well. Instead, hide your state behind service objects and let your controllers call out to them to do the heavy lifting.
How to solve the user impersonation problem?
class ApplicationController < ActionController::Base # nothing to see here, go away human end module UsersHelper def impersonated_user current_account.user end end class UsersController < ApplicationController def assign_user @user = User.find params[:id] end end
A helper to the rescue. We can use a view helper in our header partial to render the impersonated user. And by using it in the header partial, I mean: call it in the application layout and pass it as a local to the header partial. No state is set anywhere in the inheritance chain. The UsersController can safely assign and manage the user from the request.
Reuse through composition
Let’s go back to summing line items amounts.
class TotalAmountCalculator def sum(items) items.reduce(0) do |total_amount, item| total_amount + (item.count * item.price) end end end class Order def initialize(calculator) @calculator = calculator end def total_amount @calculator.sum line_items end end class Invoice def initialize(calculator) @calculator = calculator end def total_amount @calculator.sum line_items end end
In this example, we have created a separate abstraction
that know only how to calculate a total from a list whose elements respond to
price. It does not care if the caller is
apples or cucumbers. It just performs the math through a clear interface. Now
Order and the
Invoice classes can change internally as much as they want
without affecting each other as long as they keep providing the same list that
TotalCalculator expects. We used Dependency
Injection to inject the
total calculator into the Invoice and Order. That would make our life much
easier when unit testing, as compared to if we have had used a static method. In
short: we have decoupled our code.
With composition you can put each piece of functionality into its own class. Then combine them all together into a working solution. That way each responsibility is isolated and encapsulated. Objects use one another through clear interfaces and never mix each other’s guts through internal state and non-public methods. They are loosely coupled. Each class is a black box to its container. If you change something inside one box no other box has to adapt to that change, because the second box works only with the first box public interface and does not know (and does not care) about the changes in its internal implementation. Each class has a single responsibility and only one reason to change. You build your abstractions around your business domain objects. You are not trying to pull common logic out of your code and build a hierarchy. Such a system where code is reused through composition and components talk to each other only through clear public interfaces is much closer to achieving reusable object-oriented software design.
Following SRP on each one of your abstractions can result in having a large number of small pieces that you have to fit together to build a bigger system. That gives you flexibility and DRYness but also can be harder to follow along and understand. There is a very good article on the topic that will definitely help you when deciding between larger or smaller bricks to use in your compositions: The Lego Way of Structuring Rails Code.
Reuse through decorators
Another good pattern to rescue you from using inheritance is the Decorator pattern. Using decorators is more flexible because you can mix responsibilities in any combination and even nest or chain them to achieve unlimited possibilities. Since the decorator pattern is too bigger topic to fit in here, it deserves a blog post on its own. Also let’s keep this article focused on composition vs inheritance and not explore all the possibilities and patterns on writing reusable code.
Reuse through strategies
Yet another good pattern to rescue you from the drawbacks of inheritance is the Strategy pattern. You may easily replace subclasses with strategy classes. Put the implementations in separate strategies and expose them behind a common interface. This approach shouts for stricter separation of concerns and easier testing. At the end of the day you have code that tolerates change. While using inheritance changing code becomes more and more difficult as you add more behaviour to the inheritance tree.
Inheritance is not evil
Despite of everything discussed and pointed out in this article, inheritance is not evil. There are many cases when it is safe to use it. The rules you have to follow to be on the safe side are:
- Shallow and narrow hierarchy. You do NOT want the inheritance hierarchy to be deep or wide.
- The subclasses should be the leaf nodes of your object graph, at the very edge of it. Being at the end they won’t know anything about other things.
- The subclass should use all the behaviour of the superclass. As we don’t want code smells like Refused Bequest in our beautiful garden of objects.
Let’s repeat that again. Inheritance is not evil. People who abuse inheritance are :-)
Favor object composition over class inheritance.
Inheritance creates tight coupling among your classes. Compose complex systems from small abstractions around your business domain concepts.