This is one of the most important principles in modern software design and many design patterns are also based on it.
Reusability of code is one of the main concerns while designing a system. Whenever we try to reuse code, we should make sure that the entities remain loosely coupled and highly cohesive.
There are two common ways of reusing code:
-
Inheritance
-
Composition
Inheritance
We all know what inheritance is. We create a base class and then use its properties and functionalities to create new classes with additional features and properties.
Let's take an example of birds.
class Bird {
void fly() {}
}Now Sparrow and Eagle can inherit from Bird.
class Sparrow extends Bird {}
class Eagle extends Bird {}This looks perfectly fine because both birds can fly.
Now suppose we want to add a Penguin.
class Penguin extends Bird {}The problem is that Penguin cannot fly.
Since Penguin inherited from Bird, it automatically gets the fly() method even though it should not have that capability.
As the system grows, we start adding more types of birds and more capabilities. Soon we end up with a hierarchy that becomes difficult to maintain.
This is one of the common problems with inheritance. A child class gets everything from the parent even when some of those things are not applicable.
Composition
Instead of inheriting capabilities, we can compose objects using smaller reusable components.
First, let's define behaviors.
interface FlyBehavior {
void fly();
}
class CanFly implements FlyBehavior {
public void fly() {
System.out.println("Flying");
}
}
class CannotFly implements FlyBehavior {
public void fly() {
System.out.println("Cannot Fly");
}
}Now our birds can use these behaviors.
class Bird {
FlyBehavior flyBehavior;
}Sparrow can have flying behavior.
class Sparrow extends Bird {
Sparrow() {
flyBehavior = new CanFly();
}
}Penguin can have non-flying behavior.
class Penguin extends Bird {
Penguin() {
flyBehavior = new CannotFly();
}
}Now every bird gets only the behavior it actually needs.
Instead of saying:
Penguin IS-A Bird that can fly
we are saying:
Penguin HAS-A flying behavior.
This is called composition.
Why Composition is Preferred?
With inheritance:
Bird
├── Sparrow
├── Eagle
├── PenguinAs new requirements come, the hierarchy keeps growing.
With composition:
Bird
|
+---- FlyBehaviorWe can simply plug in different behaviors whenever required.
This makes the system:
-
More flexible
-
Easier to extend
-
Easier to maintain
-
Less tightly coupled
This is why modern software design generally prefers:
Favor Composition Over Inheritance
