Files
gruperly/.agents/skills/clean-code-principles/rules/core-composition.md
2026-09-04 16:49:24 -03:00

7.3 KiB

id, title, category, priority, tags, related
id title category priority tags related
core-composition Composition Over Inheritance core-principles critical
composition
inheritance
flexibility
design
solid-srp-class
solid-dip-injection
core-encapsulation

Composition Over Inheritance

Favor composing objects from smaller, focused pieces over building deep inheritance hierarchies. Composition provides more flexibility, better encapsulation, and avoids the fragile base class problem.

Bad Example

// Anti-pattern: Deep inheritance hierarchy

class Animal {
  protected name: string;
  protected energy: number = 100;

  constructor(name: string) {
    this.name = name;
  }

  eat(amount: number): void {
    this.energy += amount;
    console.log(`${this.name} is eating. Energy: ${this.energy}`);
  }

  sleep(hours: number): void {
    this.energy += hours * 10;
    console.log(`${this.name} slept for ${hours} hours. Energy: ${this.energy}`);
  }
}

class Bird extends Animal {
  fly(): void {
    this.energy -= 20;
    console.log(`${this.name} is flying. Energy: ${this.energy}`);
  }
}

class Duck extends Bird {
  swim(): void {
    this.energy -= 5;
    console.log(`${this.name} is swimming. Energy: ${this.energy}`);
  }

  quack(): void {
    console.log(`${this.name} says quack!`);
  }
}

class FlyingFish extends Animal {
  // Problem: Can't inherit from both Bird and Fish
  // Must duplicate flying code or create awkward hierarchy

  swim(): void {
    this.energy -= 5;
    console.log(`${this.name} is swimming. Energy: ${this.energy}`);
  }

  // Duplicated from Bird class!
  fly(): void {
    this.energy -= 20;
    console.log(`${this.name} is flying. Energy: ${this.energy}`);
  }
}

class Penguin extends Bird {
  // Problem: Penguins can't fly but inherit fly()
  // Must override to throw error - LSP violation

  fly(): void {
    throw new Error('Penguins cannot fly!');
  }

  swim(): void {
    this.energy -= 5;
    console.log(`${this.name} is swimming. Energy: ${this.energy}`);
  }
}

// More problems:
// - What about a robot bird? It doesn't eat or sleep.
// - What about a bat? It flies but isn't a bird.
// - Every change to Animal affects all subclasses.
// - Testing requires understanding entire hierarchy.

Good Example

// Correct approach: Composition with focused behaviors

// Define behaviors as interfaces
interface Eater {
  eat(amount: number): void;
}

interface Sleeper {
  sleep(hours: number): void;
}

interface Flyer {
  fly(): void;
}

interface Swimmer {
  swim(): void;
}

interface Speaker {
  speak(): void;
}

// Implement behaviors as standalone classes
class StandardEater implements Eater {
  constructor(private entity: { name: string; energy: number }) {}

  eat(amount: number): void {
    this.entity.energy += amount;
    console.log(`${this.entity.name} is eating. Energy: ${this.entity.energy}`);
  }
}

class StandardSleeper implements Sleeper {
  constructor(private entity: { name: string; energy: number }) {}

  sleep(hours: number): void {
    this.entity.energy += hours * 10;
    console.log(`${this.entity.name} slept for ${hours} hours. Energy: ${this.entity.energy}`);
  }
}

class WingedFlyer implements Flyer {
  constructor(
    private entity: { name: string; energy: number },
    private energyCost: number = 20
  ) {}

  fly(): void {
    this.entity.energy -= this.energyCost;
    console.log(`${this.entity.name} is flying. Energy: ${this.entity.energy}`);
  }
}

class AquaticSwimmer implements Swimmer {
  constructor(
    private entity: { name: string; energy: number },
    private energyCost: number = 5
  ) {}

  swim(): void {
    this.entity.energy -= this.energyCost;
    console.log(`${this.entity.name} is swimming. Energy: ${this.entity.energy}`);
  }
}

