Back to blog

Observer, Command và State Patterns Giải Thích Rõ Ràng

oopbehavioral-patternsdesign-patternstypescriptpythonjava
Observer, Command và State Patterns Giải Thích Rõ Ràng

Giới thiệu

Trong bài trước bạn đã nắm vững hai behavioral pattern để manage algorithm: Strategy để chọn algorithm lúc runtime và Template Method cho skeleton của algorithm. Đây là bài cuối của series — ta sẽ cover ba behavioral pattern cuối cùng, nền tảng của mọi event-driven và stateful system trong thực tế.

PatternMột Câu Tóm Gọn
ObserverTự động notify nhiều object khi state của một object thay đổi
CommandĐóng gói request thành object — cho phép undo/redo, queue, và log
StateCho phép object thay đổi behavior khi internal state của nó thay đổi

Ba pattern này chính là foundation của UI framework, game engine, workflow system, và mọi event-driven architecture bạn sẽ gặp.

Bạn Sẽ Học Được Gì

✅ Xây dựng event system loosely-coupled với Observer pattern
✅ Implement generic event bus dùng trong real framework
✅ Đóng gói action thành object với Command — có undo/redo miễn phí
✅ Xây dựng command queue và macro command
✅ Model complex state machine gọn gàng với State pattern
✅ Loại bỏ multi-level conditional logic khỏi stateful object
✅ Biết khi nào dùng pattern nào và cách kết hợp chúng

Điều Kiện Tiên Quyết


Phần 1: Observer Pattern

Vấn Đề: Tight Coupling Khi State Thay Đổi

Bạn đang xây weather station. Nhiều display (điều kiện hiện tại, thống kê, dự báo) đều cần update khi dữ liệu sensor thay đổi:

// ❌ Trước Observer — WeatherStation biết mọi display
class WeatherStation {
  private temperature: number = 0;
  private currentDisplay: CurrentConditionsDisplay;
  private statsDisplay: StatisticsDisplay;
  private forecastDisplay: ForecastDisplay;
 
  // Thêm display mới = phải sửa class này ❌
  update(temperature: number): void {
    this.temperature = temperature;
    this.currentDisplay.update(temperature);    // tight coupling
    this.statsDisplay.update(temperature);      // tight coupling
    this.forecastDisplay.update(temperature);   // tight coupling
    // Display mới = sửa method này ❌
  }
}

Vấn đề:

  1. WeatherStation coupled với mọi display — phải biết từng cái theo tên
  2. Không add/remove display lúc runtime — danh sách hardcode
  3. Unit testing phải tạo đủ display dù chỉ care một cái
  4. Vi phạm Open/Closed Principle — thêm display = sửa station

Observer pattern giải quyết bằng cách đảo ngược dependency: display subscribe vào station; station broadcast mà không cần biết ai đang nghe.


Cấu Trúc Pattern

Các thành phần:

  • Subject (Publisher) — duy trì danh sách observer; notify khi state thay đổi
  • Observer (Subscriber) — interface mà mọi listener phải implement
  • ConcreteSubject — object thực sự có state mà người khác quan tâm
  • ConcreteObserver — phản ứng khi subject state thay đổi

Hollywood Principle: "Đừng gọi cho chúng tôi, chúng tôi sẽ gọi cho bạn." Observer không poll subject — subject gọi họ.


TypeScript Implementation

Ví Dụ 1: Weather Station

// Observer interface — mọi subscriber phải implement
interface WeatherObserver {
  update(temperature: number, humidity: number, pressure: number): void;
}
 
// Subject interface — mọi publisher phải implement
interface WeatherSubject {
  attach(observer: WeatherObserver): void;
  detach(observer: WeatherObserver): void;
  notify(): void;
}
 
// Concrete Subject — publisher
class WeatherStation implements WeatherSubject {
  private observers: WeatherObserver[] = [];
  private temperature: number = 0;
  private humidity: number = 0;
  private pressure: number = 0;
 
  attach(observer: WeatherObserver): void {
    this.observers.push(observer);
    console.log("📡 Observer đăng ký thành công");
  }
 
  detach(observer: WeatherObserver): void {
    const idx = this.observers.indexOf(observer);
    if (idx !== -1) {
      this.observers.splice(idx, 1);
      console.log("📡 Observer hủy đăng ký");
    }
  }
 
  notify(): void {
    for (const observer of this.observers) {
      observer.update(this.temperature, this.humidity, this.pressure);
    }
  }
 
  // Business method — tự động trigger notification
  setMeasurements(temp: number, humidity: number, pressure: number): void {
    this.temperature = temp;
    this.humidity = humidity;
    this.pressure = pressure;
    this.notify(); // broadcast tới mọi subscriber
  }
}
 
// Concrete Observers — mỗi cái phản ứng khác nhau
class CurrentConditionsDisplay implements WeatherObserver {
  update(temperature: number, humidity: number): void {
    console.log(`🌡️  Hiện tại: ${temperature}°C, độ ẩm ${humidity}%`);
  }
}
 
class StatisticsDisplay implements WeatherObserver {
  private temps: number[] = [];
 
  update(temperature: number): void {
    this.temps.push(temperature);
    const avg = this.temps.reduce((s, t) => s + t, 0) / this.temps.length;
    const max = Math.max(...this.temps);
    const min = Math.min(...this.temps);
    console.log(`📊 Thống kê: TB ${avg.toFixed(1)}°C, Max ${max}°C, Min ${min}°C`);
  }
}
 
class ForecastDisplay implements WeatherObserver {
  private lastPressure: number = 0;
 
  update(_temp: number, _hum: number, pressure: number): void {
    if (pressure > this.lastPressure) {
      console.log("⛅ Dự báo: Thời tiết đang cải thiện!");
    } else if (pressure < this.lastPressure) {
      console.log("🌧️  Dự báo: Coi chừng mưa và lạnh hơn");
    } else {
      console.log("🌤️  Dự báo: Như cũ");
    }
    this.lastPressure = pressure;
  }
}
 
// Sử dụng — observer subscribe/unsubscribe lúc runtime
const station = new WeatherStation();
const current = new CurrentConditionsDisplay();
const stats = new StatisticsDisplay();
const forecast = new ForecastDisplay();
 
station.attach(current);
station.attach(stats);
station.attach(forecast);
 
