Chương 07 · Behavioral — Part 2

Behavioral Patterns — Phần 2 (5 mẫu)

State · Strategy · Template Method · Visitor · Interpreter. Hoàn tất 23 GoF — và phân biệt rõ Strategy vs State, Template vs Strategy.

1. Tổng quan

5 pattern còn lại — phổ biến nhất là Strategy và Template Method. State thường được nhầm với Strategy. Visitor và Interpreter dùng ít hơn nhưng cực mạnh trong domain phù hợp (compiler, AST, DSL).

PatternMột câu
StateObject đổi hành vi khi state đổi — như đổi class
StrategyTách thuật toán thành object thay thế được
Template MethodĐịnh nghĩa skeleton, để subclass điền chi tiết
VisitorTách operation khỏi cấu trúc object
InterpreterĐịnh nghĩa grammar + interpreter cho ngôn ngữ nhỏ

2. State

PATTERN №19 — BEHAVIORAL

State

Cho phép object thay đổi hành vi khi state nội bộ thay đổi. Như đổi class runtime.

Vấn đề Object có nhiều state với hành vi khác nhau, mọi method có if/switch theo state. Lợi ích Mỗi state là 1 class — code rõ, không if/switch khắp nơi. Đánh đổi Tạo thêm class. Cho FSM nhỏ có thể overkill.

2.1. Vấn đề: switch hell

// ❌ Mọi method có switch
class Order {
  private status: 'pending' | 'paid' | 'shipped' | 'delivered' | 'cancelled' = 'pending';

  pay() {
    if (this.status === 'pending') this.status = 'paid';
    else throw new Error('Cannot pay in status ' + this.status);
  }

  ship() {
    if (this.status === 'paid') this.status = 'shipped';
    else throw new Error('Cannot ship in status ' + this.status);
  }

  deliver() {
    if (this.status === 'shipped') this.status = 'delivered';
    else throw new Error('Cannot deliver in status ' + this.status);
  }

  cancel() {
    if (this.status === 'shipped' || this.status === 'delivered')
      throw new Error('Cannot cancel');
    this.status = 'cancelled';
  }
}

2.2. State pattern

// State interface
interface OrderState {
  pay(order: Order): void;
  ship(order: Order): void;
  deliver(order: Order): void;
  cancel(order: Order): void;
}

class Order {
  state: OrderState = new PendingState();

  setState(s: OrderState) { this.state = s; }
  pay()     { this.state.pay(this); }
  ship()    { this.state.ship(this); }
  deliver() { this.state.deliver(this); }
  cancel()  { this.state.cancel(this); }
}

class PendingState implements OrderState {
  pay(order: Order)     { order.setState(new PaidState()); console.log('Paid'); }
  ship(order: Order)    { throw new Error('Cannot ship unpaid'); }
  deliver(order: Order) { throw new Error('Cannot deliver unpaid'); }
  cancel(order: Order)  { order.setState(new CancelledState()); }
}

class PaidState implements OrderState {
  pay(order: Order)     { throw new Error('Already paid'); }
  ship(order: Order)    { order.setState(new ShippedState()); console.log('Shipped'); }
  deliver(order: Order) { throw new Error('Cannot deliver before ship'); }
  cancel(order: Order)  { order.setState(new CancelledState()); /* refund */ }
}

class ShippedState implements OrderState {
  pay(order: Order)     { throw new Error('Already paid'); }
  ship(order: Order)    { throw new Error('Already shipped'); }
  deliver(order: Order) { order.setState(new DeliveredState()); console.log('Delivered'); }
  cancel(order: Order)  { throw new Error('Cannot cancel shipped'); }
}

class DeliveredState implements OrderState {
  pay(order: Order)     { throw new Error('Already done'); }
  ship(order: Order)    { throw new Error('Already done'); }
  deliver(order: Order) { throw new Error('Already delivered'); }
  cancel(order: Order)  { throw new Error('Cannot cancel delivered'); }
}

class CancelledState implements OrderState {
  pay(order: Order)     { throw new Error('Cancelled'); }
  ship(order: Order)    { throw new Error('Cancelled'); }
  deliver(order: Order) { throw new Error('Cancelled'); }
  cancel(order: Order)  { /* no-op */ }
}

