TungDaDev's Blog

command pattern

Command pattern.webp
Published on
/9 mins read/

Các bài học nhập môn thường gói gọn Command Pattern trong ví dụ chiếc điều khiển từ xa bật/tắt bóng đèn (Remote Control & Light). Nhưng dưới góc độ của một Kiến trúc sư Hệ thống, Command Pattern mang một sứ mệnh vĩ mô hơn nhiều: Biến một động từ (hành động gọi hàm) thành một danh từ (đối tượng dữ liệu hạng nhất - First-Class Object). Khi một hành động đã được đóng gói thành một đối tượng độc lập, bạn có thể trì hoãn nó trong thời gian (Queueing / Delayed Execution), phát tán nó qua không gian mạng (RPC / Message Bus), ghi vết nó vào đĩa (Write-Ahead Logging), đảo ngược nó khi lỗi (Undo / Compensating Transaction) và mở đường cho toàn bộ mô hình kiến trúc tối tân như CQRS và Event Sourcing.

Từ hệ thống khôi phục trạng thái (Undo/Redo) trong các công cụ soạn thảo, cho đến cơ chế giao dịch bù trừ trong Saga Orchestration của Microservices, Command Pattern là khối xây dựng cơ bản để kiểm soát các tác vụ biến đổi trạng thái (State Mutations).

Bài viết này phân tích chi tiết sự tiến hóa của Command Pattern và thiết lập một Transaction Command Engine chuẩn production có khả năng hoàn tác an toàn.


# bản chất kiến trúc: biến động từ thành danh từ (objectifying actions)

Trong lập trình thông thường, một hành động là một phương thức được thực thi ngay lập tức:

// Lập trình thủ tục: Thực thi gắn chặt với thời gian và không gian
accountService.transferMoney(fromAcc, toAcc, amount);

Với Command Pattern, hành động đó được "đóng băng" thành một thực thể dữ liệu độc lập:

# các chiều phân tách vượt trội của Command

  1. Phân tách theo Thời Gian (Temporal Decoupling): Lệnh có thể được tạo ra lúc 8h sáng, nhưng được lưu vào hàng đợi và thực thi lúc 2h đêm (Batch Job / Off-Peak processing).
  2. Phân tách theo Không Gian (Spatial Decoupling): Lệnh được tạo ở Frontend Web Server, tuần tự hóa (Serialize) thành JSON, gửi qua RabbitMQ và thực thi trên Worker Pod ở một Data Center khác.
  3. Khả năng đảo ngược trạng thái (Reversibility): Mỗi lệnh mang theo đầy đủ thông tin hoặc ảnh chụp (Snapshot) để tự hoàn tác chính nó.

# kiến trúc bộ động cơ hoàn tác: dual-stack undo/redo engine

Làm thế nào để xây dựng một tính năng Undo/Redo chuẩn xác cho các phần mềm nghiệp vụ (như chỉnh sửa bảng tính tài chính hoặc bản vẽ kỹ thuật)?

Giải pháp chuẩn mực là kết hợp Command Pattern với Memento Pattern sử dụng hai ngăn xếp: Undo Stack và Redo Stack:

# hiện thực hóa undo/redo engine an toàn đa luồng

public interface TransactionCommand {
    void execute();
    void undo();
    String getDescription();
}
 
public class UndoRedoManager {
 
    private final int maxHistorySize;
    private final Deque<TransactionCommand> undoStack = new ArrayDeque<>();
    private final Deque<TransactionCommand> redoStack = new ArrayDeque<>();
 
    public UndoRedoManager(int maxHistorySize) {
        this.maxHistorySize = maxHistorySize;
    }
 
    public synchronized void executeCommand(TransactionCommand command) {
        command.execute();
 
        // Giới hạn dung lượng stack để tránh OOM Memory Leak
        if (undoStack.size() >= maxHistorySize) {
            undoStack.removeLast(); // Vứt bỏ lịch sử cũ nhất
        }
 
        undoStack.push(command);
        redoStack.clear(); // Bắt buộc xóa Redo stack khi người dùng phát sinh hành động mới
    }
 
    public synchronized boolean undo() {
        if (undoStack.isEmpty()) {
            return false;
        }
        TransactionCommand command = undoStack.pop();
        command.undo();
        redoStack.push(command);
        return true;
    }
 
    public synchronized boolean redo() {
        if (redoStack.isEmpty()) {
            return false;
        }
        TransactionCommand command = redoStack.pop();
        command.execute();
        undoStack.push(command);
        return true;
    }
}

# tiến hóa lên kiến trúc CQRS (Command query responsibility segregation)

Trong các hệ thống phân tán quy mô lớn (High-Load Architecture), Command Pattern phát triển thành một phong cách kiến trúc hoàn chỉnh: CQRS.

# nguyên tắc bất biến của CQRS

  1. Commands: Đại diện cho ý định biến đổi trạng thái (Create, Update, Delete). Đặt tên theo động từ mệnh lệnh (CreateOrderCommand, CancelSubscriptionCommand). Không bao giờ trả về dữ liệu nghiệp vụ, chỉ trả về trạng thái (Success / Failure / TransactionId).
  2. Queries: Đại diện cho hành vi tra cứu dữ liệu (GetOrderByIdQuery). Tuyệt đối không được làm biến đổi trạng thái (Side-Effect Free).

