Do you remember the first time you read the "Design Patterns" book? I remember mine. I can never forget that feeling – every problem has to be solved using an existing pattern from the book. Patterns quickly overrun my projects, turning simple concepts into monstrous aberrations. In my early days as a developer, I had a similar experience, but at that time, it was inheritance, not design patterns.
I was young, confused, and overloaded with conflicting information on how to create object-oriented designs. I didn't have many tools in my toolbelt, but still I was passionate to craft reusable abstractions. As all the OOP books aggressively promote inheritance, I made up my mind that it was the right instrument to get rid of duplications.
And so it began – inheritance invaded any code that showed signs of shared logic. I extracted common methods into base classes and turned them into reusable pieces. That approach was working well at the very start – my code seemed DRY. I have created a hierarchy of classes that served a single purpose – group common methods into base classes.
Certain doubts about that approach began to arise after a few weeks when new requirements came in, and I had to make changes. When I had 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 I had to do so, I was: "OK, I guess that's the dues one has to pay to write reusable code".
That OOP book claims inheritance is the right path. The book cannot be wrong. So I continued to do what the book said. By the fifth time I changed something in a superclass and had to adapt to that change throughout my tree of objects, I said to myself – "Enough is enough. Inheritance is not the right instrument to solve the duplicate code problem. There has to be a better way to create reusable object-oriented designs."
As the years passed and I gained more experience, I found many other ways to achieve reusable design. Inheritance was causing more pain than gain. The thing my younger self 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.
Code duplication
It is a good practice to eliminate any unnecessary duplication in your codebase. Let's stress the word "unnecessary", as sometimes it is OK to have duplications to keep our abstractions clean. Duplication is cheaper than having the wrong abstraction.
Still, your goal is to express each concept in your domain only once as code. Designing object-oriented software is hard. 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 is still a valid statement.
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
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
The code smells. I cannot find a good name for the superclass, as it is responsible for different use cases. When a requirement change for invoicing comes, that change should happen in the base class. But since orders also depend on that functionality – how do we change the implementation for invoices without breaking the orders?
Inheritance creates tight coupling among 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 must adapt to that change in the specializations. Child objects inherit responsibilities from their parents and combine them with their responsibility. Objects have many responsibilities violating SRP. There is no encapsulation between children and parents. Data is no longer hidden.
The hierarchy of objects explodes when you have many independent pieces
depending on a base class. Take, for example, 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 that puzzle by splitting functionalities into base classes and creating a
new specialization for each responsibility, you end up in a combinatorial
explosion.
That combinatorial hell is clearly expressed in some libraries like React. If
you try to share code through inheritance in React, you will end up with an
infinite number of components. That's why patterns like render props
and
high-order components
emerged.
Reuse through mixins
In Ruby, we can share code using mixins. We can include a module within a class definition. The module's instance methods become available as methods in the class. They get mixed in. Mixed-in modules behave as superclasses. The mixins concept can be found in other languages (Scala, PHP traits) and libraries (React). The advice about using mixins is relevant to any programming language that supports multiple inheritance (C++, Python).
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
Including a mixin is inheritance. The approach doesn't provide encapsulation. The code is DRY but tightly coupled. If you need to change the module, you have to go to all the classes where it's included and adapt. Things get uglier if the module depends on the class's internal state. The problems with mixins are implicit dependencies, name clashes, and snowballing complexity. As a rule of thumb – avoid mixins and multiple inheritance.
Reuse through inheritance in Rails
In Rails, things can get messy when abusing inheritance. Consider the Rails global state – within your controllers, views, and helpers, you have access to the session, HTTP params, cookies, request/response, and to any instance variable you assign to a controller.
When you abstract common code into a BaseController
, and assign an instance
variable, it will appear not only in all controllers that inherit from it but
also in the views and helpers. You may do something like this:
class BaseController < ActionController::Base
def impersonate_user
@user = current_account.user
end
end
And then use it like this:
class UsersController < BaseController
def assign_user
@user = User.find params[:id]
end
end
Or even worse:
class UsersController < BaseController
def assign_user
@user ||= User.find params[:id]
end
end
Declaring an instance variable in a base controller makes you constantly fear that someone may re-assign that variable in a sub-controller and use it for something else. The code above presents a serious security hole.
Reuse through composition
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
We have created a separate abstraction TotalAmountCalculator
. It knows only
how to calculate a total from a list whose elements respond to count
and
price
. It doesn't care if the caller is Order
or Invoice
or anyone else.
It just performs the math through a clear public interface.
We have decoupled our objects. Order
and Invoice
can change as they need
without affecting each other. We have injected the total calculator into the
Invoice
and Order
, making testing easier – we can replace it with a
test double if needed.
With composition, you put each piece of functionality into its own class and then compose those classes together into your object-oriented design. Each responsibility is isolated. Data and internal guts are hidden. Objects use each other through clear public interfaces. They never mix internal logic through state or protected methods. They are loosely coupled.
A class is a black box to its dependant. If you change something inside one box, no other box has to adapt to that change. You build your abstractions around your domain concepts. You compose a system where code is reused through composition, and components talk to each other only through clear public interfaces.
Inheritance is not evil
There are many cases when it is fine to use inheritance. My rules 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 the system. Being at the edge, they won't know about your domain concepts.
- The subclass should use the behavior of the superclass. A clear is-a relationship. Avoid any code smells (Refused Bequest).
Takeaway
Favor object composition over class inheritance. Inheritance creates tight coupling among your classes. Compose complex systems from small abstractions around your business domain concepts like a Lego.
References
- Practical Object-Oriented Design In Ruby, by Sandi Metz
- Favor composition over inheritance, from Effective Java by Joshua Bloch
- The Lego Way of Structuring Rails Code
- Replace Inheritance with Delegation
- Mixins Considered Harmful by Dan Abramov