station.setMeasurements(26, 65, 1013);
station.setMeasurements(29, 70, 1009);
 
// Gỡ display lúc runtime — zero code change ở WeatherStation
station.detach(forecast);
station.setMeasurements(27, 60, 1012);
// ForecastDisplay không nhận update nữa

Ví Dụ 2: Generic Event Bus

Trong ứng dụng thực tế, Observer pattern thường được implement dưới dạng typed event bus:

type EventCallback<T = any> = (data: T) => void;
 
class EventBus {
  private listeners: Map<string, EventCallback[]> = new Map();
 
  on<T>(event: string, callback: EventCallback<T>): () => void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event)!.push(callback as EventCallback);
 
    // Trả về hàm unsubscribe (giống React useEffect cleanup)
    return () => this.off(event, callback as EventCallback);
  }
 
  off(event: string, callback: EventCallback): void {
    const cbs = this.listeners.get(event);
    if (cbs) {
      this.listeners.set(event, cbs.filter(cb => cb !== callback));
    }
  }
 
  emit<T>(event: string, data: T): void {
    this.listeners.get(event)?.forEach(cb => cb(data));
  }
}
 
// Sử dụng
const bus = new EventBus();
 
const unsubscribeA = bus.on<{ username: string }>("user:login", ({ username }) => {
  console.log(`📧 Gửi email chào mừng ${username}`);
});
 
bus.on<{ username: string }>("user:login", ({ username }) => {
  console.log(`📝 Audit log: ${username} vừa đăng nhập`);
});
 
bus.on<{ orderId: string; total: number }>("order:placed", ({ orderId, total }) => {
  console.log(`📦 Xử lý đơn hàng ${orderId} — $${total}`);
});
 
bus.emit("user:login", { username: "alice" });
// 📧 Gửi email chào mừng alice
// 📝 Audit log: alice vừa đăng nhập
 
bus.emit("order:placed", { orderId: "ORD-001", total: 149.99 });
// 📦 Xử lý đơn hàng ORD-001 — $149.99
 
unsubscribeA();
bus.emit("user:login", { username: "bob" });
// 📝 Audit log: bob vừa đăng nhập (listener đầu đã bị remove)

Ví Dụ 3: Stock Price Tracker

interface StockObserver {
  onPriceChange(symbol: string, price: number, change: number): void;
}
 
class StockMarket {
  private observers: Map<string, StockObserver[]> = new Map();
  private prices: Map<string, number> = new Map();
 
  subscribe(symbol: string, observer: StockObserver): void {
    if (!this.observers.has(symbol)) {
      this.observers.set(symbol, []);
    }
    this.observers.get(symbol)!.push(observer);
  }
 
  updatePrice(symbol: string, newPrice: number): void {
    const oldPrice = this.prices.get(symbol) ?? newPrice;
    const change = ((newPrice - oldPrice) / oldPrice) * 100;
    this.prices.set(symbol, newPrice);
 
    this.observers.get(symbol)?.forEach(obs =>
      obs.onPriceChange(symbol, newPrice, change)
    );
  }
}
 
class AlertSystem implements StockObserver {
  constructor(private threshold: number) {}
 
  onPriceChange(symbol: string, price: number, change: number): void {
    if (Math.abs(change) >= this.threshold) {
      console.log(`🚨 CẢNH BÁO: ${symbol} biến động ${change.toFixed(2)}% → $${price}`);
    }
  }
}
 
class Portfolio implements StockObserver {
  private holdings: Map<string, number>;
 
  constructor(holdings: Record<string, number>) {
    this.holdings = new Map(Object.entries(holdings));
  }
 
  onPriceChange(symbol: string, price: number): void {
    const shares = this.holdings.get(symbol) ?? 0;
    const value = shares * price;
    console.log(`💼 Portfolio: ${symbol} × ${shares} cổ phiếu = $${value.toFixed(2)}`);
  }
}
 
const market = new StockMarket();
const alert = new AlertSystem(5); // cảnh báo khi biến động ≥ 5%
const portfolio = new Portfolio({ AAPL: 10, GOOG: 5 });
 
market.subscribe("AAPL", alert);
market.subscribe("AAPL", portfolio);
market.subscribe("GOOG", portfolio);
 
market.updatePrice("AAPL", 180);
market.updatePrice("AAPL", 192); // tăng 6.67% — kích hoạt alert
// 🚨 CẢNH BÁO: AAPL biến động 6.67% → $192
// 💼 Portfolio: AAPL × 10 cổ phiếu = $1920.00

Python Implementation

from __future__ import annotations
from abc import ABC, abstractmethod
 
class Observer(ABC):
    @abstractmethod
    def update(self, event: str, data: dict) -> None:
        pass
 
class Subject:
    def __init__(self) -> None:
        self._observers: list[Observer] = []
 
    def attach(self, observer: Observer) -> None:
        self._observers.append(observer)
 
    def detach(self, observer: Observer) -> None:
        self._observers.remove(observer)
 
    def notify(self, event: str, data: dict) -> None:
        for obs in self._observers:
            obs.update(event, data)
 
class ECommerceStore(Subject):
    def place_order(self, order_id: str, total: float) -> None:
        print(f"\n🛒 Đơn hàng #{order_id} được đặt — ${total:.2f}")
        self.notify("order_placed", {"order_id": order_id, "total": total})
 
    def cancel_order(self, order_id: str) -> None:
        print(f"\n❌ Đơn hàng #{order_id} bị hủy")
        self.notify("order_cancelled", {"order_id": order_id})
 
class EmailNotifier(Observer):
    def update(self, event: str, data: dict) -> None:
        if event == "order_placed":
            print(f"📧 Email: Đơn hàng #{data['order_id']} đã được xác nhận!")
        elif event == "order_cancelled":
            print(f"📧 Email: Đơn hàng #{data['order_id']} đã bị hủy.")
 
class InventoryManager(Observer):
    def update(self, event: str, data: dict) -> None:
        if event == "order_placed":
            print(f"📦 Kho: Đặt trước hàng cho đơn #{data['order_id']}")
        elif event == "order_cancelled":
            print(f"📦 Kho: Giải phóng hàng đặt trước cho đơn #{data['order_id']}")
 
class AnalyticsTracker(Observer):
    def update(self, event: str, data: dict) -> None:
        print(f"📊 Analytics: Ghi event '{event}' → {data}")
 
