DEV Community

Gabriele Buffolino
Gabriele Buffolino

Posted on • Originally published at bite-sized.hashnode.dev on

Inheritance vs composition: a fight against Egyptian gods

We all heard about the "favor composition over inheritance" mantra, but only a few have read Thoughtworks' article on the matter, by Steven Lowe. What I am going to do in this bite is to describe what would happen if this principle is ignored.

Unfortunately, I have seen this happening a lot of times, unveiling a common misunderstanding of how inheritance should be used.

Pyramids and gods

Everything starts with a conversation that goes like this:

Alice: "Hey, those two classes have something in common."

Bob: "Yeah, let's create a common base class to maximize code reuse!"

Do it once. Do it twice. Do it for a sprint. Do it for a whole quarter. Now each class exposes a few methods to subclasses in addition to every other method exposed by its superclasses. And there's a fairly good amount of classes. What was initially a simple hierarchy turned out to be a fearsome pyramid, staring grumpily at you as you try to understand what's the reason for that AbstractThingWithProxyWithoutUserManagerBase to exist.

Even worse, you may find that some subclasses just override superclass methods with empty implementations or use other tricks to subtract functionality from the base class.

And here begins the nightmare. You need that WithProxy thing, but you already subclassed AbstractSpaceRocketWithChewingGumsInThePocketHandler and that WithProxy is just too far away in the pyramid. You hear the Sphynx laughing at you: "You'll never solve this riddle!". You are tempted to refactor only a bit of that pyramid, but you're unsure where and how to start. This is Seth threatening you: "Don't you dare! Hail the holy pyramid or everything will crawl down!"

"I need to deliver the feature!", you cry. "My customer agreed on a deadline!". Of course a tight deadline. So you brace yourself, dry the tears from your eyes and start massaging the pyramid to spot a hole where you can pinpoint your new class. You find it! Feature done! The customer will be happy! You lie to yourself: "I paid attention, I know I did my best! This will work!". But you don't really know what you just did and only guessed and bet for your solution to be right. Sphynx is still laughing, and Seth hails the now bigger pyramid.

The ancient Egyptian gods won once again.

A pragmatic short-term solution

We need to visualize our goals:

  1. We need to deliver a feature now

  2. We need to be able to deliver more features in the future

To reach goal 1, we need to pave the way to reach goal 2 and take a first step toward it. This is because if I want to deliver a feature in the future, then the code around will hopefully be good enough to let me do it easily. So, if I want to deliver a feature now when the code is not good enough, I have to accommodate it to be good enough for the feature to be implemented properly.

In our short example, we could extract that WithProxy in a utility class and use it wherever is needed, breaking the verticality of inheritance and favoring the horizontality of composition.

In the long run, the pyramid will get shorter and our Egyptian gods will stop laughing at us.

The ideal solution

The issue here is that inheritance was used as the default approach to code reuse, while we should carefully evaluate what to use case by case. When Alice and Bob started arguing to pull the code up the hierarchy, we should have replied: "Why don't we segregate the common code in a class and establish a dependency relationship instead?". This is the start of the composition approach.

The takeaway

Understanding the difference between is-a relationship versus has-a is crucial to make a proper decision and the Thoughtworks' article is a must-read on the topic.

Inheritance's main goal is to model domains, not to reuse code. Code reuse comes as a side effect. Always use composition if you just want to share some common code.

But if you want a rough rule of thumb just to start over, here is a decision flow in six steps:

  1. Read Thoughtworks' article

  2. Read Thoughtworks' article

  3. Did you read Thoughtworks' article?

  4. Do you want just to put some code in common? Go with composition. Common code is a dependency, a has-a relationship.

  5. Are you modeling entities for the same domain? Go with inheritance. Modeling entities falls inside the is-a hat (beware to not cross your domain boundaries and to not subtract functionalities from base classes. Subclasses must be functionality additives.)

  6. Something else? In doubt? Go with composition by default and move to inheritance only when you figure out you actually need inheritance.

Top comments (0)