Composition vs. Inheritance: Pros and Cons

Where do composition and inheritance fit in Object Oriented Programming?

Bright
Geek Culture
7 min readAug 20, 2022

--

Photo by GR Stocks on Unsplash

This tutorial will look at inheritance and composition, which is right for a use case. The sample codes are in Java, but the underlying principles are the same for all programming languages that support Object Oriented Programming (OOP). Before diving in, we first have to understand the terms. Let’s begin with inheritance.

Definition

In object-oriented programming, inheritance is the mechanism of basing an object or class upon another object (prototype-based inheritance) or class (class-based inheritance), retaining similar implementation. — Wikipedia

There’s plenty of English going on, but for its simple meaning, inheritance makes it possible for a class to extend its functionalities to other classes. It mimics what we have in the real world called hereditary, where kids take features from their parents.

Unsurprisingly, the class that extends another class is called a childclass or subclass, and the class that’s extended is the parentclass or superclass (all pun intended). Inheritance has an “is-a” relationship.

For example, a lion comes from the cat family, so a lion “is-a” cat. A hyena, likewise. They both have paws and inherit their prey instincts. Yet, each has unique features that distinguish it from the other. It is typical of subclasses in OOP with different methods specific to them alone.

Composition involves parts that make up the whole. A car is never complete without a steering wheel, an engine, or seats. For this, we say a car “has-a” steering wheel. The first noticeable difference between inheritance and composition is the “is-a” and “has-a” relationships.

Let’s create three classes and see how they relate to each other via inheritance.

Inheritance in action

The above code is a simple abstract Plain Old Java Object (POJO) with five instance variables, a constructor that initializes all variables, and an abstract earnings method. It’s a requirement in Java to have a constructor for the subclasses to invoke it during object instantiation. We explicitly created the constructor to pass all the instance variables as parameters.

Please pay close attention to the private access modifier we used here and their corresponding setters and getters. We chose private as part of OOP best practices to avoid breaking encapsulation.

Note: This isn’t a tutorial on OOP, so I’ll defer a detailed explanation to a later blog.

Now let’s create two subclasses, FullTimeEmployee and ContractEmployee.

FullTimeEmployee class

The above code snippet is a subclass that extends the Employee class and overrides the earnings method, nothing fancy here. It also has an extra instance variable, “overtimeAllowance,” whose getter it uses to compute the earnings.

Now, let’s have a look at the other subclass.

ContractEmployee class

This code looks nearly identical to the FulltimeEmployee class. The only differences are it has no extra field and computes the earnings method differently.

Ripple effect of a simple change in the abstract class

Let’s assume after a routine audit of the financials, the auditors found out the company was losing money to ghost names, so they decided to remedy the situation. The Software Architects came up with a brilliant strategy that would help solve the problem by introducing another instance variable, “isAccountExpired,” to prevent the continuous payment of employees who have left the company.

This single change calls for a modification in the superclass and all its accompanying subclasses. Let’s change per the new requirement as follows:

Note: Only class definitions, method signatures, and new changes are in this code snippet. The ellipsis “…“ everywhere denotes code continuation.

The new Employee class

The new FullTimeEmployee class

The new ContractEmployee subclass

Do you get the drift? A simple change in the superclass affects all the subclasses down the hierarchy chain. We only had two subclasses for our use case, so it wasn’t a big deal. Imagine hundreds of subclasses in a large codebase inherited from the Employee class. Your guess is as good as mine.

Let’s do things differently. We will extract the accountNumber into its class and pass it to the Employee class. With this single change, we’re delving into the realms of the “has-a” relationship.

Composition in action

The above code is a simple account class with three instance variables and a constructor.

And now, we modify the Employee class accordingly. Find the source code with the latest changes below:

In hindsight, we wrote more lines of code than we already had. So what’s the catch? Why are we trying to solve the inheritance problem by introducing a whole Account class?

If you’re thinking along these lines, your concerns are justified. You initially write a decent amount of code, but as the requirements keep changing, that’s where composition shines.

In our last Sprint Planning, the Product owner told us the users wanted a name field in their Account class. We only modify the account class, and every other class remains.

Advantages of inheritance

Suppose you can guarantee the only place you set your data is from a corresponding set method and get method for getting data. In that case, you can reduce the time spent debugging your code because you know that only those methods would be responsible for manipulating such data.

It also comes in handy when you decide to modify the implementation details of your methods. You would only need to alter the methods instead of making changes to many different places in the superclass and subclasses of the code base.

Last but not least, if you’re sure you’re never going to modify your superclass, inheritance may be the right fit. It trims down the number of extra classes you’d need with composition.

Disadvantages of inheritance

“Inheritance is a powerful way to achieve code reuse, but it is not always the best tool for the job. Used inappropriately, it leads to fragile software.” — Effective Java, by Joshua Block.

From our examples above, it is clear that the subclasses are tightly coupled to the superclass, which violates the O in SOLID principles. Changes in the superclass would break changes until all of its descendants have been modified accordingly. The best way to avoid unnecessary tight-coupling is by introducing composition wherever possible.

Again, composition enhances code reuse a great deal. Any class that would want to use the Account class is free to do so due to its loose-coupling nature. Passing it as a reference, we can invoke all account’s public methods, which were in-existent prior.

Another good reason to favor composition over inheritance is unit testing. If an instance of an Account class is unknown, we can mock it with some fake data and carry on with our test. It is easier this way than with the inheritance approach.

Conclusion

This tutorial taught us what inheritance and composition are and their differences. To conclude, use composition wherever possible and only use inheritance sparingly. The main downside with composition is the number of lines of code you’d write. Yet that is compensated for by how loosely coupled and flexible the code would look in the long run.

If you enjoyed this article, please follow my handle Bright for more stories on Software Development and tech in general.

--

--