Lập trình · Mẫu thiết kế
Mẫu Command (Design Pattern) giải thích kèm ví dụ
Trả lời nhanh
Command là một mẫu thiết kế hành vi (behavioral), biến một yêu cầu thành một đối tượng độc lập chứa mọi thứ cần thiết để thực hiện nó. Vì yêu cầu là một đối tượng, bạn có thể lưu, truyền đi, đưa vào hàng đợi, ghi log và hoàn tác sau đó. Đối tượng kích hoạt command (ví dụ một nút bấm) không cần biết đối tượng nào làm việc hay làm bằng cách nào.
Mục đích và vấn đề mà mẫu này giải quyết
Mục đích: đóng gói một yêu cầu thành một đối tượng, nhờ đó có thể tham số hóa client với các yêu cầu khác nhau, xếp hàng hoặc ghi log các yêu cầu và hỗ trợ các thao tác có thể hoàn tác. Command là một trong các mẫu hành vi trong sách Gang of Four (GoF).
Hãy nghĩ đến một trình soạn thảo văn bản. Cùng một hành động, như “in đậm văn bản”, có thể được kích hoạt bởi nút trên thanh công cụ, mục menu và phím tắt Ctrl+B. Nếu mỗi nơi này gọi thẳng trình soạn thảo, logic bị lặp ở ba chỗ. Tệ hơn, trình soạn thảo còn phải hỗ trợ undo: sau khi người dùng nhấn Ctrl+Z, chương trình cần biết hành động trước đó là gì và cách đảo ngược nó.
Command giải quyết cả hai vấn đề. Mỗi hành động trở thành một đối tượng có phương thức execute và tùy chọn phương thức undo. Nút bấm, menu và phím tắt đều giữ cùng một đối tượng command. Trình soạn thảo giữ một danh sách lịch sử các command đã thực hiện, nên undo nghĩa là lấy command cuối cùng trong danh sách và gọi undo của nó.
Cấu trúc và các thành phần
| Vai trò | Trách nhiệm |
|---|---|
| Command | Interface có phương thức execute (và thường có undo) |
| Concrete command | Lưu receiver và các tham số, rồi gọi các phương thức của receiver trong execute |
| Receiver | Đối tượng biết cách làm công việc thật sự, như một tài liệu hoặc một tài khoản ngân hàng |
| Invoker | Yêu cầu một command chạy; có thể giữ lịch sử để undo và redo |
| Client | Tạo các concrete command và nối chúng với receiver và invoker |
Cài đặt bằng TypeScript: undo và redo
TextDocument là receiver. InsertText và DeleteLast là các command nhớ đủ thông tin để tự đảo ngược. Editor là invoker với hai ngăn xếp: một cho undo và một cho redo.
interface Command {
execute(): void;
undo(): void;
}
class TextDocument {
text = "";
}
class InsertText implements Command {
constructor(private doc: TextDocument, private value: string) {}
execute() { this.doc.text += this.value; }
undo() { this.doc.text = this.doc.text.slice(0, -this.value.length); }
}
class DeleteLast implements Command {
private removed = "";
constructor(private doc: TextDocument, private count: number) {}
execute() {
this.removed = this.doc.text.slice(-this.count);
this.doc.text = this.doc.text.slice(0, -this.count);
}
undo() { this.doc.text += this.removed; }
}
class Editor {
private done: Command[] = [];
private undone: Command[] = [];
run(cmd: Command) { cmd.execute(); this.done.push(cmd); this.undone = []; }
undo() { const c = this.done.pop(); if (c) { c.undo(); this.undone.push(c); } }
redo() { const c = this.undone.pop(); if (c) { c.execute(); this.done.push(c); } }
}
const doc = new TextDocument();
const editor = new Editor();
editor.run(new InsertText(doc, "Hello"));
editor.run(new InsertText(doc, ", world"));
editor.run(new DeleteLast(doc, 7));
console.log(doc.text); // Hello
editor.undo();
console.log(doc.text); // Hello, world
editor.redo();
console.log(doc.text); // HelloTrước và sau
Không có mẫu này, giao diện gọi thẳng tài liệu và không thể undo, vì không có gì ghi lại điều đã xảy ra:
// Before: each trigger calls the receiver directly
boldButton.onclick = () => doc.makeBold(selection);
menuBold.onclick = () => doc.makeBold(selection);
// After: every trigger shares one command, and the editor records it
const bold = () => new MakeBold(doc, selection);
boldButton.onclick = () => editor.run(bold());
menuBold.onclick = () => editor.run(bold());Phiên bản thứ hai là một đoạn mã giả định có lớp command MakeBold giống các lớp ở trên. Giao diện không còn biết cách in đậm hoạt động ra sao, và mọi hành động đi qua một nơi mà ở đó chúng có thể được ghi lại, ghi log hoặc hoàn tác.
Khi nào nên dùng và khi nào không
- Dùng cho undo và redo, macro (danh sách command chạy cùng nhau) và lịch sử command.
- Dùng để xếp hàng hoặc lên lịch công việc: hàng đợi job lưu các đối tượng command và worker thực hiện chúng sau.
- Dùng để ghi log hoặc phát lại thao tác, ví dụ gửi các command đã tuần tự hóa sang máy chủ khác.
- Dùng để tách các thành phần giao diện khỏi mã mà chúng kích hoạt.
- Đừng dùng khi bạn chỉ cần một callback. Trong TypeScript, hàm vốn là giá trị hạng nhất, nên
button.onclick = savelà một command hoàn toàn ổn khi không cần undo. Hãy thêm lớp command khi bạn cần hành vi bổ sung nhưundo, tên để hiển thị hoặc tuần tự hóa.
Ứng dụng thực tế
- Interface
Runnablecủa Java là một command với một phương thứcrun, được thread và executor dùng để chạy công việc sau. - Action của Redux là các đối tượng thuần túy mô tả một thay đổi; reducer áp dụng chúng, và log các action cho phép gỡ lỗi du hành thời gian. Đây là họ hàng gần của Command, với logic nằm ở reducer thay vì ở command.
- VS Code và nhiều trình soạn thảo khác đăng ký các command có tên có thể gán vào phím, menu và command palette.
- Database migration thường có bước
upvàdown, chính là cặp execute và undo của một command.
Các mẫu liên quan
| Mẫu | Khác với Command ở điểm nào |
|---|---|
| Strategy | Strategy hoán đổi cách thực hiện một tác vụ; Command đóng gói việc cần làm (một yêu cầu) để có thể lưu, xếp hàng hoặc hoàn tác. |
| Memento | Memento lưu ảnh chụp trạng thái để undo; Command lưu thao tác và đảo ngược nó. Hai mẫu thường được kết hợp khi việc đảo ngược khó thực hiện. |
| Chain of Responsibility | Một command có thể được chuyển dọc chuỗi handler để tìm handler thực hiện nó. |
| Composite | Macro command là một Composite: một command tạo từ các command khác. |
| Observer | Observer thông báo một sự kiện cho nhiều listener; Command là một yêu cầu gửi tới một receiver. |
Câu hỏi thường gặp
Mẫu Command cài đặt undo như thế nào?
Mỗi command có phương thức undo đảo ngược execute của chính nó, thường dùng dữ liệu đã lưu trong lúc thực thi. Invoker giữ một ngăn xếp các command đã thực hiện và gọi undo trên command gần nhất.
Mẫu Command khác mẫu Strategy ở điểm nào?
Strategy biểu diễn các cách khác nhau để làm cùng một tác vụ và do đối tượng sử dụng nó chọn. Command biểu diễn một yêu cầu cụ thể, cùng receiver và tham số của nó, có thể chạy sau, xếp hàng hoặc hoàn tác.
Hàm callback có phải là command không?
Callback là một command tối giản: nó đóng gói một hành động chạy sau. Bạn cần một đối tượng command đầy đủ khi muốn thêm undo, mô tả, tuần tự hóa hoặc siêu dữ liệu khác.
Macro command là gì?
Macro command giữ một danh sách command và thực hiện chúng theo thứ tự. Undo của nó đảo ngược theo thứ tự ngược lại, đó là một ứng dụng của mẫu Composite.