# Sử dụng
store = ECommerceStore()
store.attach(EmailNotifier())
store.attach(InventoryManager())
store.attach(AnalyticsTracker())
 
store.place_order("ORD-001", 149.99)
store.cancel_order("ORD-001")

Java Implementation

import java.util.ArrayList;
import java.util.List;
 
public interface NewsletterObserver {
    void onNewArticle(String title, String category);
}
 
public class NewsletterPublisher {
    private final List<NewsletterObserver> subscribers = new ArrayList<>();
 
    public void subscribe(NewsletterObserver observer) {
        subscribers.add(observer);
        System.out.println("✅ Subscriber mới đã thêm");
    }
 
    public void unsubscribe(NewsletterObserver observer) {
        subscribers.remove(observer);
        System.out.println("👋 Subscriber đã rời");
    }
 
    public void publishArticle(String title, String category) {
        System.out.println("\n📰 Đăng bài: " + title + " [" + category + "]");
        for (NewsletterObserver observer : subscribers) {
            observer.onNewArticle(title, category);
        }
    }
}
 
public class TechReader implements NewsletterObserver {
    private final String name;
    public TechReader(String name) { this.name = name; }
 
    @Override
    public void onNewArticle(String title, String category) {
        if ("Tech".equals(category) || "Programming".equals(category)) {
            System.out.println("👨‍💻 " + name + " đọc: " + title);
        }
    }
}
 
public class AllTopicsReader implements NewsletterObserver {
    private final String name;
    public AllTopicsReader(String name) { this.name = name; }
 
    @Override
    public void onNewArticle(String title, String category) {
        System.out.println("📚 " + name + " thêm '" + title + "' vào danh sách đọc");
    }
}
 
// Main
NewsletterPublisher publisher = new NewsletterPublisher();
publisher.subscribe(new TechReader("Alice"));
publisher.subscribe(new AllTopicsReader("Bob"));
 
publisher.publishArticle("Strategy Pattern Explained", "Programming");
publisher.publishArticle("Morning Yoga Routine", "Health");

Phần 2: Command Pattern

Vấn Đề: Action Bị Coupled Chặt

Bạn đang xây ứng dụng GUI với button, menu, và keyboard shortcut. Mỗi phần tử UI gọi trực tiếp action:

// ❌ Trước Command — button gọi thẳng business object
class SaveButton {
  private document: Document;
 
  onClick(): void {
    this.document.save();  // tight coupling với Document
  }
}
 
// Vấn đề:
// 1. Mỗi UI element coupled chặt với receiver cụ thể
// 2. Không undo/redo — action là fire-and-forget
// 3. Không thể log, queue, hay replay action
// 4. Không thể gán cùng action cho nhiều trigger (button + menu + shortcut)

Command pattern giải quyết bằng cách biến action thành object. Mỗi command object biết cách execute và cách undo action đó.


Cấu Trúc Pattern

Các thành phần:

  • Command — interface: execute() và undo()
  • ConcreteCommand — wrap một receiver + lưu state cần cho undo
  • Receiver — object thực sự làm việc (TextEditor, Light, DB)
  • Invoker — trigger command; duy trì command history cho undo/redo; không biết command làm gì

Core insight: Invoker không gọi receiver trực tiếp. Nó chỉ gọi command.execute(). Điều này decouple trigger (nhấn button) khỏi action (thứ thực sự chạy).


TypeScript Implementation

Ví Dụ 1: Text Editor với Undo/Redo

// Command interface
interface Command {
  execute(): void;
  undo(): void;
}
 
// Receiver — object thực sự thao tác text
class TextDocument {
  private content: string = "";
 
  write(text: string): void { this.content += text; }
  delete(length: number): void { this.content = this.content.slice(0, -length); }
  replace(from: string, to: string): void { this.content = this.content.replace(from, to); }
  getContent(): string { return this.content; }
}
 
// Concrete Commands — mỗi cái đóng gói một action có thể đảo ngược
class WriteCommand implements Command {
  constructor(private doc: TextDocument, private text: string) {}
 
  execute(): void { this.doc.write(this.text); }
  undo(): void { this.doc.delete(this.text.length); } // đảo ngược: xóa phần vừa viết
}
 
class ReplaceCommand implements Command {
  constructor(
    private doc: TextDocument,
    private from: string,
    private to: string
  ) {}
 
  execute(): void { this.doc.replace(this.from, this.to); }
  undo(): void { this.doc.replace(this.to, this.from); }
}
 
// Invoker — quản lý command history cho undo/redo
class TextEditor {
  private history: Command[] = [];
  private undoneStack: Command[] = [];
  private doc: TextDocument = new TextDocument();
 
  execute(command: Command): void {
    command.execute();
    this.history.push(command);
    this.undoneStack = []; // xóa redo stack khi có action mới
    console.log(`📝 Sau execute: "${this.doc.getContent()}"`);
  }
 
  undo(): void {
    const command = this.history.pop();
    if (command) {
      command.undo();
      this.undoneStack.push(command);
      console.log(`↩️  Sau undo:    "${this.doc.getContent()}"`);
    } else {
      console.log("Không có gì để undo.");
    }
  }
 
  redo(): void {
    const command = this.undoneStack.pop();
    if (command) {
      command.execute();
      this.history.push(command);
      console.log(`↪️  Sau redo:    "${this.doc.getContent()}"`);
    } else {
      console.log("Không có gì để redo.");
    }
  }
 
  getDocument(): TextDocument { return this.doc; }
}
 
// Sử dụng
const editor = new TextEditor();
const doc = editor.getDocument();
 
editor.execute(new WriteCommand(doc, "Xin chào"));
// 📝 Sau execute: "Xin chào"
 
editor.execute(new WriteCommand(doc, ", Design Patterns!"));
// 📝 Sau execute: "Xin chào, Design Patterns!"
 
editor.execute(new ReplaceCommand(doc, "Design Patterns", "TypeScript"));
// 📝 Sau execute: "Xin chào, TypeScript!"
 
editor.undo();
// ↩️  Sau undo:    "Xin chào, Design Patterns!"
 
editor.undo();
// ↩️  Sau undo:    "Xin chào"
 
editor.redo();
// ↪️  Sau redo:    "Xin chào, Design Patterns!"

