Inheritance vs. Composition

In object-oriented programming, inheritance and composition are two techniques for relating our classes and providing code reusability.

Inheritance is where we inherit properties and functionality from a parent class that we can reuse or overwrite in our child class. In addition, our child classes can define additional functionality that isn’t defined in the parent class.

On the other hand, composition is where we construct our classes by composing smaller pieces or components. In JavaScript, this can be achieved by creating functions that can be appended to our classes as needed.

So which technique should we use when designing software? I will explain that in this post.

Inheritance

Inheritance was introduced to programming in 1969 and was popular for some time. Class hierarchies could be created to allow for code reuse between classes:

Inheritance Example 1

The cry(), eat() and drink() methods of the Dinosaur class are inherited by the Tyrannosaurus and Triceratops classes. They are able to use them without having to explicitly define them. Additionally, we can overwrite the cry() method in each class to match our child class.

Inheritance can be thought of as an “is a” relationship:

  • A Tyrannosaurus is a dinosaur.
  • A Tiger is a cat.

This seems great. We can reuse code from our parent class and be more efficient, right? Yes.. but there are drawbacks to this approach.

Issues With Inheritance

Something I find funny is that in school, we were taught inheritance, how it is used and the benefit it provides. However, no one thought to mention the drawbacks of using it or why it might not be a good choice when designing enterprise software. In my opinion, school hardly prepares you for what real coding is like. Going back, I would probably have enrolled in a coding bootcamp. Okay, enough with the rant, back to the post!

Let’s say we had an Animal parent class that was used in a large application, maybe a video game that had a lot of different kinds of animals. Similar to the Dinosaur class, the Animal class has eat() and drink() methods, but it has a sleep() method as well:

Inheritance Example 2

Imagine if we had many classes inheriting from our Animal class and the sleep() method was being called in many places throughout the application. Then, we come to find out that there are some animals that don’t need sleep, like dolphins and bullfrogs.

Disclaimer: It is debatable whether dolphins do or don’t need sleep. Technically, they do “sleep” but it is different because they have to be conscious to breathe. So both halves of their brain alternate between sleeping and being active. For the sake of this example, let’s say dolphins don’t need sleep.

We then decide to change our Animal class to not have a sleep() method and instead only implement it in the child classes that need it. However, this would require a lot of work and code changes. This is the main issue with inheritance, it causes tightly coupled code. Changes to the parent class can cause havoc throughout the codebase where that class is used and inherited from. Also, inheritance hierarchies are very rigid; once they’re implemented, they’re hard to change later without rewriting the application entirely.

Further, a lot of baggage comes with inheritance. In JavaScript, inheriting means you extend the functionality of the immediate parent object, but also any functionality in the prototype chain. You may end up having objects with a lot of functionality not appropriate to them. Or you may end up with bulky objects known as “god objects”.

Additionally, inheritance requires you to commit to a rigid design early in development when it isn’t clear how the application will change. It forces you to predict the future in a sense. But any developer can tell you that software requirements are rarely set in stone. They almost always change.

On top of all of this, there aren’t that many real world scenarios that fit the inheritance relationship.

So now that I’ve convinced you that inheritance is bad and evil, let’s see what composition has to offer.

Composition

While inheritance allowed us to inherit functionality from another class, composition allows us to create components that we can add to our class as we need it:

Composition Example 1

The benefit of this approach is that it’s much more flexible if we decide to make changes to our class. The code is loosely coupled. And of course, we still get the benefit of code reusability.

Earlier, I said that inheritance can be thought of as an “is a” relationship. Composition, on the other hand, can be described as a “has a” relationship:

  • A computer has a CPU.
  • A human has a heart.

Going back to our “animal application”, if we had implemented it using composition instead of inheritance, we could have simply just removed the sleep() method from the class instead of having to make changes to a class hierarchy.

In a situation where inheritance would be beneficial, composition can usually be used instead for the same benefit but with less risk.

React.js Uses Composition

Something interesting I discovered while writing this post, React.js advocates “composition over inheritance” and adheres to the principle when designing the React library.

React props are used to propagate objects down to nested components. This allows reuse of functionality without having the rigidness and fragility that comes with inheritance. React suggests that if you want to reuse non-UI functionality, then it is best to create JavaScript modules that can be imported instead of extending the class.

If you’re new to React.js, check out my post Intro to React.js.

The Final Verdict

You may be thinking that composition should always be used and that inheritance is useless. Inheritance does work well in certain situations, but it is important to consider the drawbacks. This is especially true when designing the architecture for a larger application.

Composition should be used in the majority of cases. In most cases where inheritance can be used to provide benefit, composition could be used just as easily with fewer risks and less rigidity. So keep that in mind. Treat each case individually and remember that design decisions like this will become easier with experience.