Why Composition Over Inheritance?
We’ve all heard the phrase “composition over inheritance,” but I don’t agree with it.
Composition and inheritance are equivalent logical relationships—they represent “has-a” and “is-a” respectively. There’s no question of one being “greater” than the other. It’s like chili peppers and potatoes: maybe stir-fried chili with meat tastes better than stir-fried potato with meat, but can you say chili peppers are greater or superior to potatoes?
A programmer’s first line of code is often printing 'hello world', because their original dream is to change the world with code. These code-obsessed nerds believe the real world runs on certain rules, and abstracting and simulating those rules is something programmers love to discuss and never tire of.
For example, to implement a bird, programmers think it’s simple. Birds all need to eat, and the characteristic of birds is that they have wings and can fly.
This kind of simulation of a real‑world object can be elegantly implemented like this:
class Bird {
eat(){}
fly(){}
}
Later, we need to simulate a chicken. A chicken is clearly a type of bird. Although free‑range chickens spend most of their time on the ground, they can still barely be considered as able to fly.
To keep the code concise and elegant, we can directly reuse the Bird code and add a crow method on top of it.
class Chicken extends Bird {
constructer(){
super()
}
// crow (rooster's call)
crow(){}
}
After implementing many types of birds with inheritance, a problem arises.
Now we need to implement an ostrich. People might grudgingly accept that chickens, ducks, and geese can fly, but saying an ostrich can fly is a bit too much.
There are two solutions now.
Solution 1: Remove flying from the Bird class.
Clearly, the ostrich proves that flying is not a universal trait of birds. The advantage of this solution is that it solves the problem at its root. The disadvantage is the huge scope of impact: a large number of birds inherit Bird and will therefore lose the ability to fly; you might have to check every bird implementation one by one and add the flying ability back.
This kind of change is like discovering a problem with the foundation after the house is almost built. Even if the developer, for the sake of code elegance, is willing to tirelessly go through the trouble, the tester won’t be happy. A small change on your side means a sharp increase in their workload.
Solution 2: Handle the ostrich specially by overriding the fly method in the ostrich class so that when an ostrich tries to fly, the code throws an error.
The advantage of this solution is a small scope of change. The disadvantage is that the code becomes ugly and dangerous. If one day the code is handed over to another developer who, ignoring the situation, makes all birds fly, that will cause a disaster. They’ll surely curse while checking Git commit history to find out who added that smelly piece of code.
So later, some big names from the engineering‑practice camp proposed “composition over inheritance.”
Composition Over Inheritance
Since not all birds can fly, what definitely can fly? Things that can fly can fly.
So the big names used simple philosophical thinking to create a Flyable class that facilitates composition.
interface Flyable {
fly(): void;
}
If a chicken needs to fly, it only needs to have a Flyable member.
class Chicken extends Bird implements Flyable {
constructor(private flyer: Flyable = new WingFlight()) {}
fly() { this.flyer.fly(); }
}
An ostrich cannot fly, so there’s no need to add a Flyable member.
class Ostrich extends Bird {
constructor() {
super();
}
run() { console.log("Racing across the desert..."); }
}
Even an airplane can fly, and it can also elegantly reuse flying code.
class Airplane implements Flyable {
constructor(private flyer: Flyable = new JetFlight()) {}
fly() { this.flyer.fly(); }
}
This compositional approach is like breaking down code functionality into LEGO bricks. The process of implementing functionality is like building with bricks.
Those of you who have experience building with LEGO know that the smaller and finer each individual brick is, the greater the freedom and possibilities of the final product. The larger the brick units, the less freedom. The same applies in software engineering: to avoid the “fragile base class problem” mentioned earlier, each functional module should be broken down as finely as possible. The benefit is more flexible code, easier reuse, and simpler testing.
But the downside is also obvious: overly fine‑grained decomposition leads to boilerplate code and duplicate code. The definition of types in the program becomes blurry—a car is no longer that means of transport but becomes a big sofa plus four wheels. A motorcycle becomes a small sofa plus two wheels. Such code is not easy to maintain when handed over to other developers; they might interpret “small sofa plus two wheels” as an electric bicycle or some other black‑smoke‑spewing theft tool, thus forcing it onto the bicycle lane, depriving it of road rights and refueling rights.
Conclusion
When talking about object‑oriented programming, people still mention “encapsulation, inheritance, polymorphism.” But “composition over inheritance” is more of an engineering practice than an OOP feature. In fact, you don’t even need classes to elegantly compose many functional pieces of code. React components are an example.
A common input component, for instance, is composed of a background box, an input component, a separator line, and several icon components. Each part can work independently or be organically integrated. This is undoubtedly the best example of “composition over inheritance.”
However, early versions of React components exemplified the concept of “inheritance” correctly. No matter what you say, custom components inherit from the Component class—the inheritance relationship is clear and unshakable.
Inheritance expresses the “is-a” logic, while composition expresses the “has-a” logic. There is no superiority or inferiority here. It’s just that in engineering practice, because requirements are always uncertain and changing, assertions of “is-a” become rare, while the use of “has-a” becomes increasingly widespread.
The phrase “composition over inheritance” may contain too many developers’ patience in the face of changing requirements, and their repeated indulgence toward product managers, bosses, and clients. Finally, gritting their teeth, they force two concepts that should be parallel to be ranked as superior and inferior.