iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Classes vs Composition

Class design patterns: composition over inheritance, dependency injection, value objects, and the choices that age well.

Design patterns — class design

EXAMPLE
// ===== Composition over inheritance =====
// Inheritance creates tight coupling between parent + child.
// Composition lets you change behaviour at runtime + test pieces in isolation.

// Bad (inheritance for code reuse):
class Animal {
  speak() { /* abstract */ }
  walk() { console.log('walking'); }
}
class Bird extends Animal {
  speak() { console.log('chirp'); }
  fly() { console.log('flying'); }
}
class Ostrich extends Bird {
  fly() { throw new Error('cannot fly'); }   // LSP violation
}

// Good (composition):
class FlyingAbility { fly() { console.log('flying'); } }
class WalkingAbility { walk() { console.log('walking'); } }

class Sparrow {
  constructor() {
    this.flying = new FlyingAbility();
    this.walking = new WalkingAbility();
  }
}
class Ostrich2 {
  constructor() {
    this.walking = new WalkingAbility();
  }
}

// ===== Dependency Injection =====
// Inject dependencies via constructor; do not new them inside.

class OrderService {
  constructor(repo, clock, emailer) {
    this.repo = repo;
    this.clock = clock;
    this.emailer = emailer;
  }
  async place(order) {
    order.created_at = this.clock.now();
    await this.repo.save(order);
    await this.emailer.send(order.email, 'order.placed', order);
  }
}

// Tests inject fakes:
new OrderService(
  { save: async () => {} },
  { now: () => new Date('2024-04-10') },
  { send: async () => {} },
);

// ===== Value objects (no identity, equal by value) =====
class Money {
  constructor(cents, currency = 'AUD') {
    if (cents < 0) throw new Error('negative');
    this.cents = cents;
    this.currency = currency;
    Object.freeze(this);
  }
  equals(other) {
    return other instanceof Money
      && other.cents === this.cents
      && other.currency === this.currency;
  }
  plus(other) {
    if (other.currency !== this.currency) throw new Error('currency mismatch');
    return new Money(this.cents + other.cents, this.currency);
  }
}

// Immutability + value equality + invariants in the constructor.

// ===== Entities (identity, change over time) =====
class User {
  constructor(id) { this.id = id; this.name = ''; }
  equals(other) { return other instanceof User && other.id === this.id; }
  rename(name) { this.name = name; return this; }
}

// ===== Static factory methods =====
class Order {
  static create(customer, lines) {
    if (!lines.length) throw new Error('empty');
    const total = lines.reduce((s, l) => s + l.price * l.qty, 0);
    return new Order(crypto.randomUUID(), customer, lines, total);
  }
  constructor(id, customer, lines, total) {
    this.id = id; this.customer = customer; this.lines = lines; this.total = total;
  }
}

// Static factories enforce invariants AT construction.

// ===== Private fields =====
class Counter {
  #count = 0;          // private (browser + Node ESM)
  increment() { this.#count++; }
  get value() { return this.#count; }
}

// ===== Abstract / Template method =====
class Pipeline {
  async run(input) {
    const stage1 = await this.parse(input);
    const stage2 = await this.transform(stage1);
    return this.serialise(stage2);
  }
  parse(_)     { throw new Error('abstract'); }
  transform(x) { return x; }                 // default
  serialise(x) { return JSON.stringify(x); }
}

// ===== Patterns to internalise =====
// - Composition over inheritance for code reuse
// - DI by constructor; never service locators
// - Value objects for invariants; entities for identity
// - Static factories for creation rules + clear intent

// ===== Pitfalls =====
// - Inheritance for code reuse (extracts code, couples classes)
// - Massive class with many responsibilities
// - Public mutable fields -> invariants slip
// - Service locator / global singletons hiding real dependencies

Why it matters

Class design rewards small reflexes: composition over inheritance, DI by constructor, value objects for invariants, entities for identity, static factories for creation rules. Skip the inheritance ladder unless the language demands it; prefer immutable values; let tests prove the wiring.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Favour composition over inheritance.
// Inherit only when the subclass IS-A real specialisation.
// Compose when one object USES another's behaviour.
Try it Yourself »

Discussion

Loading…