OOP Concepts · Object Relationships

Composition over Inheritance

“Favour composition over inheritance” is one of the most quoted design rules. Instead of inheriting behaviour from a parent, an object holds other objects that do the work — and you can swap them freely.

Intermediate 6 min Lesson 20 of 38 Runnable examplesExamples use modern JavaScript (ES2022 private fields) and C# 13.
0/38 in OOP Concepts
On this page · 6 min · Intermediate

In simple words#

Build with LEGO pieces instead of carving one statue. Combining small parts (composition) is easier to change than a deep family tree (inheritance).

Composition
Build objects from other objects.
Deep inheritance
Long parent chains that are hard to change.
Flexibility
Swap parts at runtime.

Learning objectives#

  • Explain the class-explosion problem with inheritance
  • Build behaviour by combining objects
  • Swap behaviour at runtime
  • Know when inheritance is still the right tool

Explanation#

Inheritance fixes behaviour at compile time and supports only one parent. Combine two features — petrol or electric, with or without GPS — and you need PetrolCar, ElectricCar, PetrolCarWithGps, ElectricCarWithGps. Each new feature doubles the classes.

Composition gives the car a powerSource object and an optional gps object. Any power source and any GPS can be combined — two small classes per feature, not a multiplying tree.

Composed parts can be replaced at runtime (fit a GPS later) and tested on their own.

Changes to a base class ripple into every subclass (the “fragile base class” problem). Composed parts only interact through small interfaces, so changes stay local.

Inheritance is still right for true, stable is-a relationships with shared behaviour — a SavingsAccount is an Account. Use composition for “has a” and “can do”.

Syntax#

C# Illustrative
public interface IPowerSource { string Start(); }
public interface INavigator { string Route(string to); }


public class Car(string name, IPowerSource power, INavigator? gps = null)
{
    public string Start() => 
quot;
{name}: {power.Start()}";
public string Navigate(string to) => gps?.Route(to) ?? "no GPS fitted"; }

Code example: Cars built from parts#

JavaScriptCars built from parts Runnable
// Inheritance would need: PetrolCar, ElectricCar, PetrolCarWithGps, ElectricCarWithGps...
// Composition: small parts, combined freely.


class PetrolEngine {
  constructor(hp) { this.hp = hp; }
  start() { return `engine ${this.hp}hp started`; }
}


class ElectricMotor {
  constructor(kw) { this.kw = kw; }
  start() { return `motor ${this.kw}kW humming`; }
}


class Gps {
  route(to) { return `route to ${to}`; }
}


class Car {
  constructor(name, powerSource, gps = null) {
    this.name = name;
    this.power = powerSource;   // has-a power source
    this.gps = gps;             // has-a (optional) GPS
  }
  start() { return `${this.name}: ${this.power.start()}`; }
  navigate(to) { return this.gps ? this.gps.route(to) : 'no GPS fitted'; }
}


const city = new Car('City', new PetrolEngine(120), new Gps());
const nexon = new Car('Nexon EV', new ElectricMotor(105));


console.log(city.start());
console.log(nexon.start());
console.log(city.navigate('Pune'));
console.log(nexon.navigate('Pune'));


nexon.gps = new Gps();          // fitted later, at runtime
console.log(nexon.navigate('Mumbai'));
Expected output
City: engine 120hp started
Nexon EV: motor 105kW humming
route to Pune
no GPS fitted
route to Mumbai
Opens this example in the full playground

Step-by-step explanation#

  1. PetrolEngine and ElectricMotor both have start(), so the car can use either.
  2. Car does not inherit from an engine; it holds one and delegates to it.
  3. The optional GPS is another part. No GPS → a sensible message instead of a new class.
  4. Fitting a GPS later is just assigning a new part — impossible with inheritance, where the class is fixed.

Real-world use case#

ASP.NET Core services are composed: an OrderService has an IOrderRepository, an IPaymentGateway and an ILogger. Swapping SQL Server for Cosmos DB means injecting a different repository, not rewriting a class hierarchy.

Common mistakes#

Inheriting just to reuse a method

class Report extends Logger to get log() is wrong — a report is not a logger. Give the report a logger instead.

Deep inheritance trees

Five levels of subclasses make every change risky. Flatten them and move varying behaviour into composed parts.

Going to the other extreme

Composition everywhere can create many tiny classes for no reason. Use inheritance when the is-a relationship is real and stable.

Best practices#

  • Ask “is-a or has-a?” before writing extends
  • Put varying behaviour into small, swappable parts
  • Depend on interfaces for those parts
  • Keep inheritance hierarchies shallow (1–2 levels)

Remember this#

Has-a beats is-a when behaviour varies.

Inheritance multiplies classes; composition adds them.

Parts can be swapped at runtime; parents cannot.

Practice exercise#

Your task: Build a Character that has a weapon part. Create Sword (attack returns "slashes for 10") and Bow (attack returns "shoots for 7"). Make a character, attack, switch weapon, attack again.

Need a hint?
Character.attack() returns `${this.name} ${this.weapon.attack()}`.
Show solution
JavaScript Runnable
class Sword { attack() { return 'slashes for 10'; } }
class Bow { attack() { return 'shoots for 7'; } }
class Character {
  constructor(name, weapon) { this.name = name; this.weapon = weapon; }
  attack() { return `${this.name} ${this.weapon.attack()}`; }
}
const hero = new Character('Arjun', new Sword());
console.log(hero.attack()); // Arjun slashes for 10
hero.weapon = new Bow();
console.log(hero.attack()); // Arjun shoots for 7
Opens this example in the full playground

Mini quiz#

Composition over Inheritance quizMultiple choice Question 1 of 3

What problem does composition solve that inheritance creates?

Score so far: 0/0

Want more? Practise all OOP Concepts MCQs · OOP Concepts interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.