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…