Chapter 03
Structural patterns
The seven patterns that compose classes and objects into larger structures while keeping them flexible.
Adapter
Also called wrapper, translator
- What
- Intent: convert the interface of an existing class into the one your code expects.
- Why
- When: plugging in a third-party library or legacy module whose method names and shapes do not match yours.
interface Sms { send(to: string, text: string): void; }
class VendorSmsAdapter implements Sms {
constructor(private api: VendorClient) {}
send(to: string, text: string) {
this.api.dispatchMessage({ msisdn: to, body: text });
}
}Bridge
Also called handle and body
- What
- Intent: split an abstraction from its implementation so the two can vary independently.
- Why
- When: two dimensions would multiply subclasses, such as shapes times renderers, or remotes times devices.
interface Renderer { circle(r: number): void; }
class Circle {
constructor(private r: number, private out: Renderer) {}
draw() { this.out.circle(this.r); }
}
new Circle(5, new SvgRenderer()).draw();
new Circle(5, new CanvasRenderer()).draw();Composite
Also called tree structure, part-whole
- What
- Intent: compose objects into trees and treat single objects and groups the same way.
- Why
- When: data is naturally a tree, such as files and folders, UI components or an organisation chart.
interface Node { size(): number; }
class File implements Node {
constructor(private bytes: number) {}
size() { return this.bytes; }
}
class Folder implements Node {
children: Node[] = [];
size() {
return this.children.reduce((s, c) => s + c.size(), 0);
}
}Decorator
Also called wrapper
- What
- Intent: add behaviour to an object by wrapping it in another object with the same interface.
- Why
- When: you need combinations of extras, such as logging, caching and retries, without a subclass for every mix.
interface Store { get(k: string): Promise<string>; }
class CachedStore implements Store {
private cache = new Map<string, string>();
constructor(private inner: Store) {}
async get(k: string) {
if (!this.cache.has(k))
this.cache.set(k, await this.inner.get(k));
return this.cache.get(k)!;
}
}Watch out
Wrapping so many layers that the order of decorators becomes a hidden source of bugs.
Facade
Also called simplified interface
- What
- Intent: one simple entry point in front of a complicated subsystem.
- Why
- When: callers need a common task done without learning five classes, such as "place order" over stock, payment and email.
class Checkout {
constructor(private stock: Stock, private pay: Payments,
private mail: Mailer) {}
async placeOrder(cart: Cart) {
await this.stock.reserve(cart.items);
await this.pay.charge(cart.total);
await this.mail.receipt(cart.email);
}
}Flyweight
Also called shared state, intrinsic and extrinsic state
- What
- Intent: share the common, unchanging part of many similar objects instead of storing it in each one.
- Why
- When: you have huge numbers of small objects, such as characters in an editor or trees in a game map, and memory matters.
- Example
- A forest of a million trees stores the mesh and texture once per species; each tree keeps only its position and size.
Proxy
Also called surrogate, lazy loading, protection proxy
- What
- Intent: a stand-in object with the same interface that controls access to the real one.
- Why
- When: you need lazy loading, access checks, remote calls or rate limiting without changing the real class or its callers.
class AuthorisedReports implements Reports {
constructor(private real: Reports, private user: User) {}
export(id: string) {
if (!this.user.isAdmin) throw new Error('Forbidden');
return this.real.export(id);
}
}Watch out
Confusing it with Decorator: a proxy controls access to the same behaviour, a decorator adds behaviour.