Mỗi state là class riêng, tự biết chuyển sang state nào. Không có switch khắp nơi.

2.3. Use cases

  • Order/booking workflow.
  • TCP connection: LISTEN → ESTABLISHED → CLOSED.
  • Game character: Idle → Walking → Running → Jumping.
  • Document workflow: Draft → Review → Published → Archived.
  • Player media: Playing → Paused → Stopped.

2.4. Alternative: state machine library

Cho FSM phức tạp, dùng XState (TS), Stateless (C#) — declarative, có visualization, side-effect quản lý tốt hơn.

// XState style:
const orderMachine = createMachine({
  id: 'order',
  initial: 'pending',
  states: {
    pending:   { on: { PAY: 'paid', CANCEL: 'cancelled' } },
    paid:      { on: { SHIP: 'shipped', CANCEL: 'cancelled' } },
    shipped:   { on: { DELIVER: 'delivered' } },
    delivered: { type: 'final' },
    cancelled: { type: 'final' },
  },
});

3. Strategy

PATTERN №20 — BEHAVIORAL

Strategy

Định nghĩa family of algorithm, đóng gói mỗi cái thành object, làm chúng thay thế lẫn nhau.

Vấn đề Có nhiều thuật toán cho cùng 1 việc (sort, compress, validate, pricing). Client muốn switch runtime. Lợi ích Tách thuật toán khỏi context — Open/Closed. Easy thêm strategy mới. Đánh đổi Client phải biết các strategy để chọn đúng.

3.1. Implementation

// Strategy interface
interface PricingStrategy {
  calculate(order: Order): Money;
}

// Concrete strategies
class StandardPricing implements PricingStrategy {
  calculate(o: Order) { return o.subtotal(); }
}

class VipDiscountPricing implements PricingStrategy {
  calculate(o: Order) {
    return o.customer.isVip() ? o.subtotal().multiply(0.85) : o.subtotal();
  }
}

class HolidayPromoPricing implements PricingStrategy {
  calculate(o: Order) {
    const isHoliday = new Date().getMonth() === 11;
    return isHoliday ? o.subtotal().subtract(Money.of(10)) : o.subtotal();
  }
}

class ClearanceSalePricing implements PricingStrategy {
  calculate(o: Order) { return o.subtotal().multiply(0.5); }
}

// Context
class Checkout {
  constructor(private pricing: PricingStrategy) {}

  setPricing(p: PricingStrategy) { this.pricing = p; }   // có thể đổi runtime

  finalize(order: Order): Money {
    return this.pricing.calculate(order);
  }
}

// Sử dụng:
const checkout = new Checkout(new StandardPricing());
console.log(checkout.finalize(order));

// Black Friday → đổi strategy
checkout.setPricing(new ClearanceSalePricing());
console.log(checkout.finalize(order));

3.2. Function as Strategy (idiomatic JS/TS)

Strategy thường chỉ là 1 function. Class chỉ cần khi strategy có state hoặc method nhiều.

type PricingFn = (order: Order) => Money;

const standardPricing: PricingFn = o => o.subtotal();
const vipPricing:      PricingFn = o => o.customer.isVip() ? o.subtotal().multiply(0.85) : o.subtotal();
const clearancePricing: PricingFn = o => o.subtotal().multiply(0.5);

class Checkout {
  constructor(private pricing: PricingFn) {}
  finalize(o: Order) { return this.pricing(o); }
}

3.3. Use cases mạnh

  • Sort: Array.sort(compareFn) — compareFn là Strategy.
  • Auth: BasicAuth, OAuth, JWT — same interface.
  • Compression: gzip, brotli, deflate.
  • Payment: Stripe, PayPal, VNPay.
  • Validation: rule chain.
  • Routing: shortest path, bus, walking.

4. State vs Strategy — phân biệt

2 pattern có cấu trúc giống hệt (Context có field reference đến interface, swap được). Khác ở intent:

State

  • State biểu diễn tình trạng của object.
  • Object tự đổi state internally (state này transition sang state khác).
  • Client thường KHÔNG chọn state — workflow tự dẫn.
  • State biết về context, có thể đổi context.state.
  • Số state hữu hạn và cố định.

Strategy

  • Strategy là thuật toán để giải 1 việc.
  • Client chọn strategy, set vào context.
  • Strategy không biết các strategy khác.
  • Có thể có vô hạn strategy mới — dễ extension.
  • Strategy không tự đổi (context không tự switch).

Câu hỏi tự kiểm tra: "Object có tự đổi 'class' của mình không?"

  • Có (Order pending → paid → shipped) → State.
  • Không (PricingEngine có 1 strategy do client set) → Strategy.

5. Template Method

PATTERN №21 — BEHAVIORAL

Template Method

Định nghĩa skeleton thuật toán trong method của parent, để subclass override một số bước cụ thể mà không thay đổi cấu trúc tổng.

Vấn đề Có thuật toán với nhiều bước, đa số bước giống nhau, vài bước khác nhau theo biến thể. Lợi ích Reuse thuật toán chính. Subclass tập trung vào điểm khác biệt. Đánh đổi Dùng inheritance — dependency chặt parent-child.

5.1. Implementation

abstract class DataExporter {
  // Template method — final, không cho override
  export(data: any[]): string {
    this.validate(data);
    const opened = this.openDocument();
    const written = this.writeRows(opened, data);
    return this.closeDocument(written);
  }

  protected validate(data: any[]) {
    if (!Array.isArray(data)) throw new Error('invalid');
  }

  // Steps abstract — subclass implement
  protected abstract openDocument(): string;
  protected abstract writeRows(doc: string, data: any[]): string;
  protected abstract closeDocument(doc: string): string;
}

class CsvExporter extends DataExporter {
  protected openDocument() { return ''; }
  protected writeRows(doc: string, data: any[]) {
    return data.map(r => Object.values(r).join(',')).join('\n');
  }
  protected closeDocument(doc: string) { return doc; }
}

class JsonExporter extends DataExporter {
  protected openDocument() { return '['; }
  protected writeRows(doc: string, data: any[]) {
    return doc + data.map(r => JSON.stringify(r)).join(',');
  }
  protected closeDocument(doc: string) { return doc + ']'; }
}

class XmlExporter extends DataExporter {
  protected openDocument() { return '<?xml version="1.0"?>\n<data>\n'; }
  protected writeRows(doc: string, data: any[]) {
    return doc + data.map(r =>
      '  <row>' + Object.entries(r).map(([k, v]) => `<${k}>${v}</${k}>`).join('') + '</row>'
    ).join('\n');
  }
  protected closeDocument(doc: string) { return doc + '\n</data>'; }
}

5.2. Hook methods

Bước có default implementation rỗng, subclass có thể override nếu cần:

abstract class DataExporter {
  export(data: any[]): string {
    this.beforeExport(data);   // hook
    /* ... */
    this.afterExport();        // hook
    return result;
  }

  // Hooks với default rỗng
  protected beforeExport(data: any[]) {}
  protected afterExport() {}
}

class CsvWithLogging extends CsvExporter {
  protected beforeExport(data: any[]) { console.log(`Exporting ${data.length} rows`); }
}

5.3. Template Method vs Strategy

Template MethodStrategy
VariationInheritance — subclass override stepComposition — swap object
CouplingCompile-time (static)Runtime (dynamic)
Reuse code chungTrong parent classPhải duplicate hoặc abstract khác
Khi nào dùngSkeleton cố định, vài step khác nhauToàn bộ thuật toán khác, swap được

Quy tắc: nếu chỉ 1-2 bước khác nhau giữa biến thể → Template Method. Nếu cả thuật toán khác → Strategy.

5.4. Use cases

  • Framework hook: React class component componentDidMount, render.
  • Spring JdbcTemplate: define connection/transaction skeleton, callback override SQL.
  • Build pipeline: skeleton (validate → compile → test → deploy), step override theo project type.
  • Game AI: skeleton tick (sense → think → act), child class override think logic.

6. Visitor

PATTERN №22 — BEHAVIORAL

Visitor

Tách operation khỏi cấu trúc object. Cho phép định nghĩa operation mới trên hierarchy mà không sửa class trong hierarchy.

Vấn đề Có hierarchy ổn định (AST, file system) cần thêm nhiều operation (print, optimize, lint, eval). Lợi ích Thêm operation mới = thêm Visitor, không sửa hierarchy. Đánh đổi Thêm node type mới phải sửa mọi Visitor. Phức tạp với double dispatch.

6.1. Vấn đề: thêm operation

Ngôn ngữ tính số đơn giản với AST: Number, Add, Multiply. Operation cần làm: evaluate, print, optimize.

// ❌ Mỗi operation phải sửa mọi class:
abstract class Expr {
  abstract evaluate(): number;
  abstract print(): string;
  abstract optimize(): Expr;
  // Thêm operation mới (lint?) → sửa mọi class
}

6.2. Visitor pattern

// Visitor interface
interface ExprVisitor<R> {
  visitNumber(e: NumberExpr): R;
  visitAdd(e: AddExpr): R;
  visitMul(e: MulExpr): R;
}

// Element interface
abstract class Expr {
  abstract accept<R>(visitor: ExprVisitor<R>): R;
}

// Concrete elements
class NumberExpr extends Expr {
  constructor(public value: number) { super(); }
  accept<R>(v: ExprVisitor<R>) { return v.visitNumber(this); }
}

class AddExpr extends Expr {
  constructor(public left: Expr, public right: Expr) { super(); }
  accept<R>(v: ExprVisitor<R>) { return v.visitAdd(this); }
}

class MulExpr extends Expr {
  constructor(public left: Expr, public right: Expr) { super(); }
  accept<R>(v: ExprVisitor<R>) { return v.visitMul(this); }
}

// Operations as visitors
class EvalVisitor implements ExprVisitor<number> {
  visitNumber(e: NumberExpr) { return e.value; }
  visitAdd(e: AddExpr)       { return e.left.accept(this) + e.right.accept(this); }
  visitMul(e: MulExpr)       { return e.left.accept(this) * e.right.accept(this); }
}

class PrintVisitor implements ExprVisitor<string> {
  visitNumber(e: NumberExpr) { return String(e.value); }
  visitAdd(e: AddExpr)       { return `(${e.left.accept(this)} + ${e.right.accept(this)})`; }
  visitMul(e: MulExpr)       { return `(${e.left.accept(this)} * ${e.right.accept(this)})`; }
}

// Sử dụng: (2 + 3) * 4
const expr = new MulExpr(new AddExpr(new NumberExpr(2), new NumberExpr(3)), new NumberExpr(4));
console.log(expr.accept(new EvalVisitor()));   // 20
console.log(expr.accept(new PrintVisitor()));  // ((2 + 3) * 4)

// Thêm operation mới — chỉ thêm Visitor:
class OptimizeVisitor implements ExprVisitor<Expr> {
  visitNumber(e: NumberExpr) { return e; }
  visitAdd(e: AddExpr) {
    const l = e.left.accept(this), r = e.right.accept(this);
    if (l instanceof NumberExpr && r instanceof NumberExpr) return new NumberExpr(l.value + r.value);
    return new AddExpr(l, r);
  }
  visitMul(e: MulExpr) { /* ... */ return e; }
}

6.3. Double dispatch

Visitor giải quyết "double dispatch" — chọn method dựa trên 2 type (visitor + element). Single dispatch chỉ chọn theo 1 type (the method's class). element.accept(visitor) dispatch theo element type, rồi visitor.visitX dispatch theo visitor type.

6.4. Use cases

  • Compiler/AST: linter, formatter, optimizer, code gen — mỗi cái 1 visitor.
  • File system: backup, virus scan, permission check.
  • UI tree: render, layout, hit-test.
  • Document: spell-check, word-count, export.

TypeScript compiler, Babel, ESLint, Prettier — đều dùng Visitor (hoặc biến thể).

6.5. Khi KHÔNG dùng

  • Hierarchy thay đổi nhiều (thêm node type) — Visitor không OCP với chiều này.
  • Operation đơn giản, ít — method trên class đủ.

7. Interpreter

PATTERN №23 — BEHAVIORAL

Interpreter

Định nghĩa biểu diễn cho grammar của ngôn ngữ + interpreter dùng biểu diễn để diễn dịch câu.

Vấn đề Có DSL (domain-specific language) đơn giản cần parse + execute. Lợi ích Grammar trở thành class hierarchy — dễ extend. Đánh đổi Class explosion với grammar phức tạp. Slow vs parser tool.

7.1. Implementation: simple boolean DSL

Hỗ trợ true, false, and, or, not, variable.

interface Expr {
  evaluate(ctx: Record<string, boolean>): boolean;
}

class TrueLit implements Expr {
  evaluate() { return true; }
}

class FalseLit implements Expr {
  evaluate() { return false; }
}

class Variable implements Expr {
  constructor(public name: string) {}
  evaluate(ctx: Record<string, boolean>) { return ctx[this.name] ?? false; }
}

class And implements Expr {
  constructor(public left: Expr, public right: Expr) {}
  evaluate(ctx: any) { return this.left.evaluate(ctx) && this.right.evaluate(ctx); }
}

class Or implements Expr {
  constructor(public left: Expr, public right: Expr) {}
  evaluate(ctx: any) { return this.left.evaluate(ctx) || this.right.evaluate(ctx); }
}

class Not implements Expr {
  constructor(public expr: Expr) {}
  evaluate(ctx: any) { return !this.expr.evaluate(ctx); }
}

// "isAdmin AND (isLoggedIn OR isVip)"
const expr = new And(
  new Variable('isAdmin'),
  new Or(new Variable('isLoggedIn'), new Variable('isVip')),
);

console.log(expr.evaluate({ isAdmin: true, isLoggedIn: false, isVip: true }));   // true
console.log(expr.evaluate({ isAdmin: false, isLoggedIn: true,  isVip: true }));  // false

7.2. Use cases

  • Rule engine: feature flag DSL, permission rule.
  • Query DSL: MongoDB query, JQ.
  • Math expression evaluator.
  • Template engine.

7.3. Khi KHÔNG dùng

Grammar phức tạp → dùng parser generator (ANTLR, PEG.js) — Interpreter pattern không scale.

8. Bảng tổng hợp 23 GoF

8.1. Creational (5)

#TênKhi dùng (nhanh)
1Factory MethodSubclass quyết định class instantiate
2Abstract FactoryFamily object cùng style/platform
3BuilderObject phức tạp với nhiều tham số
4PrototypeClone template thay vì tạo từ đầu
5Singleton1 instance toàn app (cẩn thận lạm dụng)

8.2. Structural (7)

6AdapterTương thích interface khác
7BridgeTách abstraction × implementation
8CompositeCây leaf + composite cùng interface
9DecoratorThêm chức năng động
10FacadeĐơn giản hóa subsystem
11FlyweightChia sẻ object — giảm memory
12ProxyKiểm soát truy cập (lazy/cache/auth)

8.3. Behavioral (11)

13Chain of ResponsibilityChuỗi handler (middleware)
14CommandAction như object (undo/queue)
15IteratorDuyệt collection thống nhất
16MediatorN-N qua trung gian
17MementoSnapshot state
18ObserverPub/Sub 1-N
19StateObject đổi hành vi theo state
20StrategyFamily thuật toán thay thế
21Template MethodSkeleton + step override
22VisitorTách operation khỏi hierarchy
23InterpreterGrammar cho DSL nhỏ
Frequency thực tế (theo project hiện đại) Cực phổ biến: Strategy, Observer, Iterator, Decorator, Facade, Adapter, Factory. Trung bình: Builder, Composite, Singleton, Command, State, Template Method, Proxy, Chain of Responsibility. Hiếm: Visitor (dùng khi compile/AST), Interpreter (DSL), Flyweight (game/specific perf), Mediator, Memento, Bridge, Prototype, Abstract Factory.

9. Bài tập

  1. State: implement TCP connection state machine với 4 state: CLOSED → LISTEN → ESTABLISHED → CLOSED. Method: open(), accept(), send(), close().
  2. Strategy: implement compression engine với 3 strategy: Gzip, Brotli, NoCompression. Context có method compress(data).
  3. Phân biệt: code dưới dùng pattern nào — State hay Strategy?
    class Player {
      private playback: PlaybackBehavior;
      setPlayback(p: PlaybackBehavior) { this.playback = p; }
      play() { this.playback.play(); }
    }
    Sửa nó thành State pattern (object tự transition).
  4. Template Method: implement HotDrink với template "boil water → brew → pour → addCondiments". Subclass Tea, Coffee override brew và addCondiments.
  5. Visitor: AST cho ngôn ngữ tính số (Number, Add, Mul). Implement Eval và Print visitor như mục 6.2. Thêm Optimize visitor (folding constants).
  6. Interpreter: parse + evaluate biểu thức boolean như "isAdmin AND (isLoggedIn OR isVip)" từ string. (Tip: dùng split đơn giản, không cần parser thật.)
  7. Liệt kê 3 pattern bạn nghĩ gặp nhiều nhất trong code hằng ngày, kèm ví dụ.

10. Quiz

Quiz cuối Chương 7

State vs Strategy khác nhau chính ở:

  • Cấu trúc class
  • Số lượng method
  • Intent: State biểu diễn workflow internal (object tự transition); Strategy là thuật toán thay thế (client chọn)
  • Performance
Cấu trúc giống nhau (Context có ref đến interface, swap được). Khác ở intent + cách dùng. State: tự transition (paid → shipped → delivered). Strategy: client set, không tự đổi (PricingEngine với GivenStrategy).

Strategy trong JS/TS có thể đơn giản hóa thành:

  • Function — không cần class nếu strategy không có state
  • Singleton
  • Generic class
  • Decorator
Array.sort(compareFn) dùng Strategy as function. .filter, .map nhận function strategy. Class Strategy chỉ cần khi có state hoặc nhiều method liên quan.

Template Method dùng:

  • Composition
  • Singleton
  • Decorator
  • Inheritance — parent định skeleton, subclass override step cụ thể
Template Method là pattern "inheritance-based". Skeleton (template method) là final/non-virtual, các bước (primitive) là abstract/virtual. Strategy thay thế bằng composition; Template Method dùng inheritance.

Visitor giải quyết vấn đề:

  • Cần thêm node type vào hierarchy
  • Cần thêm operation mới vào hierarchy mà không sửa class trong hierarchy
  • Object quá nhiều RAM
  • Class abstract
Visitor OCP theo chiều operation: thêm operation mới = thêm Visitor class, không sửa hierarchy. Trade-off: thêm node TYPE phải sửa mọi Visitor. Hierarchy ổn định, operation thay đổi nhiều → Visitor.

"Double dispatch" trong Visitor nghĩa là:

  • Gọi 2 method liên tiếp
  • Chạy trên 2 thread
  • Method được chọn dựa trên 2 type (element type + visitor type) qua chuỗi element.accept(visitor) rồi visitor.visitX
  • Tăng gấp đôi performance
OOP thông thường single dispatch (chọn method theo type của receiver). Visitor đạt double dispatch: bước 1 dispatch theo element type (accept), bước 2 dispatch theo visitor type (visitX). Cho phép operation linh hoạt với hierarchy.

Interpreter pattern phù hợp khi:

  • Có DSL nhỏ với grammar đơn giản (rule engine, query DSL)
  • Cần tốc độ cao
  • Có grammar phức tạp 100+ rule
  • Mọi lúc
Interpreter tốt cho DSL nhỏ: feature flag rule, permission, formula đơn giản. Grammar phức tạp → dùng parser generator (ANTLR, PEG.js) — Interpreter không scale với expression hơn ~10 rule.

XState hoặc state machine library thay thế cho:

  • Strategy
  • Visitor
  • Observer
  • State pattern (cho FSM phức tạp với side-effect, parallel state, history)
XState (TS), Stately, Stateless (C#) là alternative declarative cho State pattern. Có state visualization, side-effect (action/service), nested state. Cho FSM ≥ 5 state phức tạp dùng XState; FSM nhỏ dùng State pattern manual.

Pattern phổ biến NHẤT trong code hàng ngày (web dev modern):

  • Visitor, Memento
  • Interpreter, Mediator
  • Strategy, Observer, Iterator, Decorator, Facade, Adapter
  • Bridge, Prototype, Flyweight
Mọi event handler = Observer. Sort/filter/map = Strategy. for...of, async iteration = Iterator. HOC, middleware = Decorator. Service layer = Facade. Tích hợp lib = Adapter. Bạn dùng những pattern này hằng ngày.

Hoàn thành Chương 7 — toàn bộ 23 GoF patterns. Tiếp theo: Chương 8 — Anti-patterns + Refactoring + DDD-lite →