Command biến một thao tác thành object dữ liệu: có đủ tham số để thực hiện, và (nếu cần undo) đủ thông tin để đảo ngược.
Khi thao tác đã là dữ liệu thì nó lưu được, xếp hàng được, phát lại được.
interface Command {
execute(doc: Doc): void
undo(doc: Doc): void
}
class InsertText implements Command {
constructor(private at: number, private text: string) {}
execute(doc: Doc) { doc.insert(this.at, this.text) }
undo(doc: Doc) { doc.delete(this.at, this.text.length) }
}
const history: Command[] = []
function run(cmd: Command) { cmd.execute(doc); history.push(cmd); redoStack.length = 0 }
function undo() { history.pop()?.undo(doc) }Những điểm dễ sai khi làm thật:
- Undo phải đủ dữ liệu. Thao tác delete phải giữ lại nội dung đã xóa lúc execute, không thể suy ra sau. Lệnh không đảo ngược được thì hoặc lưu snapshot (Memento), hoặc chặn undo qua nó.
- Gộp lệnh. Gõ từng ký tự mà mỗi ký tự một lệnh thì undo trở nên khó chịu. Gộp các lệnh cùng loại trong một khoảng thời gian ngắn thành một lệnh tổng.
- Redo phải bị xóa khi có lệnh mới sau undo, nếu không lịch sử phân nhánh và trạng thái sai.
- Chạy qua queue thì lệnh phải serialize được — chỉ chứa dữ liệu thuần, không giữ tham chiếu tới object runtime. Kèm theo là yêu cầu idempotent, vì queue có thể giao lại cùng một lệnh; đưa commandId vào và bỏ qua lệnh đã xử lý.