Ví Dụ 2: Smart Home Remote Control

class Light {
  constructor(private location: string) {}
  on(): void { console.log(`💡 Đèn ${this.location} BẬT`); }
  off(): void { console.log(`💡 Đèn ${this.location} TẮT`); }
}
 
class Thermostat {
  private temperature: number = 20;
  setTemperature(temp: number): void {
    console.log(`🌡️  Điều hòa: ${this.temperature}°C → ${temp}°C`);
    this.temperature = temp;
  }
  getTemperature(): number { return this.temperature; }
}
 
class LightOnCommand implements Command {
  constructor(private light: Light) {}
  execute(): void { this.light.on(); }
  undo(): void { this.light.off(); }
}
 
class LightOffCommand implements Command {
  constructor(private light: Light) {}
  execute(): void { this.light.off(); }
  undo(): void { this.light.on(); }
}
 
class SetTemperatureCommand implements Command {
  private previousTemp: number;
 
  constructor(private thermostat: Thermostat, private newTemp: number) {
    this.previousTemp = thermostat.getTemperature();
  }
 
  execute(): void { this.thermostat.setTemperature(this.newTemp); }
  undo(): void { this.thermostat.setTemperature(this.previousTemp); }
}
 
// Macro Command — execute nhiều command như một
class MacroCommand implements Command {
  constructor(private commands: Command[]) {}
 
  execute(): void { this.commands.forEach(cmd => cmd.execute()); }
 
  undo(): void {
    // Undo theo thứ tự ngược lại
    [...this.commands].reverse().forEach(cmd => cmd.undo());
  }
}
 
class SmartRemote {
  private history: Command[] = [];
 
  press(command: Command): void {
    command.execute();
    this.history.push(command);
  }
 
  undoLast(): void {
    const cmd = this.history.pop();
    if (cmd) {
      cmd.undo();
      console.log("↩️  Action cuối đã được undo");
    }
  }
}
 
// Sử dụng
const phongKhach = new Light("Phòng Khách");
const phongNgu = new Light("Phòng Ngủ");
const dieuHoa = new Thermostat();
const remote = new SmartRemote();
 
remote.press(new LightOnCommand(phongKhach));
remote.press(new SetTemperatureCommand(dieuHoa, 23));
 
// Macro — cảnh "Xem Phim": tắt đèn + chỉnh nhiệt độ
const xemPhim = new MacroCommand([
  new LightOffCommand(phongKhach),
  new LightOffCommand(phongNgu),
  new SetTemperatureCommand(dieuHoa, 22),
]);
 
remote.press(xemPhim);
remote.undoLast(); // Undo toàn bộ macro theo thứ tự ngược

Ví Dụ 3: Command Queue (Task Scheduling)

interface AsyncCommand {
  execute(): Promise<void>;
  getName(): string;
}
 
class CommandQueue {
  private queue: AsyncCommand[] = [];
  private running: boolean = false;
 
  enqueue(command: AsyncCommand): void {
    this.queue.push(command);
    console.log(`📥 Đã queue: ${command.getName()} (queue size: ${this.queue.length})`);
  }
 
  async process(): Promise<void> {
    if (this.running) return;
    this.running = true;
 
    while (this.queue.length > 0) {
      const command = this.queue.shift()!;
      console.log(`⚙️  Đang chạy: ${command.getName()}`);
      await command.execute();
    }
 
    this.running = false;
    console.log("✅ Tất cả command đã được xử lý");
  }
}
 
class SendEmailCommand implements AsyncCommand {
  constructor(private to: string, private subject: string) {}
  getName(): string { return `SendEmail(${this.to})`; }
 
  async execute(): Promise<void> {
    await new Promise(r => setTimeout(r, 100));
    console.log(`📧 Email gửi tới ${this.to}: "${this.subject}"`);
  }
}
 
class ResizeImageCommand implements AsyncCommand {
  constructor(private imagePath: string, private width: number) {}
  getName(): string { return `ResizeImage(${this.imagePath})`; }
 
  async execute(): Promise<void> {
    await new Promise(r => setTimeout(r, 200));
    console.log(`🖼️  Resize ${this.imagePath} về ${this.width}px`);
  }
}
 
const queue = new CommandQueue();
queue.enqueue(new SendEmailCommand("[email protected]", "Chào mừng!"));
queue.enqueue(new ResizeImageCommand("avatar.jpg", 200));
queue.enqueue(new SendEmailCommand("[email protected]", "Đơn hàng đã giao"));
 
queue.process();

Python Implementation

from abc import ABC, abstractmethod
 
class Command(ABC):
    @abstractmethod
    def execute(self) -> None:
        pass
 
    @abstractmethod
    def undo(self) -> None:
        pass
 
# Receiver
class BankAccount:
    def __init__(self, owner: str, balance: float = 0):
        self.owner = owner
        self._balance = balance
 
    def deposit(self, amount: float) -> None:
        self._balance += amount
        print(f"  💰 Nạp ${amount:.2f} → Số dư: ${self._balance:.2f}")
 
    def withdraw(self, amount: float) -> bool:
        if amount > self._balance:
            print(f"  ❌ Không đủ tiền (số dư: ${self._balance:.2f})")
            return False
        self._balance -= amount
        print(f"  💸 Rút ${amount:.2f} → Số dư: ${self._balance:.2f}")
        return True
 
    @property
    def balance(self) -> float:
        return self._balance
 
class DepositCommand(Command):
    def __init__(self, account: BankAccount, amount: float):
        self._account = account
        self._amount = amount
 
    def execute(self) -> None:
        self._account.deposit(self._amount)
 
    def undo(self) -> None:
        self._account.withdraw(self._amount)
 
class WithdrawCommand(Command):
    def __init__(self, account: BankAccount, amount: float):
        self._account = account
        self._amount = amount
        self._success = False
 
    def execute(self) -> None:
        self._success = self._account.withdraw(self._amount)
 
    def undo(self) -> None:
        if self._success:
            self._account.deposit(self._amount)
 
