Hi there! I'm Maneshwar. Currently, I’m building a private AI code review tool that runs on your LLM key (OpenAI, Gemini, etc.) with flat, no-seat pricing — designed for small teams. Check it out, if that’s your kind of thing.
In object-oriented programming (OOP), one of the most common questions developers face is how to reuse code effectively.
Traditionally, inheritance has been the go-to mechanism: a class extends another, reusing its methods and properties while adding or overriding behavior.
But inheritance, while powerful, comes with pitfalls—tight coupling, rigid hierarchies, and difficulty in adapting to new requirements.
To address these challenges, the principle of composition over inheritance has emerged as a cleaner, more flexible alternative.
Why Not Just Use Inheritance?
Inheritance allows classes to share code, but it also creates a strong dependency between child and parent classes.
If a parent class changes, every subclass is affected.
Over time, deep inheritance hierarchies can become brittle and hard to maintain.
For example, imagine modeling different types of game objects:
- A
Player
that can move, collide, and be drawn. - A
Cloud
that moves and is visible, but doesn’t collide. - A
Building
that is visible and solid, but doesn’t move.
Using inheritance, you’d either face multiple inheritance (which can lead to diamond problems) or end up creating endless variations like VisibleAndMovable
, VisibleAndSolid
, and so on. This quickly becomes unmanageable.
What Is Composition Over Inheritance?
The principle of composition over inheritance suggests that instead of building hierarchies where objects inherit behavior, you build objects by assembling smaller components that each implement specific functionality.
In short:
-
Inheritance models “is-a” relationships (a
Car
is aVehicle
). -
Composition models “has-a” relationships (a
Car
has anEngine
,Wheels
, and aSteeringSystem
).
By composing objects from interchangeable components, you keep systems more modular, flexible, and maintainable.
Basics of Composition in OOP
A common approach is to define interfaces for behaviors and let classes implement or delegate them:
-
Identify behaviors as interfaces (
Movable
,Drawable
,Collidable
). -
Implement components for each behavior (
Visible
,Solid
,NotVisible
,Movable
, etc.). - Assemble domain objects by injecting the right combination of behaviors.
This way, changing a behavior doesn’t require rewriting hierarchies—it just means swapping components.
Example: Inheritance vs. Composition
Using Inheritance (C++ Example)
class Object {
public:
virtual void update() {}
virtual void draw() {}
virtual void collide(Object objects[]) {}
};
class Visible : public Object {
public:
void draw() override { /* draw logic */ }
};
class Movable : public Object {
public:
void update() override { /* movement logic */ }
};
class Solid : public Object {
public:
void collide(Object objects[]) override { /* collision logic */ }
};
To define a Player
that is Visible, Movable, and Solid, you’d need multiple inheritance or specialized hybrid classes—leading to complexity.
Using Composition
Instead, define delegates for each behavior:
class VisibilityDelegate { public: virtual void draw() = 0; };
class UpdateDelegate { public: virtual void update() = 0; };
class CollisionDelegate { public: virtual void collide(Object objects[]) = 0; };
Then compose them:
class Object {
VisibilityDelegate* v;
UpdateDelegate* u;
CollisionDelegate* c;
public:
Object(VisibilityDelegate* v, UpdateDelegate* u, CollisionDelegate* c)
: v(v), u(u), c(c) {}
void update() { u->update(); }
void draw() { v->draw(); }
void collide(Object objects[]) { c->collide(objects); }
};
Now, creating objects is just assembly:
class Player : public Object {
public:
Player() : Object(new Visible(), new Movable(), new Solid()) {}
};
class Smoke : public Object {
public:
Smoke() : Object(new Visible(), new Movable(), new NotSolid()) {}
};
This avoids tangled inheritance and allows behavior changes at runtime (swap Movable
with NotMovable
dynamically).
Benefits of Composition Over Inheritance
✅ Flexibility – Behaviors can be mixed and matched without rigid hierarchies.
✅ Maintainability – Fewer dependencies between classes.
✅ Extensibility – New behaviors can be added without restructuring existing code.
✅ Runtime adaptability – Behaviors can be swapped on the fly.
Think of it like building with LEGO blocks instead of carving a single, rigid statue. Components can be rearranged to fit new requirements easily.
Drawbacks and Trade-offs
Of course, composition isn’t perfect:
- It often requires boilerplate forwarding methods, since methods must delegate to components.
- Inheritance can be faster to implement for simple cases where a base class provides most functionality.
- Overusing composition can lead to too many small classes, making code harder to navigate.
A balanced approach is often best—favor composition, but use inheritance where it makes sense (e.g., abstract base classes for shared contracts).
Real-world Examples
- Go and Rust rely heavily on composition instead of inheritance.
- UI frameworks (like React or Flutter) use composition to build complex components.
- Design Patterns (from the GoF book) like Strategy, Decorator, and Adapter are practical applications of this principle.
Conclusion
Composition over inheritance encourages us to think in terms of what an object can do rather than what an object is.
By assembling behavior from modular components, software becomes more adaptable, less brittle, and easier to extend.
Inheritance still has its place, but when designing systems for the long haul, composition often leads to cleaner, more resilient architectures.
LiveReview helps you get great feedback on your PR/MR in a few minutes.
Saves hours on every PR by giving fast, automated first-pass reviews. Helps both junior/senior engineers to go faster.
If you're tired of waiting for your peer to review your code or are not confident that they'll provide valid feedback, here's LiveReview for you.