Mẫu Lệnh: Giải Quyết Vấn Đề Hoàn Tác Trong Phát Triển Frontend 🚀
Cơn Ác Mộng Hoàn Tác
Hãy tưởng tượng bạn đang xây dựng một trình chỉnh sửa văn bản phong phú. Người dùng gõ, định dạng, xóa, dán hình ảnh—và rồi họ sẽ nhấn Ctrl+Z với hy vọng mọi thứ sẽ quay ngược lại. Nếu không có Mẫu Lệnh, bạn sẽ phải đối mặt với "Cơn Ác Mộng Hoàn Tác": một câu lệnh switch khổng lồ cố gắng theo dõi mọi hành động có thể xảy ra, hoặc tệ hơn, cố gắng đảo ngược các thay đổi trạng thái như một nhà khảo cổ mã.
Bạn đã trải qua chưa? Chắc chắn rồi, ai cũng đã từng.
Mẫu Lệnh giúp bạn thoát khỏi cơn ác mộng này. Thay vì xây dựng một hệ thống hoàn tác, bạn sẽ xây dựng một cỗ máy thời gian. Đóng gói mọi hành động trong một đối tượng lệnh, xếp chồng chúng lại, và việc hoàn tác trở nên đơn giản như việc lấy các mục ra khỏi ngăn xếp. Đừng cầu nguyện cho việc theo dõi trạng thái hoàn hảo—hãy xếp hàng lệnh và để chúng tự xử lý.
Kiểm Tra Thực Tế Remote Điều Khiển
Hãy bắt đầu với một điều gì đó quen thuộc. Bạn đã xây dựng một remote điều khiển đa năng tuyệt vời với ba nút ma thuật—nó có thể điều khiển mọi thứ! Tất nhiên, bạn thử nghiệm trên TV, mèo và vợ/chồng của bạn:
Remote TV – Hoạt động như một giấc mơ. Nhấn TurnOn → TV sáng. Nhấn TurnOff → tối om. Nhấn ChangeChannel → chương trình mới xuất hiện. Cuối cùng, một bộ thu thực sự nhận lệnh! 📺
Remote Mèo – Cực kỳ hỗn loạn. Nhấn CuddleCommand → mèo xuất hiện ở góc đối diện. Nhấn StopBeggingForFood → mèo nuốt cả bát thức ăn, rồi kêu như thể đã đói hàng thập kỷ. Nhấn BehaveCommand → mèo nhìn bạn và từ từ đẩy cốc cà phê của bạn xuống bàn. Kinh điển. 😹
Remote Vợ/Chồng – Nhấn DoDishesCommand... im lặng. Nhấn ListenToMeCommand... vẫn đang tải. Nhấn FixFaucetCommand... bạn biết kết quả rồi đấy. 😂
Điều tuyệt vời ở đây là: cùng một cấu trúc remote, nhưng kết quả hoàn toàn khác nhau. Mỗi bộ thu hiểu lệnh theo cách riêng của nó. Remote không quan tâm liệu nó đang nói chuyện với một chiếc TV logic, một chú mèo hỗn loạn, hay một người vợ/chồng không phản hồi—nó chỉ thực hiện lệnh và tiếp tục.
Đó chính là Mẫu Lệnh: tách rời người gọi lệnh khỏi bộ thu, đóng gói yêu cầu trong các gói gọn gàng, và để mỗi bộ thu xử lý theo cách mà chúng thấy phù hợp.
Cơn Hoảng Loạn Vào Chiều Thứ Sáu
Bạn đã bao giờ gặp PM của mình thản nhiên nói "Ôi, chúng ta có thể thêm chức năng hoàn tác không?" vào chiều thứ Sáu lúc 4 giờ chưa? Nếu mã của bạn chưa sử dụng Mẫu Lệnh, bạn sẽ phải trải qua một cuối tuần phẫu thuật kiến trúc trên một chuyến tàu đang di chuyển.
Vấn đề là: thêm chức năng hoàn tác vào mã hiện có giống như cố gắng lắp túi khí vào một chiếc xe đang chạy. Với Mẫu Lệnh, chức năng hoàn tác đã được tích hợp từ ngày đầu tiên.
Mẫu Lệnh: Thực Tế Trong Mã
Hãy cùng xây dựng từng bước, bắt đầu với bộ thu hợp tác nhất mà con người biết đến: một bóng đèn. Khác với mèo (hoặc vợ/chồng), bóng đèn có đúng hai trạng thái và luôn phản hồi với lệnh.
typescript
// Giao diện Lệnh - hợp đồng cho tất cả các lệnh
interface ICommand {
execute(): void;
unexecute(): void; // Phần ma thuật cho hoàn tác
}
// Bộ thu - thứ thực sự thực hiện công việc
class Light {
private isOn = false;
turnOn() {
this.isOn = true;
console.log("Bóng đèn đã BẬT");
}
turnOff() {
this.isOn = false;
console.log("Bóng đèn đã TẮT");
}
}
// Lệnh - các hành động được đóng gói trong đối tượng
class LightOnCommand implements ICommand {
constructor(private light: Light) {}
execute() {
this.light.turnOn();
}
unexecute() {
this.light.turnOff(); // Biết cách hoàn tác chính nó
}
}
class LightOffCommand implements ICommand {
constructor(private light: Light) {}
execute() {
this.light.turnOff();
}
unexecute() {
this.light.turnOn(); // Mỗi lệnh là một cỗ máy thời gian của riêng nó
}
}
// Người gọi - điều khiển từ xa của bạn
class RemoteControl {
private undoStack: ICommand[] = [];
private redoStack: ICommand[] = [];
constructor(private on: ICommand, private off: ICommand) {}
pressOn() {
this.on.execute();
this.undoStack.push(this.on);
this.redoStack = []; // Xóa redo khi có hành động mới
}
pressOff() {
this.off.execute();
this.undoStack.push(this.off);
this.redoStack = [];
}
pressUndo() {
if (this.undoStack.length > 0) {
const command = this.undoStack.pop()!;
command.unexecute(); // Lệnh biết cách hoàn tác chính nó
this.redoStack.push(command);
}
}
pressRedo() {
if (this.redoStack.length > 0) {
const command = this.redoStack.pop()!;
command.execute();
this.undoStack.push(command);
}
}
}
// Sử dụng - đơn giản và rõ ràng
const light = new Light();
const onCommand = new LightOnCommand(light);
const offCommand = new LightOffCommand(light);
const remote = new RemoteControl(onCommand, offCommand);
remote.pressOn(); // Bóng đèn đã BẬT
remote.pressOff(); // Bóng đèn đã TẮT
remote.pressUndo(); // Bóng đèn đã BẬT (hoàn tác lệnh tắt)
remote.pressRedo(); // Bóng đèn đã TẮT (làm lại lệnh tắt)
Lưu ý cách remote không biết gì về bóng đèn—nó chỉ thực hiện các lệnh. Đó là ma thuật của việc tách rời.
Thực Tế Frontend: Nơi Bạn Đã Thấy Điều Này Trước Đây
Mẫu Lệnh không phải là một bài tập học thuật—bạn tương tác với nó hàng ngày:
- Hoàn tác trong Google Docs - Mỗi lần gõ phím là một lệnh
- Các thao tác trong Figma/Canva - Vẽ, di chuyển, định dạng—tất cả đều là lệnh
- VS Code - Mỗi lần chỉnh sửa, định dạng, tái cấu trúc đều được đóng gói dưới dạng lệnh
- Các trình chỉnh sửa ảnh - Cắt, lọc, điều chỉnh—mỗi bước đều có thể hoàn tác vì nó là một lệnh
- Bất kỳ ứng dụng nào có Ctrl+Z - Nếu nó có chức năng hoàn tác, có khả năng nó sử dụng Mẫu Lệnh
Bất cứ khi nào bạn thấy "Chỉnh sửa → Hoàn tác" trong menu, có ai đó đã triển khai Mẫu Lệnh (hoặc nên làm vậy).
Vấn Đề Nút Tái Sử Dụng
Đây là nơi Mẫu Lệnh trở thành vũ khí bí mật của bạn. Hãy tưởng tượng một nút Hoàn tác duy nhất mà bạn có thể đặt ở bất kỳ đâu trong ứng dụng React của bạn—thông báo toaster, modals, trình chỉnh sửa WYSIWYG. Cùng một nút, nhưng hành vi hoàn toàn khác nhau.
Nút không biết nó đang hoàn tác điều gì. Mỗi ngữ cảnh cung cấp lệnh riêng của mình:
typescript
// Giao diện lệnh Hoàn tác
interface UndoCommand {
execute(): void;
}
// Nút Hoàn tác Đa Năng - hoạt động ở bất kỳ đâu
const UndoButton = ({ command }: { command: UndoCommand }) => (
<button onClick={() => command.execute()}>
↶ Hoàn tác
</button>
);
// Các lệnh khác nhau cho các ngữ cảnh khác nhau
const toasterUndo: UndoCommand = {
execute: () => showDeletedItemAgain(),
};
const modalUndo: UndoCommand = {
execute: () => reopenModal(),
};
const editorUndo: UndoCommand = {
execute: () => undoLastTextOperation(),
};
// Cùng một nút, nhưng sức mạnh siêu khác nhau
function App() {
return (
<div>
<ToastNotification>
<UndoButton command={toasterUndo} />
</ToastNotification>
<Modal>
<UndoButton command={modalUndo} />
</Modal>
<TextEditor>
<UndoButton command={editorUndo} />
</TextEditor>
</div>
);
}
Một nút để thống trị chúng. Mẫu Lệnh làm cho các thành phần của bạn thực sự có thể tái sử dụng.
Quản Lý Bộ Nhớ: Thực Tế Khắc Nghiệt
Đây là điều mà các hướng dẫn không nói với bạn: Mẫu Lệnh có thể làm đầy RAM của bạn. Mỗi hành động có thể hoàn tác sống trong bộ nhớ cho đến khi bạn xóa nó. Tôi đã học được điều này theo cách khó khăn khi người dùng làm sập ứng dụng của chúng tôi bằng cách gõ một cuốn tiểu thuyết.
Chiến lược sản xuất:
- Giới hạn ngăn xếp hoàn tác (tối đa 50-100 lệnh)
- Gộp các thao tác nhỏ (hợp nhất các nhấn phím thành các lệnh cấp độ từ)
- Nén các hành động lặp lại (10 lần di chuyển con trỏ → 1 lệnh "di chuyển phải 10")
- Xóa khi có các thao tác lớn (lưu tệp, điều hướng trang)
typescript
class SmartRemoteControl {
private undoStack: ICommand[] = [];
private readonly MAX_UNDO_HISTORY = 50;
execute(command: ICommand) {
command.execute();
this.undoStack.push(command);
// Ngăn ngừa rò rỉ bộ nhớ
if (this.undoStack.length > this.MAX_UNDO_HISTORY) {
this.undoStack.shift(); // Tạm biệt, lệnh cũ nhất
}
}
}
Các Mẫu Nâng Cao: Nâng Cao Trò Chơi Của Bạn
Lệnh Macro - Nhóm các thao tác thành siêu lệnh:
typescript
// "Định dạng Đoạn" = Chọn Tất cả + Đậm + Căn giữa
const formatParagraph = new MacroCommand([
new SelectAllCommand(),
new BoldCommand(),
new AlignCenterCommand()
]);
// Một lần hoàn tác đảo ngược tất cả ba thao tác
Chuỗi Lệnh - Xây dựng các đường ống tự động quay ngược lại khi thất bại:
typescript
// Đường ống gửi biểu mẫu
new CommandPipeline()
.add(new ValidateFormCommand())
.add(new SaveDraftCommand())
.add(new SubmitToAPICommand())
.add(new ShowSuccessCommand())
.executeWithRollback(); // Nếu bất kỳ bước nào thất bại, các bước trước tự động hoàn tác
Khi Nào KHÔNG Sử Dụng Mẫu Lệnh
Đừng trở thành nhà phát triển biến mọi thứ thành lệnh. Bỏ qua nó cho:
- Các chuyển đổi đơn giản (
isDark ? 'dark' : 'light') - Các hành động một lần sẽ không bao giờ cần hoàn tác
- Các đường dẫn quan trọng về hiệu suất (việc dispatch lệnh có chi phí)
- CRUD cơ bản nơi bạn chỉ gọi một phương thức
Nếu bạn đang tạo lệnh chỉ để gọi một phương thức đơn lẻ mà không cần hoàn tác, bạn đang suy nghĩ quá nhiều. Không phải mỗi lần nhấn nút đều cần phải là một lệnh.
Kết Luận
Mẫu Lệnh biến hỗn loạn thành trật tự:
- Người gọi (nút, remote) không cần biết mình điều khiển cái gì
- Lệnh đóng gói yêu cầu và xử lý logic hoàn tác của chính nó
- Bộ thu thực hiện công việc mà không biết ai đã gọi chúng
- Bạn có chức năng hoàn tác/làm lại miễn phí, cùng với sự linh hoạt tuyệt vời
Nó tương đương với việc có một remote đa năng thực sự hoạt động. Bạn sẽ cảm ơn bản thân khi các yêu cầu thay đổi trong sprint tiếp theo (và chúng luôn thay đổi).
Bạn có câu chuyện nào về Mẫu Lệnh không? Hãy chia sẻ trong phần bình luận—tất cả chúng ta đều đã trải qua.