class TransactionManager:
    def __init__(self):
        self._history: list[Command] = []
        self._undone: list[Command] = []
 
    def execute(self, command: Command) -> None:
        command.execute()
        self._history.append(command)
        self._undone.clear()
 
    def undo(self) -> None:
        if self._history:
            cmd = self._history.pop()
            cmd.undo()
            self._undone.append(cmd)
            print("  ↩️  Đã undo")
        else:
            print("  Không có gì để undo")
 
    def redo(self) -> None:
        if self._undone:
            cmd = self._undone.pop()
            cmd.execute()
            self._history.append(cmd)
            print("  ↪️  Đã redo")
        else:
            print("  Không có gì để redo")
 
# Sử dụng
account = BankAccount("Alice", balance=1000)
manager = TransactionManager()
 
print("=== Giao Dịch ===")
manager.execute(DepositCommand(account, 500))
manager.execute(WithdrawCommand(account, 200))
manager.execute(WithdrawCommand(account, 1500))  # thất bại
 
print("\n=== Undo/Redo ===")
manager.undo()
manager.undo()
manager.redo()

Java Implementation

import java.util.ArrayDeque;
import java.util.Deque;
 
public interface FileCommand {
    void execute();
    void undo();
    String getDescription();
}
 
public class FileSystem {
    public void createFile(String path) { System.out.println("📄 Tạo: " + path); }
    public void deleteFile(String path) { System.out.println("🗑️  Xóa: " + path); }
    public void moveFile(String from, String to) {
        System.out.println("📁 Di chuyển: " + from + " → " + to);
    }
}
 
public class CreateFileCommand implements FileCommand {
    private final FileSystem fs;
    private final String path;
 
    public CreateFileCommand(FileSystem fs, String path) {
        this.fs = fs;
        this.path = path;
    }
 
    @Override public void execute() { fs.createFile(path); }
    @Override public void undo() { fs.deleteFile(path); }
    @Override public String getDescription() { return "Tạo " + path; }
}
 
public class MoveFileCommand implements FileCommand {
    private final FileSystem fs;
    private final String from, to;
 
    public MoveFileCommand(FileSystem fs, String from, String to) {
        this.fs = fs;
        this.from = from;
        this.to = to;
    }
 
    @Override public void execute() { fs.moveFile(from, to); }
    @Override public void undo() { fs.moveFile(to, from); }
    @Override public String getDescription() { return "Di chuyển " + from + " → " + to; }
}
 
public class FileManager {
    private final Deque<FileCommand> history = new ArrayDeque<>();
 
    public void execute(FileCommand command) {
        command.execute();
        history.push(command);
    }
 
    public void undo() {
        if (!history.isEmpty()) {
            FileCommand cmd = history.pop();
            cmd.undo();
            System.out.println("↩️  Đã undo: " + cmd.getDescription());
        }
    }
}
 
// Main
FileSystem fs = new FileSystem();
FileManager manager = new FileManager();
 
manager.execute(new CreateFileCommand(fs, "/docs/bao_cao.txt"));
manager.execute(new CreateFileCommand(fs, "/docs/ghi_chu.txt"));
manager.execute(new MoveFileCommand(fs, "/docs/bao_cao.txt", "/archive/bao_cao.txt"));
 
System.out.println("\n--- Undo operations ---");
manager.undo();
manager.undo();

Phần 3: State Pattern

Vấn Đề: Conditional Logic Phình To Theo State

Bạn đang xây vending machine. Behavior của nó phụ thuộc vào state hiện tại: chờ xu, đủ tiền, hết hàng. Không có State pattern:

// ❌ Trước State — switch/if khổng lồ khắp nơi
class VendingMachine {
  private state: "idle" | "hasMoney" | "dispensing" | "outOfStock" = "idle";
 
  insertCoin(amount: number): void {
    if (this.state === "idle") {
      console.log("Nhận xu");
      this.state = "hasMoney";
    } else if (this.state === "hasMoney") {
      console.log("Thêm xu");
    } else if (this.state === "dispensing") {
      console.log("Đợi tí...");
    } else if (this.state === "outOfStock") {
      console.log("Hết hàng — trả xu lại");
    }
  }
  // Mỗi method có một đống if. Thêm state mới = đụng vào mọi method ❌
}

State pattern giải quyết điều này: mỗi state có class riêng. Object delegate xuống class state hiện tại thay vì chạy conditional. Thêm state mới không cần đụng vào state class hiện có.


Cấu Trúc Pattern

Các thành phần:

  • Context — object có behavior thay đổi; delegate xuống state object hiện tại; expose setState() để transition
  • State — interface mọi state class implement
  • ConcreteState — đóng gói behavior của một state cụ thể; trigger transition bằng cách gọi context.setState()

Core insight: State object chuyển đổi context bằng cách gọi context.setState(newState). Không có conditional — chính state quyết định điều gì đến tiếp theo.


TypeScript Implementation

Ví Dụ 1: Vòng Đời Đơn Hàng

// Context
class Order {
  private state: OrderState;
  private orderId: string;
 
  constructor(orderId: string) {
    this.orderId = orderId;
    this.state = new PendingState();
    console.log(`\n📋 Đơn hàng #${orderId} được tạo (Chờ xử lý)`);
  }
 
  setState(state: OrderState): void { this.state = state; }
 
  pay(): void { this.state.pay(this); }
  ship(): void { this.state.ship(this); }
  deliver(): void { this.state.deliver(this); }
  cancel(): void { this.state.cancel(this); }
  getOrderId(): string { return this.orderId; }
}
 
// State interface
interface OrderState {
  pay(order: Order): void;
  ship(order: Order): void;
  deliver(order: Order): void;
  cancel(order: Order): void;
}
 
// Concrete States
class PendingState implements OrderState {
  pay(order: Order): void {
    console.log(`💳 Đơn hàng #${order.getOrderId()} đã thanh toán! → Đang xử lý`);
    order.setState(new ProcessingState());
  }
  ship(order: Order): void { console.log("❌ Chưa thể giao — chưa thanh toán"); }
  deliver(order: Order): void { console.log("❌ Chưa thể nhận — chưa giao hàng"); }
  cancel(order: Order): void {
    console.log(`🚫 Đơn hàng #${order.getOrderId()} đã hủy (trước khi thanh toán)`);
    order.setState(new CancelledState());
  }
}
 
