Chapter 05
Behavioural patterns: varying behaviour
Patterns for swapping algorithms, changing behaviour with state, fixing a skeleton of steps, and adding operations to object structures.
Strategy
Also called policy, pluggable algorithm
- What
- Intent: define a family of algorithms behind one interface and choose one at run time.
- Why
- When: you have if-else chains choosing how to do something, such as pricing rules, sorting or shipping cost.
type Pricing = (base: number) => number;
const regular: Pricing = (b) => b;
const student: Pricing = (b) => b * 0.8;
const sale: Pricing = (b) => Math.max(b - 500, 0);
function total(base: number, pricing: Pricing) {
return pricing(base);
}State
Also called state machine, finite state machine
- What
- Intent: an object changes its behaviour when its internal state changes, as if it changed class. Each state is its own object.
- Why
- When: many methods switch on the same status field, such as an order that is pending, paid, shipped or cancelled.
interface OrderState { ship(o: Order): void; }
class Paid implements OrderState {
ship(o: Order) { o.state = new Shipped(); }
}
class Pending implements OrderState {
ship() { throw new Error('Pay first'); }
}Watch out
Mixing it up with Strategy: the client picks a strategy, while states switch themselves.
Template Method
Also called hook methods, skeleton algorithm
- What
- Intent: a base class fixes the steps of an algorithm and lets subclasses fill in some of them.
- Why
- When: several variants share the same outline, such as importers that all read, validate and save but parse differently.
abstract class Importer {
run(path: string) {
const rows = this.parse(read(path)); // varies
rows.forEach(validate); // fixed
save(rows); // fixed
}
protected abstract parse(raw: string): Row[];
}Visitor
Also called double dispatch
- What
- Intent: add new operations to a fixed set of classes without changing them, by moving each operation into a visitor object.
- Why
- When: the class hierarchy is stable but operations keep growing, such as compilers walking syntax trees to type-check, print or optimise.
interface Visitor<R> { num(n: Num): R; add(a: Add): R; }
class Num { constructor(public v: number) {}
accept<R>(v: Visitor<R>) { return v.num(this); } }
class Add { constructor(public l: Expr, public r: Expr) {}
accept<R>(v: Visitor<R>) { return v.add(this); } }Watch out
Using it when new classes are added often: every new class forces a change to every visitor.
Interpreter
Also called grammar, expression tree
- What
- Intent: represent the grammar of a small language as classes and evaluate sentences by walking the tree.
- Why
- When: you have a simple, stable mini-language such as filter rules, feature flags or formulas.
- Example
- A rule like
country = "PK" and plan = "pro"becomes an And node holding two Equals nodes, each with anevaluate(user)method.
Watch out
Growing it into a full language by hand; use a parser generator once the grammar gets serious.