DEV Community

Cover image for Composition Over Inheritance: A Flexible Design Principle
Athreya aka Maneshwar
Athreya aka Maneshwar

Posted on

Composition Over Inheritance: A Flexible Design Principle

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 a Vehicle).
  • Composition models “has-a” relationships (a Car has an Engine, Wheels, and a SteeringSystem).

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:

  1. Identify behaviors as interfaces (Movable, Drawable, Collidable).
  2. Implement components for each behavior (Visible, Solid, NotVisible, Movable, etc.).
  3. 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 */ }
};
Enter fullscreen mode Exit fullscreen mode

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; };
Enter fullscreen mode Exit fullscreen mode

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); }
};
Enter fullscreen mode Exit fullscreen mode

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()) {}
};
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (3)

Collapse
 
citronbrick profile image
CitronBrick • Edited

On a completely tangential note, coming from a primarily Java background,
it never sat well with me that, access modifiers are at the same level of indentation as class modifiers in C++ & C#.
This post reminded me of it after a long time.

Collapse
 
dola_akhter_f2cb24aed7a48 profile image
Dola Akhter

I enjoy the global reach Virturo provides for trading

Some comments may only be visible to logged-in visitors. Sign in to view all comments.