class ProcessingState implements OrderState {
  pay(order: Order): void { console.log("ℹ️  Đã thanh toán rồi"); }
  ship(order: Order): void {
    console.log(`📦 Đơn hàng #${order.getOrderId()} đã giao! → Đang vận chuyển`);
    order.setState(new ShippedState());
  }
  deliver(order: Order): void { console.log("❌ Chưa thể nhận — chưa giao hàng"); }
  cancel(order: Order): void {
    console.log(`🚫 Đơn hàng #${order.getOrderId()} đã hủy (hoàn tiền)`);
    order.setState(new CancelledState());
  }
}
 
class ShippedState implements OrderState {
  pay(order: Order): void { console.log("ℹ️  Đã thanh toán rồi"); }
  ship(order: Order): void { console.log("ℹ️  Đã giao rồi"); }
  deliver(order: Order): void {
    console.log(`🎉 Đơn hàng #${order.getOrderId()} đã nhận! → Hoàn thành`);
    order.setState(new DeliveredState());
  }
  cancel(order: Order): void { console.log("❌ Không thể hủy — hàng đã giao đi"); }
}
 
class DeliveredState implements OrderState {
  pay(order: Order): void { console.log("ℹ️  Đã thanh toán rồi"); }
  ship(order: Order): void { console.log("ℹ️  Đã giao rồi"); }
  deliver(order: Order): void { console.log("ℹ️  Đã nhận rồi"); }
  cancel(order: Order): void { console.log("❌ Không thể hủy — đã nhận hàng"); }
}
 
class CancelledState implements OrderState {
  pay(order: Order): void { console.log("❌ Đơn hàng đã hủy"); }
  ship(order: Order): void { console.log("❌ Đơn hàng đã hủy"); }
  deliver(order: Order): void { console.log("❌ Đơn hàng đã hủy"); }
  cancel(order: Order): void { console.log("ℹ️  Đã hủy rồi"); }
}
 
// Sử dụng — state transition
const order = new Order("ORD-2026-001");
order.ship();       // ❌ Chưa thể giao — chưa thanh toán
order.pay();        // 💳 Đã thanh toán!
order.ship();       // 📦 Đã giao!
order.cancel();     // ❌ Không thể hủy — hàng đã giao đi
order.deliver();    // 🎉 Đã nhận!
order.cancel();     // ❌ Không thể hủy — đã nhận hàng

State transition diagram:


Ví Dụ 2: Đèn Giao Thông

interface TrafficLightState {
  next(light: TrafficLight): void;
  getColor(): string;
  getDuration(): number;
}
 
class TrafficLight {
  private state: TrafficLightState = new GreenState();
 
  transition(): void {
    console.log(`🚦 ${this.state.getColor()} (${this.state.getDuration()}s) → chuyển...`);
    this.state.next(this);
  }
 
  setState(state: TrafficLightState): void {
    this.state = state;
    console.log(`   → ${this.state.getColor()}`);
  }
}
 
class GreenState implements TrafficLightState {
  next(light: TrafficLight): void { light.setState(new YellowState()); }
  getColor(): string { return "🟢 Xanh"; }
  getDuration(): number { return 30; }
}
 
class YellowState implements TrafficLightState {
  next(light: TrafficLight): void { light.setState(new RedState()); }
  getColor(): string { return "🟡 Vàng"; }
  getDuration(): number { return 5; }
}
 
class RedState implements TrafficLightState {
  next(light: TrafficLight): void { light.setState(new GreenState()); }
  getColor(): string { return "🔴 Đỏ"; }
  getDuration(): number { return 25; }
}
 
const light = new TrafficLight();
for (let i = 0; i < 5; i++) {
  light.transition();
}

Ví Dụ 3: Document Approval Workflow

class Document {
  private state: DocumentState = new DraftState();
  content: string = "";
 
  setState(state: DocumentState): void { this.state = state; }
  submit(): void { this.state.submit(this); }
  approve(): void { this.state.approve(this); }
  reject(): void { this.state.reject(this); }
  publish(): void { this.state.publish(this); }
  getStateName(): string { return this.state.getName(); }
}
 
interface DocumentState {
  submit(doc: Document): void;
  approve(doc: Document): void;
  reject(doc: Document): void;
  publish(doc: Document): void;
  getName(): string;
}
 
class DraftState implements DocumentState {
  submit(doc: Document): void {
    console.log("📤 Tài liệu đã gửi để duyệt");
    doc.setState(new ReviewState());
  }
  approve(doc: Document): void { console.log("❌ Bản nháp phải gửi trước"); }
  reject(doc: Document): void { console.log("❌ Bản nháp phải gửi trước"); }
  publish(doc: Document): void { console.log("❌ Phải được duyệt trước"); }
  getName(): string { return "Bản Nháp"; }
}
 
class ReviewState implements DocumentState {
  submit(doc: Document): void { console.log("ℹ️  Đang chờ duyệt rồi"); }
  approve(doc: Document): void {
    console.log("✅ Tài liệu được duyệt");
    doc.setState(new ApprovedState());
  }
  reject(doc: Document): void {
    console.log("❌ Tài liệu bị từ chối — về bản nháp");
    doc.setState(new DraftState());
  }
  publish(doc: Document): void { console.log("❌ Phải được duyệt trước"); }
  getName(): string { return "Đang Duyệt"; }
}
 
class ApprovedState implements DocumentState {
  submit(doc: Document): void { console.log("ℹ️  Đã duyệt rồi"); }
  approve(doc: Document): void { console.log("ℹ️  Đã duyệt rồi"); }
  reject(doc: Document): void {
    console.log("📝 Hủy duyệt — về bản nháp");
    doc.setState(new DraftState());
  }
  publish(doc: Document): void {
    console.log("🌐 Tài liệu đã đăng!");
    doc.setState(new PublishedState());
  }
  getName(): string { return "Đã Duyệt"; }
}
 
class PublishedState implements DocumentState {
  submit(doc: Document): void { console.log("ℹ️  Đã đăng rồi"); }
  approve(doc: Document): void { console.log("ℹ️  Đã đăng rồi"); }
  reject(doc: Document): void {
    console.log("⬇️  Gỡ xuống — về bản nháp");
    doc.setState(new DraftState());
  }
  publish(doc: Document): void { console.log("ℹ️  Đã đăng rồi"); }
  getName(): string { return "Đã Đăng"; }
}
 
