在软件交互设计中,我们经常会遇到这样的场景:用户点击了一个按钮,系统立即执行了某个不可逆的操作。比如删除重要文件、提交订单付款、发布敏感内容等。这种"一锤子买卖"式的交互,往往会让用户感到不安——毕竟人非圣贤,孰能无过?
命令模式(Command Pattern)就像给操作装上了一颗"后悔药"。它把用户请求封装成独立的对象,允许我们记录操作历史、支持撤销/重做、甚至实现操作队列和延迟执行。这种设计模式最早诞生于Smalltalk语言,如今已成为GUI框架和交互系统的标配。
我曾在电商后台系统开发中深刻体会到命令模式的价值。当时产品经理要求实现一个"批量商品上架"功能,开发初期我们直接调用了上架API。结果测试阶段运营同事误操作导致500多件商品提前曝光,只能紧急联系DBA回滚数据库。重构时引入命令模式后,所有上架操作都先进入待执行队列,管理员确认无误后才真正生效,从此再没出现过类似事故。
标准的命令模式包含以下几个核心角色:
Command(命令接口)
java复制public interface Command {
void execute();
void undo();
}
ConcreteCommand(具体命令)
java复制public class DeleteFileCommand implements Command {
private FileReceiver receiver;
private String filename;
public DeleteFileCommand(FileReceiver r, String f) {
this.receiver = r;
this.filename = f;
}
@Override
public void execute() {
receiver.deleteFile(filename);
}
@Override
public void undo() {
receiver.restoreFile(filename);
}
}
Receiver(接收者)
java复制public class FileReceiver {
public void deleteFile(String name) {
// 实际删除文件逻辑
}
public void restoreFile(String name) {
// 从回收站恢复文件
}
}
Invoker(调用者)
java复制public class FileManager {
private Stack<Command> history = new Stack<>();
public void executeCommand(Command cmd) {
cmd.execute();
history.push(cmd);
}
public void undoLastCommand() {
if (!history.isEmpty()) {
Command cmd = history.pop();
cmd.undo();
}
}
}
以文本编辑器为例,完整的命令模式交互流程如下:
关键提示:命令对象应当是不可变的(immutable)。一旦创建就不应修改其内部状态,这样才能确保撤销操作能准确恢复到之前的状态。
假设我们要为一个绘图应用实现以下功能:
typescript复制interface DrawingCommand {
execute(): void;
undo(): void;
}
以移动图形命令为例:
typescript复制class MoveCommand implements DrawingCommand {
private shape: Shape;
private oldPos: Point;
private newPos: Point;
constructor(shape: Shape, newPos: Point) {
this.shape = shape;
this.oldPos = shape.position;
this.newPos = newPos;
}
execute() {
this.shape.moveTo(this.newPos);
}
undo() {
this.shape.moveTo(this.oldPos);
}
}
typescript复制class CommandHistory {
private undoStack: DrawingCommand[] = [];
private redoStack: DrawingCommand[] = [];
execute(cmd: DrawingCommand) {
cmd.execute();
this.undoStack.push(cmd);
this.redoStack = []; // 清空重做栈
}
undo() {
if (this.undoStack.length > 0) {
const cmd = this.undoStack.pop()!;
cmd.undo();
this.redoStack.push(cmd);
}
}
redo() {
if (this.redoStack.length > 0) {
const cmd = this.redoStack.pop()!;
cmd.execute();
this.undoStack.push(cmd);
}
}
}
javascript复制// 移动按钮点击事件
moveButton.addEventListener('click', () => {
const selectedShape = canvas.getSelectedShape();
const newPosition = calculateNewPosition();
const cmd = new MoveCommand(selectedShape, newPosition);
commandHistory.execute(cmd);
});
// 撤销按钮
undoButton.addEventListener('click', () => {
commandHistory.undo();
});
// 重做按钮
redoButton.addEventListener('click', () => {
commandHistory.redo();
});
复合命令:将多个操作合并为一个原子命令
typescript复制class MacroCommand implements DrawingCommand {
private commands: DrawingCommand[];
constructor(commands: DrawingCommand[]) {
this.commands = [...commands];
}
execute() {
this.commands.forEach(cmd => cmd.execute());
}
undo() {
[...this.commands].reverse().forEach(cmd => cmd.undo());
}
}
懒执行:对于耗时操作,可以实现延迟执行
typescript复制class HeavyOperationCommand implements DrawingCommand {
private executed = false;
private result: any;
execute() {
if (!this.executed) {
this.result = expensiveCalculation();
this.executed = true;
}
applyResult(this.result);
}
undo() {
revertResult(this.result);
}
}
快照式撤销:对于复杂对象,可以使用备忘录模式存储状态快照
typescript复制class StatefulCommand implements DrawingCommand {
private originator: Originator;
private backup: Memento;
constructor(originator: Originator) {
this.originator = originator;
}
execute() {
this.backup = this.originator.save();
// ...执行操作...
}
undo() {
this.originator.restore(this.backup);
}
}
在需要保证原子性的业务场景中,命令模式可以扩展实现事务:
java复制public class TransactionManager {
private List<Command> commands = new ArrayList<>();
public void addCommand(Command cmd) {
commands.add(cmd);
}
public boolean commit() {
try {
for (Command cmd : commands) {
cmd.execute();
}
return true;
} catch (Exception e) {
rollback();
return false;
}
}
private void rollback() {
for (int i = commands.size()-1; i >= 0; i--) {
commands.get(i).undo();
}
}
}
对于需要排队执行的场景(如打印任务):
python复制class PrintScheduler:
def __init__(self):
self.queue = deque()
self.current_task = None
def add_job(self, command):
self.queue.append(command)
self._process_next()
def _process_next(self):
if not self.current_task and self.queue:
self.current_task = self.queue.popleft()
threading.Thread(target=self._execute_task).start()
def _execute_task(self):
self.current_task.execute()
self.current_task = None
self._process_next()
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 撤销后状态不一致 | 命令对象被复用或修改 | 确保命令是不可变的,每次操作创建新实例 |
| 内存占用过高 | 历史记录未清理 | 设置历史栈大小限制,或定期归档旧命令 |
| 撤销操作卡顿 | 撤销逻辑过于复杂 | 改用快照式撤销,或异步执行撤销操作 |
| 重做结果不正确 | 执行新命令后未清空重做栈 | 在execute()时重置redo栈 |
对于简单场景,可以省略Receiver层,让命令对象直接包含执行逻辑:
javascript复制class AlertCommand {
constructor(message) {
this.message = message;
}
execute() {
alert(this.message);
}
undo() {
console.log('Alert cannot be undone');
}
}
虽然结构相似,但两者意图不同:
界面操作必用命令模式:任何可能需要进行撤销/重做的UI操作,都应该考虑使用命令模式
慎用全局命令历史:不同模块最好维护各自的历史栈,避免相互干扰
提供可视化历史:像Photoshop那样显示可浏览的操作历史,大幅提升用户体验
考虑持久化:将命令序列化存储,可以实现跨会话的撤销历史
在最近的一个表单设计器项目中,我们为每个字段操作都实现了命令模式。实测下来,虽然初期开发量增加了约30%,但后期维护成本降低了60%以上,特别是当产品要求新增"批量回退到上一步"功能时,我们仅用2小时就完成了迭代。这再次验证了命令模式在交互密集型系统中的长期价值。