// Compose animals from behaviors
class Duck implements Eater, Sleeper, Flyer, Swimmer, Speaker {
  public name: string;
  public energy: number = 100;

  private eater: Eater;
  private sleeper: Sleeper;
  private flyer: Flyer;
  private swimmer: Swimmer;

  constructor(name: string) {
    this.name = name;
    this.eater = new StandardEater(this);
    this.sleeper = new StandardSleeper(this);
    this.flyer = new WingedFlyer(this);
    this.swimmer = new AquaticSwimmer(this);
  }

  eat(amount: number): void {
    this.eater.eat(amount);
  }

  sleep(hours: number): void {
    this.sleeper.sleep(hours);
  }

  fly(): void {
    this.flyer.fly();
  }

  swim(): void {
    this.swimmer.swim();
  }

  speak(): void {
    console.log(`${this.name} says quack!`);
  }
}

// Penguin: swims but doesn't fly - no problem!
class Penguin implements Eater, Sleeper, Swimmer, Speaker {
  public name: string;
  public energy: number = 100;

  private eater: Eater;
  private sleeper: Sleeper;
  private swimmer: Swimmer;

  constructor(name: string) {
    this.name = name;
    this.eater = new StandardEater(this);
    this.sleeper = new StandardSleeper(this);
    this.swimmer = new AquaticSwimmer(this);
  }

  eat(amount: number): void {
    this.eater.eat(amount);
  }

  sleep(hours: number): void {
    this.sleeper.sleep(hours);
  }

  swim(): void {
    this.swimmer.swim();
  }

  speak(): void {
    console.log(`${this.name} says squawk!`);
  }
}

// Flying fish: swims and flies - easy!
class FlyingFish implements Swimmer, Flyer {
  public name: string;
  public energy: number = 100;

  private swimmer: Swimmer;
  private flyer: Flyer;

  constructor(name: string) {
    this.name = name;
    this.swimmer = new AquaticSwimmer(this);
    this.flyer = new WingedFlyer(this, 30); // Different energy cost
  }

  swim(): void {
    this.swimmer.swim();
  }

  fly(): void {
    this.flyer.fly();
  }
}

// Robot bird: flies but doesn't eat or sleep
class RobotBird implements Flyer, Speaker {
  public name: string;
  public energy: number = 100;

  private flyer: Flyer;

  constructor(name: string) {
    this.name = name;
    this.flyer = new WingedFlyer(this, 10); // Efficient robot
  }

  fly(): void {
    this.flyer.fly();
  }

  speak(): void {
    console.log(`${this.name} says BEEP BOOP!`);
  }

  recharge(): void {
    this.energy = 100;
    console.log(`${this.name} recharged to full energy.`);
  }
}

// Functions work with any entity that has the required behavior
function makeEntityFly(flyer: Flyer): void {
  flyer.fly();
}

function feedEntity(eater: Eater, amount: number): void {
  eater.eat(amount);
}

// Works with duck, flying fish, or robot bird
makeEntityFly(new Duck('Donald'));
makeEntityFly(new FlyingFish('Nemo'));
makeEntityFly(new RobotBird('R2D2'));

// Works with duck or penguin, but not robot bird (correctly!)
feedEntity(new Duck('Donald'), 50);
feedEntity(new Penguin('Pingu'), 50);
// feedEntity(new RobotBird('R2D2'), 50); // Type error - RobotBird isn't an Eater

Why

  1. Flexibility: Compose any combination of behaviors. No artificial hierarchy constraints.

  2. Avoids Diamond Problem: No multiple inheritance issues. Just implement multiple interfaces.

  3. LSP Compliance: No need to override methods to throw errors. Types only have methods they actually support.

  4. Reusability: Behaviors can be reused across unrelated types.

  5. Testability: Test behaviors in isolation. Mock specific behaviors easily.

  6. Runtime Flexibility: Can change behaviors at runtime by swapping implementations.

  7. Stable Dependencies: Behavior implementations are stable. Adding new composed types doesn't affect existing code.