// Sử dụng
const doc = new Document();
console.log(`Trạng thái: ${doc.getStateName()}`); // Bản Nháp
doc.approve();    // ❌ Bản nháp phải gửi trước
doc.submit();     // 📤 Gửi để duyệt
doc.reject();     // ❌ Bị từ chối — về bản nháp
doc.submit();     // 📤 Gửi lại
doc.approve();    // ✅ Duyệt
doc.publish();    // 🌐 Đã đăng!
doc.reject();     // ⬇️  Gỡ xuống — về bản nháp

Python Implementation

from abc import ABC, abstractmethod
 
class VendingMachineState(ABC):
    @abstractmethod
    def insert_coin(self, machine: "VendingMachine", amount: float) -> None:
        pass
 
    @abstractmethod
    def select_product(self, machine: "VendingMachine", product: str, price: float) -> None:
        pass
 
    @abstractmethod
    def dispense(self, machine: "VendingMachine") -> None:
        pass
 
    @abstractmethod
    def cancel(self, machine: "VendingMachine") -> None:
        pass
 
class VendingMachine:
    def __init__(self) -> None:
        self._state: VendingMachineState = IdleState()
        self._balance: float = 0
        self._selected_price: float = 0
 
    def set_state(self, state: VendingMachineState) -> None:
        self._state = state
 
    def add_balance(self, amount: float) -> None:
        self._balance += amount
 
    def set_product(self, product: str, price: float) -> None:
        self._selected_price = price
 
    def get_balance(self) -> float:
        return self._balance
 
    def get_selected_price(self) -> float:
        return self._selected_price
 
    def reset(self) -> None:
        self._balance = 0
        self._selected_price = 0
 
    def insert_coin(self, amount: float) -> None:
        self._state.insert_coin(self, amount)
 
    def select_product(self, product: str, price: float) -> None:
        self._state.select_product(self, product, price)
 
    def dispense(self) -> None:
        self._state.dispense(self)
 
    def cancel(self) -> None:
        self._state.cancel(self)
 
class IdleState(VendingMachineState):
    def insert_coin(self, machine: VendingMachine, amount: float) -> None:
        machine.add_balance(amount)
        print(f"💰 Nhận ${amount:.2f} — Số dư: ${machine.get_balance():.2f}")
        machine.set_state(HasMoneyState())
 
    def select_product(self, machine: VendingMachine, product: str, price: float) -> None:
        print("❌ Bỏ xu trước")
 
    def dispense(self, machine: VendingMachine) -> None:
        print("❌ Bỏ xu và chọn sản phẩm trước")
 
    def cancel(self, machine: VendingMachine) -> None:
        print("ℹ️  Không có gì để hủy")
 
class HasMoneyState(VendingMachineState):
    def insert_coin(self, machine: VendingMachine, amount: float) -> None:
        machine.add_balance(amount)
        print(f"💰 Thêm ${amount:.2f} — Số dư: ${machine.get_balance():.2f}")
 
    def select_product(self, machine: VendingMachine, product: str, price: float) -> None:
        if machine.get_balance() >= price:
            machine.set_product(product, price)
            print(f"🎯 Chọn: {product} (${price:.2f}) — đang xuất hàng...")
            machine.set_state(DispensingState())
        else:
            shortage = price - machine.get_balance()
            print(f"❌ Không đủ — cần thêm ${shortage:.2f} cho {product}")
 
    def dispense(self, machine: VendingMachine) -> None:
        print("❌ Chọn sản phẩm trước")
 
    def cancel(self, machine: VendingMachine) -> None:
        change = machine.get_balance()
        machine.reset()
        machine.set_state(IdleState())
        print(f"↩️  Đã hủy — trả lại ${change:.2f}")
 
class DispensingState(VendingMachineState):
    def insert_coin(self, machine: VendingMachine, amount: float) -> None:
        print("⏳ Đang xuất hàng — chờ chút")
 
    def select_product(self, machine: VendingMachine, product: str, price: float) -> None:
        print("⏳ Đang xuất hàng — chờ chút")
 
    def dispense(self, machine: VendingMachine) -> None:
        change = machine.get_balance() - machine.get_selected_price()
        machine.reset()
        machine.set_state(IdleState())
        if change > 0:
            print(f"🎁 Đã xuất! Tiền thừa: ${change:.2f}")
        else:
            print("🎁 Đã xuất! Chúc ngon!")
 
    def cancel(self, machine: VendingMachine) -> None:
        print("❌ Đang xuất hàng — không thể hủy")
 
# Sử dụng
vm = VendingMachine()
vm.select_product("Coca", 1.50)   # ❌ Bỏ xu trước
vm.insert_coin(1.00)              # 💰 Nhận
vm.select_product("Coca", 1.50)   # ❌ Không đủ
vm.insert_coin(0.50)              # 💰 Thêm
vm.select_product("Coca", 1.50)   # ✅ Đang xuất
vm.dispense()                     # 🎁 Đã xuất!

Java Implementation

public interface TCPState {
    void open(TCPConnection conn);
    void close(TCPConnection conn);
    void acknowledge(TCPConnection conn);
    String getStateName();
}
 
public class TCPConnection {
    private TCPState state = new ClosedState();
 
    public void setState(TCPState state) {
        System.out.println("  → Chuyển sang: " + state.getStateName());
        this.state = state;
    }
 
    public void open()        { state.open(this); }
    public void close()       { state.close(this); }
    public void acknowledge() { state.acknowledge(this); }
    public String getState()  { return state.getStateName(); }
}
 
public class ClosedState implements TCPState {
    @Override public void open(TCPConnection conn) {
        System.out.println("🔌 Mở kết nối (gửi SYN)");
        conn.setState(new ListenState());
    }
    @Override public void close(TCPConnection conn) { System.out.println("ℹ️  Đã đóng rồi"); }
    @Override public void acknowledge(TCPConnection conn) { System.out.println("❌ Chưa mở kết nối"); }
    @Override public String getStateName() { return "Đóng"; }
}
 
public class ListenState implements TCPState {
    @Override public void open(TCPConnection conn) { System.out.println("ℹ️  Đang nghe rồi"); }
    @Override public void close(TCPConnection conn) {
        System.out.println("🔌 Đóng kết nối");
        conn.setState(new ClosedState());
    }
    @Override public void acknowledge(TCPConnection conn) {
        System.out.println("✅ Nhận SYN-ACK — kết nối thành công");
        conn.setState(new EstablishedState());
    }
    @Override public String getStateName() { return "Đang Nghe"; }
}
 
