那天下午三点半,团队里的小王突然把键盘一推,抓着自己头发说:"这个撤销功能根本做不了!"我们正在开发的在线文档编辑器已经迭代到第三周,所有基础编辑功能都已就位,唯独这个看似简单的"撤销"按钮,让整个团队陷入了泥沼。
问题出在架构上。小王在每个按钮的点击事件里直接写死了业务逻辑:加粗按钮直接操作DOM修改样式,删除按钮直接调用splice方法修改数据。这种写法在初期确实快速见效,但当我们需要追溯操作历史时,系统就像得了失忆症——没有任何操作记录,自然也无法回退。
想象你走进一家餐厅,如果跳过服务员直接对厨师喊"来份宫保鸡丁",会发生什么?厨师确实会立即开始烹饪,但:
这正是小王代码的现状——UI组件(顾客)直接调用了业务逻辑(厨师),没有中间的命令对象(订单)作为缓冲。命令模式的核心价值,就是引入这个"订单系统",让每个操作都变成可追溯、可撤销的独立对象。
先看改造前的危险代码:
javascript复制// 危险写法:业务逻辑直接耦合在UI事件中
boldButton.addEventListener('click', () => {
document.getElementById('content').style.fontWeight = 'bold';
});
采用命令模式后,同样的功能变为:
javascript复制// 1. 定义命令类
class BoldCommand {
constructor(element) {
this.element = element;
this.previousWeight = null;
}
execute() {
this.previousWeight = this.element.style.fontWeight;
this.element.style.fontWeight = 'bold';
}
undo() {
this.element.style.fontWeight = this.previousWeight;
}
}
// 2. 使用命令
const command = new BoldCommand(document.getElementById('content'));
boldButton.addEventListener('click', () => command.execute());
这个简单的改造带来了三个关键优势:
实际项目中的命令往往更复杂。以文档编辑器为例,我们需要处理多种命令类型:
javascript复制class CommandManager {
constructor() {
this.history = [];
this.redoStack = [];
}
execute(command) {
command.execute();
this.history.push(command);
this.redoStack = []; // 新命令清空重做栈
}
undo() {
const command = this.history.pop();
if (command) {
command.undo();
this.redoStack.push(command);
}
}
redo() {
const command = this.redoStack.pop();
if (command) {
command.execute();
this.history.push(command);
}
}
}
// 复合命令示例
class MacroCommand {
constructor() {
this.commands = [];
}
add(command) {
this.commands.push(command);
}
execute() {
this.commands.forEach(cmd => cmd.execute());
}
undo() {
[...this.commands].reverse().forEach(cmd => cmd.undo());
}
}
关键设计原则:命令对象应该像快递员——只负责传递包裹(调用方法),不关心包裹里是什么(业务逻辑)。所有业务计算应该留在接收者(如Editor类)中实现。
数据库事务的原子性要求正适合命令模式。假设我们需要实现银行转账:
javascript复制class TransferCommand {
constructor(fromAccount, toAccount, amount) {
this.fromAccount = fromAccount;
this.toAccount = toAccount;
this.amount = amount;
}
execute() {
if (this.fromAccount.balance < this.amount) {
throw new Error('Insufficient balance');
}
this.fromAccount.balance -= this.amount;
this.toAccount.balance += this.amount;
}
undo() {
this.toAccount.balance -= this.amount;
this.fromAccount.balance += this.amount;
}
}
// 使用示例
const cmd = new TransferCommand(accountA, accountB, 100);
try {
cmd.execute();
commandManager.execute(cmd);
} catch (e) {
console.error('Transfer failed:', e.message);
}
命令对象可以存储在队列中延迟处理:
javascript复制class TaskQueue {
constructor() {
this.queue = [];
this.timer = null;
}
addCommand(command) {
this.queue.push(command);
if (!this.timer) {
this.timer = setInterval(this.process.bind(this), 1000);
}
}
process() {
const command = this.queue.shift();
if (command) {
command.execute();
} else {
clearInterval(this.timer);
this.timer = null;
}
}
}
通过序列化命令对象,可以实现操作审计:
javascript复制class LoggableCommand {
constructor(receiver, action, params) {
this.receiver = receiver;
this.action = action;
this.params = params;
this.timestamp = new Date();
}
execute() {
this.receiver[this.action](...this.params);
this.log();
}
log() {
const entry = {
action: this.action,
params: this.params,
timestamp: this.timestamp.toISOString()
};
console.log('Action logged:', entry);
// 实际项目中可发送到服务器
}
}
长时间运行的应用程序可能积累大量命令对象,需要特殊处理:
javascript复制class OptimizedCommandManager {
constructor(maxHistory = 50) {
this.maxHistory = maxHistory;
this.history = [];
}
execute(command) {
command.execute();
this.history.push(command);
// 环形缓冲区策略
if (this.history.length > this.maxHistory) {
this.history.shift();
}
}
}
某些操作(如网络请求)无法简单撤销,需要特殊标记:
javascript复制class IrreversibleCommand {
constructor() {
this.isIrreversible = true;
}
execute() {
// 发送网络请求...
}
undo() {
if (this.isIrreversible) {
console.warn('This operation cannot be undone');
return false;
}
}
}
命令对象的测试应覆盖三个关键方面:
javascript复制describe('BoldCommand', () => {
let element, command;
beforeEach(() => {
element = { style: { fontWeight: '' } };
command = new BoldCommand(element);
});
it('should apply bold style on execute', () => {
command.execute();
expect(element.style.fontWeight).toBe('bold');
});
it('should restore original style on undo', () => {
element.style.fontWeight = 'normal';
command.execute();
command.undo();
expect(element.style.fontWeight).toBe('normal');
});
});
为命令添加调试信息可以帮助排查问题:
javascript复制class DebuggableCommand {
constructor() {
this.id = Math.random().toString(36).slice(2, 9);
}
execute() {
console.log(`[CMD ${this.id}] Executing at ${new Date().toISOString()}`);
// ...实际逻辑
}
undo() {
console.log(`[CMD ${this.id}] Undoing at ${new Date().toISOString()}`);
// ...实际逻辑
}
}
初级阶段:单个命令对象
javascript复制const cmd = { execute: () => console.log('Done') };
中级阶段:支持撤销的命令
javascript复制const cmd = {
execute: saveContent,
undo: restoreContent
};
高级阶段:支持事务的复合命令
javascript复制const transaction = new TransactionCommand([
new AddTextCommand('Hello'),
new FormatCommand('bold')
]);
命令模式常与其他模式结合使用:
与备忘录模式结合:
javascript复制class StatefulCommand {
constructor(target) {
this.target = target;
this.backup = null;
}
saveState() {
this.backup = JSON.stringify(this.target.state);
}
restoreState() {
this.target.state = JSON.parse(this.backup);
}
}
与责任链模式结合:
javascript复制class ChainedCommand {
constructor(commands) {
this.commands = commands;
}
execute() {
for (const cmd of this.commands) {
if (!cmd.execute()) {
return false; // 链式中断
}
}
return true;
}
}
在真实项目中,命令模式的价值往往在需求变更时凸显。就像我们文档编辑器项目,当产品经理突然要求增加"操作回放"功能时,因为已经采用命令模式,我们只需要新增一个CommandPlayer类:
javascript复制class CommandPlayer {
constructor(history) {
this.history = history;
}
replay(speed = 1) {
const interval = 1000 / speed;
this.history.forEach((cmd, i) => {
setTimeout(() => {
cmd.execute();
console.log(`Replaying step ${i + 1}`);
}, i * interval);
});
}
}
这种架构弹性,正是命令模式带给我们的长期收益。它可能在前三天让你觉得多写了20%的代码,但在第三个月会为你节省80%的调试时间。