# Saga pattern: compensating transactions (giao dịch bù trừ)

Trong kiến trúc Microservices, bạn không thể dùng Distributed Two-Phase Commit để rollback khi một bước bị lỗi. Command Pattern cung cấp cơ chế Giao Dịch Bù Trừ (Compensating Transaction) trong Saga:

# bản chất của compensating Command

  • Không phải là một phép "Rollback DB" vật lý.
  • Đó là một Command nghiệp vụ mới làm nhiệm vụ đảo ngược tác động logic của lệnh trước đó:
    • Lệnh trước: ChargeCreditCardCommand($100)
    • Lệnh bù trừ: RefundCreditCardCommand($100)

# hiện thực hóa: ledger balance Command với khả năng hoàn tác

Dưới đây là một ví dụ thực chiến trong hệ thống Core Banking: Thực hiện lệnh chuyển tiền có lưu lại Memento trạng thái số dư trước đó để hỗ trợ hoàn tác an toàn:

public class TransferMoneyCommand implements TransactionCommand {
 
    private final AccountRepository accountRepo;
    private final String sourceAccId;
    private final String targetAccId;
    private final BigDecimal amount;
 
    // Memento: Lưu trữ trạng thái số dư trước khi chuyển để phục vụ undo
    private BigDecimal preSourceBalance;
    private BigDecimal preTargetBalance;
    private boolean executed = false;
 
    public TransferMoneyCommand(
            AccountRepository accountRepo,
            String sourceAccId,
            String targetAccId,
            BigDecimal amount) {
        this.accountRepo = accountRepo;
        this.sourceAccId = sourceAccId;
        this.targetAccId = targetAccId;
        this.amount = amount;
    }
 
    @Override
    @Transactional
    public void execute() {
        var source = accountRepo.findById(sourceAccId).orElseThrow();
        var target = accountRepo.findById(targetAccId).orElseThrow();
 
        // 1. Chụp ảnh Memento trước khi mutate
        this.preSourceBalance = source.getBalance();
        this.preTargetBalance = target.getBalance();
 
        // 2. Kiểm tra số dư và trừ tiền
        if (source.getBalance().compareTo(amount) < 0) {
            throw new InsufficientBalanceException("Số dư không đủ");
        }
 
        source.setBalance(source.getBalance().subtract(amount));
        target.setBalance(target.getBalance().add(amount));
 
        accountRepo.save(source);
        accountRepo.save(target);
        this.executed = true;
    }
 
    @Override
    @Transactional
    public void undo() {
        if (!executed) return;
 
        var source = accountRepo.findById(sourceAccId).orElseThrow();
        var target = accountRepo.findById(targetAccId).orElseThrow();
 
        // Phục hồi lại chính xác số dư từ Memento
        source.setBalance(preSourceBalance);
        target.setBalance(preTargetBalance);
 
        accountRepo.save(source);
        accountRepo.save(target);
        this.executed = false;
    }
 
    @Override
    public String getDescription() {
        return "Chuyển tiền: " + amount + " từ " + sourceAccId + " sang " + targetAccId;
    }
}

# bảng kiểm tra sẵn sàng vận hành (architect's checklist)

  • 1. Giới Hạn Kích Thước Undo Stack: Đảm bảo undoStack có cấu hình dung lượng tối đa (ví dụ: 50 phần tử) để tránh rò rỉ bộ nhớ Heap khi lưu trữ hàng ngàn commands dài hạn.
  • 2. Tính Bất Biến Của Command (Immutability): Dữ liệu tham số đầu vào của Command phải là bất biến (sử dụng Java Record) để ngăn chặn việc bị can thiệp giữa lúc tạo và lúc thực thi.
  • 3. Tính Idempotent Của Lệnh Bù Trừ: Mọi Compensating Command trong Saga phải được thiết kế có tính Idempotent (chạy 1 lần hay nhiều lần đều an toàn nếu bị retry do mạng).
  • 4. Serialization Support: Nếu Command được lưu vào Queue hoặc Database, đảm bảo class có thể chuyển đổi JSON 2 chiều một cách trơn tru qua Jackson.
  • 5. Audit Logging: Mọi Command thực thi làm thay đổi trạng thái nhạy cảm phải được ghi nhận vào bảng Audit Trail kèm thông tin người dùng và thời gian.

# lời kết

Command Pattern là cầu nối quan trọng nhất kết nối tư duy Lập trình Hướng đối tượng (OOP) với Kiến trúc Hệ thống Phân tán (Distributed Systems).

Bằng cách đóng gói các hành động thành những thực thể độc lập, bạn mở ra vô vàn cánh cửa kiến trúc cao cấp: từ bộ cơ chế Undo/Redo thanh lịch, mô hình phân tách đọc/ghi CQRS, cho đến khả năng tự phục hồi bền bỉ của Saga Compensating Transactions. Đó chính là bước nhảy vọt khẳng định tư duy kiến trúc của một Kỹ sư Hệ thống hàng đầu.


Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.