public class EstablishedState implements TCPState {
    @Override public void open(TCPConnection conn) { System.out.println("ℹ️  Đã kết nối rồi"); }
    @Override public void close(TCPConnection conn) {
        System.out.println("👋 Gửi FIN — đóng kết nối");
        conn.setState(new ClosedState());
    }
    @Override public void acknowledge(TCPConnection conn) { System.out.println("📨 Dữ liệu được ACK"); }
    @Override public String getStateName() { return "Đã Kết Nối"; }
}
 
// Main
TCPConnection conn = new TCPConnection();
System.out.println("Trạng thái: " + conn.getState()); // Đóng
 
conn.acknowledge(); // ❌ Chưa mở kết nối
conn.open();        // 🔌 Mở
conn.acknowledge(); // ✅ Kết nối thành công
conn.acknowledge(); // 📨 ACK dữ liệu
conn.close();       // 👋 Đóng

Ba Pattern Cùng Nhau: Ví Dụ Thực Tế

Observer, Command và State hoạt động cùng nhau trong document editing system:

// State — theo dõi document lifecycle
// Command — mọi chỉnh sửa đều có thể undo
// Observer — UI component phản ứng với thay đổi state + content
 
class DocumentEditor {
  private eventBus = new EventBus();
  private commandManager = new TextEditor();
  private doc: TextDocument;
  private workflowState: DocumentState = new DraftState();
 
  constructor() {
    this.doc = this.commandManager.getDocument();
 
    // Observer lắng nghe editor
    this.eventBus.on("content:changed", (content: string) => {
      console.log(`📊 Số từ: ${content.split(" ").filter(Boolean).length}`);
    });
 
    this.eventBus.on("state:changed", (state: string) => {
      console.log(`🔔 Toolbar cập nhật cho state: ${state}`);
    });
  }
 
  type(text: string): void {
    this.commandManager.execute(new WriteCommand(this.doc, text));
    this.eventBus.emit("content:changed", this.doc.getContent());
  }
 
  undoTyping(): void {
    this.commandManager.undo();
    this.eventBus.emit("content:changed", this.doc.getContent());
  }
 
  submitForReview(): void {
    this.workflowState.submit({ setState: (s: DocumentState) => {
      this.workflowState = s;
      this.eventBus.emit("state:changed", s.getName());
    }} as any);
  }
}
 
const editor = new DocumentEditor();
editor.type("Xin chào");
editor.type(", Design Patterns!");
editor.undoTyping();
editor.submitForReview();

So Sánh Ba Pattern

Tiêu ChíObserverCommandState
Mục đíchNotify nhiều khi có thay đổiĐóng gói actionThay đổi behavior theo state
CouplingDecouple publisher và subscriberDecouple invoker và receiverDecouple context và state logic
Lợi ích chínhAdd/remove subscriber runtimeUndo/redo, queueKhông cần conditional
Lưu stateKhôngCó (để undo)Có (behavior theo state)
ChiềuMột-nhiều broadcastMột action → receiverObject delegate xuống state
Thực tếEvent system, React stateUI action, giao dịchWorkflow, state machine

Framework Thực Tế Dùng Ba Pattern Này

Observer Trong Thực Tế

  • React / Vue / Angular — component re-render là Observer: state thay đổi notify component (subscriber)
  • Redux — store.subscribe(listener) là Observer kinh điển
  • RxJS — Observable.subscribe() là Observer nâng cao
  • Node.js EventEmitter — emitter.on("event", callback) là Observer
  • DOM Events — addEventListener là Observer

Command Trong Thực Tế

  • Git — mỗi commit là Command; git revert là undo
  • Browser history — history.pushState() / history.back() là Command
  • Database transaction — COMMIT/ROLLBACK đóng gói command + undo
  • Redux action — dispatched action là Command; reducer là receiver
  • Text editor (VS Code, IntelliJ) — Ctrl+Z là Command.undo()

State Trong Thực Tế

  • Giao thức TCP/IP — CLOSED → LISTEN → ESTABLISHED → TIME_WAIT
  • Promise — pending → fulfilled | rejected
  • React hooks — useState quản lý state transition
  • Hệ thống đơn hàng — Chờ xử lý → Đang xử lý → Đang giao → Đã giao
  • Video player — Dừng → Đang phát → Tạm dừng → Đang tải

Tóm Tắt và Những Điểm Cần Nhớ

Bài này hoàn thành series OOP & Design Patterns. Đây là những gì mỗi pattern cuối cùng mang lại:

Observer Pattern — Publish/Subscribe:

✅ Định nghĩa quan hệ một-nhiều: một subject, nhiều observer
✅ Observer subscribe/unsubscribe runtime — zero thay đổi ở subject
✅ Loose coupling: subject chỉ biết Observer interface, không biết concrete class
✅ Nền tảng của mọi event-driven architecture

Command Pattern — Action Được Đóng Gói:

✅ Biến request thành object — lưu, queue, log, delay, hay replay chúng
✅ Có undo/redo bằng cách lưu reverse operation
✅ Decouple trigger (button) khỏi action (receiver)
✅ Macro command kết hợp nhiều command thành một

State Pattern — State Machine Không Có Conditional:

✅ Mỗi state là một class — behavior nằm đúng chỗ
✅ State object tự xử lý transition — không có central dispatcher
✅ Thêm state mới không cần đụng vào state hiện có
✅ Method của context gọn — chúng chỉ delegate xuống


🎉 Series Hoàn Thành!

Bạn đã hoàn thành series OOP & Design Patterns — cả 15 bài gồm:

  • Phase 1: Bốn trụ cột, class, encapsulation, inheritance, polymorphism
  • Phase 2: SOLID principles
  • Phase 3: Creational pattern — Singleton, Factory, Builder, Prototype
  • Phase 4: Structural pattern — Adapter, Facade, Decorator, Proxy
  • Phase 5: Behavioral pattern — Strategy, Template Method, Observer, Command, State

Bạn giờ có nền tảng vững chắc trong object-oriented design. Những pattern này xuất hiện trong mọi framework và codebase lớn — từ đây bạn sẽ nhận ra chúng ở khắp nơi.

Series Tiếp Theo Được Đề Xuất

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.