Too much of a good thing? Composition over inheritance

6 min readPublished July 24, 2018Updated May 02, 2022

I recently heard a story about a sous chef in training. He had no experience in a kitchen, and the head chef was getting frustrated by his lack of seasoning and bland dishes. The head chef told the sous chef, "I want you to add salt until you think you’ve added too much and then add some more. I won’t be happy until you have someone send the food back for being too salty.” It took a few hours, but eventually a patron sent back their food. The head chef had proved his point, and the sous chef learned how to properly season his food and the limits of seasoning.

This is how I feel about the programming mantra “composition over inheritance,” where composition is like salt in the kitchen. Experienced chefs know to add salt early and often as salt enhances the natural flavor of most foods, but there can be too much of a good thing.

Before we start, let’s clarify what we mean when we talk about inheritance and composition. Inheritance is when a subclass may use the fields and methods of its parent class. A subclass can override the fields and methods of its parent class to change the functionality. Composition is when a class holds a reference to another object in a field and delegates some responsibility to that object.

“Composition over inheritance” has become one of the most accepted best practices in our industry. And when I learn about the way things “should” be done, I tend to dive in head first and take it to an extreme. Inheritance is bad? I stop using inheritance altogether. Composition is good? I compose anything and everything, even if that means I created an object for a one line method. While this is very obviously not the right way to go, I learn a lot very quickly.

Anything that can be modeled using inheritance can be modeled using composition and this interchangeability can be tricky to get right. We’ve all heard the IS-A vs HAS-A coding idiom and this is a great rule of thumb, but it can be an oversimplification. When some coders (including me) hear that composition is better than inheritance, its really easy to start turning IS-A’s into HAS-A’s in our heads. My experience using composition to such an extreme has taught me that inheritance gets a bad rap in the programming world, but like anything, it has a time and a place. And composition is not a panacea. It can very quickly turn around and bite us.

Inheritance can be extremely useful to add semantic meaning to our code. If we are modeling true IS-A relationships, inheritance is our friend. A new team member will open our code and be able to derive meaning from the inheritance structure. This speeds up onboarding and understanding when our inheritance tree truly models our domain structure.

Inheritance is especially useful when we have a family of objects that share the majority of the same business logic and the differences between them can be thought of as “tweaks.” Tweaks between parents and children should be used to add functionality to the children classes and tweaks between children should be small and localized. If we find ourselves removing functionality as we move around the inheritance tree, we should reevaluate if inheritance is the right solution to this particular problem. And if we find ourselves wanting to share functionality between siblings and cousins, then either our inheritance structure is wrong (less likely) or composition would be a better solution for the problem we are trying to solve (more likely).

To make inheritance successful, we should keep our inheritance trees shallow. The deeper our tree goes, the more consequences we will suffer if we need to make a change near the base of the tree. Its very easy to violate the single responsibility principle if we rely too heavily on inheritance. Even when we’re encapsulating common logic in the parent, our classes should still be small and have high cohesion (keeping similar and related things together and to the point). If we find ourselves with large classes that have a lot of what I like to call “handle methods” then we have probably violated the single responsibility principle. (“Handle methods” are those methods that we can’t come up with a good name for so they all look like “handleWhatever”)

Composition is a great alternative solution to make our code loosely coupled and highly cohesive. When using composition to structure our code, conforming to the single responsibility principle becomes less labor intensive. Creating small classes and composing them together within our objects creates code that welcomes changing requirements. If requirements change and we've used composition to smartly structure our code, we can implement a new instance of the interface and plug that new implementation in place of the old one to meet our changed requirements.

The cost of composition can become too high when our composition trail is winding around and around in our code and becoming difficult to track. If we find ourselves opening file after file to trace what should be a simple solution, then we may be taking the “composition over inheritance” mantra a tad too far. When we can no longer hold the mental model in our brain space, it is overcomplicated. I once worked on a project that took composition so far that no one in the company actually understood how the code fit together, and every story I was assigned included time to unravel the dependency structure and understand what it was that I was trying to build.

Hybrid solutions using both inheritance and composition tend to be the most usable in real-world code. For example, let’s look at how we would model event planning.

Let’s have a base class for Event and some subclasses that inherit from it. Our base Event class will maintain some logic for a guest list to keep track of who we have invited and what they’ve RSVPed.

class Event
  def invited_guests
    @guests
  end

  def attending_guests
    @guests.find_all { |guest|  guest.attending? }
  end

  def not_attending_guests
    @guests.find_all { |guest|  !guest.attending? }
  end
end

Then for our purposes, let’s say that we have two types of events, Celebration and Meeting. This is an example of inheritance doing what its intended for, a Celebration is an Event and a Meeting is an Event that both have RSVPs to keep track of.

Now let’s add some functionality to one of our subclasses. Most Celebrations have decorations so could add a field to our Celebration class for decorations. This field maintains a list of objects that implement a decoration interface. Meetings do not require decorations so we do not want to put the Decorations field in our base Event class.

Adding the decoration field is an example that highlights the small, additive changes we want to make in child classes when we’re using inheritance and also how to use composition effectively. At a birthday party, we want balloons and streamers but at our Halloween party, we want cobwebs and backlights. By delegating responsibility out to each decoration, we don’t bloat the celebration objects with logic specific to decorations such as decoration cost, supplies needed to hang them, where to purchase them and anything else that decorations care about.

class Celebration < Event
  attr_reader :decorations

  def initialize
    @decorations = []
  end

  def add_decoration(decoration)
    @decorations << decoration
  end

  def decoration_budget
    @decorations.sum {|decoration| decoration.cost } 
  end
end
class Meeting < Event
  attr_reader :agenda

  // ... code omitted for brevity

end

Find related posts:Programming concepts

Well-Rounded Dev

Liked this post? Subscribe to receive semi-regular thoughts by email.

    I won't send you spam. Unsubscribe at any time.