Clean Code: Composition Over Inheritance

In object-oriented programming, we often think in terms of inheritance. A Dog is an Animal. A Car is a Vehicle. This is the "is-a" relationship, and it's a powerful way to model the world. But it has its limits.

What happens when you have a Penguin? A penguin is a Bird, but it can't fly. If your Bird class has a fly() method, you end up with a confusing situation. This is where composition comes in. Instead of saying a thing is a another thing, we can say it has a capability.

This leads to the principle: prefer composition over inheritance. It gives you more flexibility and avoids the problems of rigid class hierarchies.

The Problem with Deep Inheritance

Deep inheritance chains can be brittle. A change in a base class can have unintended consequences in its subclasses. Let's look at the classic bird example.

// Bad: A rigid inheritance hierarchy
class Animal {
  move() { /* ... */ }
  eat() { /* ... */ }
}

class Bird extends Animal {
  fly() { /* ... */ }
}

class Penguin extends Bird {
  // This is awkward. Penguins are birds, but they can't fly.
  // This violates the Liskov Substitution Principle.
  fly() {
    throw new Error("Penguins can't fly!");
  }
}

class Duck extends Bird {
  swim() { /* ... */ }
}

// Good: Using composition to build objects from capabilities
interface Movable {
  move(): void;
}

interface Flyable {
  fly(): void;
}

interface Swimmable {
  swim(): void;
}

class Bird {
  private movable: Movable;
  private flyable?: Flyable;
  private swimmable?: Swimmable;
  
  constructor(movable: Movable, flyable?: Flyable, swimmable?: Swimmable) {
    this.movable = movable;
    this.flyable = flyable;
    this.swimmable = swimmable;
  }
  
  move() {
    this.movable.move();
  }
  
  fly() {
    if (this.flyable) {
      this.flyable.fly();
    }
  }
  
  swim() {
    if (this.swimmable) {
      this.swimmable.swim();
    }
  }
}

With composition, we can create a Duck that can move, fly, and swim, and a Penguin that can move and swim, without any confusing inheritance problems.

The Strategy Pattern: A Form of Composition

The Strategy Pattern is a perfect example of composition in action. Instead of using inheritance to define different behaviors, you use composition to inject them at runtime.

Let's say you're building a payment processor.

// Bad: Using inheritance for different payment methods
abstract class PaymentMethod {
  abstract processPayment(amount: number): void;
}

class CreditCardPayment extends PaymentMethod {
  processPayment(amount: number) {
    // Credit card specific logic
  }
}

class PayPalPayment extends PaymentMethod {
  processPayment(amount: number) {
    // PayPal specific logic
  }
}

// Good: Using composition and the Strategy Pattern
interface PaymentStrategy {
  processPayment(amount: number): void;
}

class CreditCardStrategy implements PaymentStrategy {
  processPayment(amount: number) {
    // Credit card specific logic
  }
}

class PayPalStrategy implements PaymentStrategy {
  processPayment(amount: number) {
    // PayPal specific logic
  }
}

class PaymentProcessor {
  private strategy: PaymentStrategy;
  
  constructor(strategy: PaymentStrategy) {
    this.strategy = strategy;
  }
  
  setStrategy(strategy: PaymentStrategy) {
    this.strategy = strategy;
  }
  
  processPayment(amount: number) {
    this.strategy.processPayment(amount);
  }
}

With this setup, you can easily add new payment methods without changing the PaymentProcessor. You just create a new class that implements the PaymentStrategy interface. This is the Open/Closed Principle in action, enabled by composition.

When to Choose?

  • Inheritance (is-a): Use it when a class is a true subtype of another. Think CheckingAccount and SavingsAccount as subtypes of BankAccount. They share a core identity.
  • Composition (has-a): Use it when you want to give an object a certain capability. Think of a Car that has an Engine. It's more about roles and responsibilities than identity.

Favoring composition leads to more flexible, modular, and maintainable code. It's a powerful tool to have in your clean coding toolbox.