Introduction
One of the foundational concepts of object-oriented programming is Inheritance. It aids in creating a "is-a" relationship between things. For instance, a dog "is-a" mammal and a car "is-a" vehicle. On the other hand, Composition is an idea in object-oriented programming that aids in creating a "has-a" relationship. As implied by the name, it combines instances of the same object. For instance, a dog "has-a" barking capacity, just as a car "has-a" engine. The same issue of code repetition is addressed by both Inheritance and Composition, albeit in different ways. While Composition begins with little specialized classes that are joined to make a general class, Inheritance begins with a large generic class that is then broken up into smaller specialized classes.
Limitations of Inheritance
Let us take a look at an example showcasing the limitation of Inheritance. Suppose we have a Mammal class representing mammals. It can have methods such as BreastFeed()
. We create sub-classes for each mammal, such as Cat, Dog, etc., and then there comes the Platypus, an egg-laying mammal - an outlier. Our Mammal class doesn't have any method to lay an egg. If we add such a method to the base class, all other mammals would also have to lay an egg. One could say that we can add a method specific to that Platypus class to lay an egg, but remember, there are other faunas with egg-laying capabilities, such as Reptiles and Birds, which would have egg-laying methods defined in their classes. In the end, we will end with code duplication, the very problem inheritance was supposed to solve.
Looking at the example above, we can see how there still is some level of code repetition when using Inheritance.
Now let's see some of the major flaws of Inheritance.
Code Bloating
When a child class inherits from an existing class, a contract is created that the child class must abide by, which requires it to copy all of the parent class's available attributes, regardless of whether they apply to the child class.
Tight Coupling
After creating a flawless object hierarchy, a little modification to a base class—let's say, the addition or deletion of a method or property—will affect all of our child classes, their child classes, and so on. We'll have to rewrite a sizable portion of the code. Changes are not helpful to Inheritance, especially when things are scaled.
Harder to Test
Tight coupling between the parent and child classes can make it harder to test since we now have to better understand the implementation of the parent class.
Rigidity
Inheritance creates a fixed hierarchy of classes. Once finalized, we will find it hard to change the existing implementations.
Why Composition?
Now that we're done blasting Inheritance let's consider why Composition is a better alternative. Let's continue with our Platypus example. Instead of Inheritance, let's use Composition to define the animals.
In the above example, we described the interface for individual behaviour. Then, we wrote classes that implement those interfaces. We can see that EggLayer
and Reproducer
classes both implement IReproducer
interfaces but have different implementations. Interfaces have allowed us to swap implementations as and when needed. Now, let us look at the Platypus
class. I composed the class with different behaviours that the Platypus
can have. The constructor accepts implementations for the class. We can use different implementations as we need with this approach. There's a special term for what we did in the constructor. It's called a Dependency Injection
. Then, we implemented class-specific methods LayEgg
and BreastFeed
for the platypus class, which actually is an abstraction over the actual implementation. Similarly, we composed the Bat
class with its specific behaviour.
Initially, it might look like we wrote more code than we would write when using Inheritance, but in the long run, we will have to change a lesser amount of code when changes need to be made. Suppose we need to change an implementation for the code for a fly; we would just write a new one and swap it out with the old one without changing any other piece of code.
Composition brings the following advantages to the table:
Flexibility
You don't have to worry about breaking something when you change something in your code. Implementations can be swapped without major changes.
Easy to Test
Since you can swap implementations in and out, you can write mock implementations anytime when you are writing tests for your code.
Loose Coupling
Any changes you make are bound to that class, and will have fewer chances to break other classes. Changing core implementations might break dependent classes, but you would not be doing that very often.
Should You Stop Using Inheritance For Composition?
The answer to this query is ambiguous. Composition offers flexibility, but it also has a lot of boilerplate. For small-scale applications where little modifications are necessary, Inheritance can be acceptable. It might even be the most effective strategy for the given task. However, I believe Composition to be a preferable strategy for scalability. You should research the ideal strategy before beginning a project.
Conclusion
In conclusion, Composition provides flexibility and loose coupling when designing complex systems. It should be preferred over Inheritance when systems are prone to changes. It also makes testing code easier since you can swap out implementations when needed.