第一次点击删除按钮时手滑误操作,看着重要数据瞬间消失却无法撤回——这种令人抓狂的体验相信每个开发者都遇到过。命令模式就像给操作界面安装了一个"时光机",它把用户动作封装成独立对象,让每个操作变得可追踪、可撤销、可排队。这种设计模式在IDE、图形编辑器和交易系统中无处不在,是构建健壮交互系统的秘密武器。
传统调用方式就像直接拨打电话:一旦按下呼叫键,通话过程就无法中断。假设我们有一个文本编辑器的保存功能:
java复制public class Editor {
public void save() {
// 直接写入磁盘
}
}
// 调用方式
editor.save();
这种实现存在三个致命缺陷:
命令模式引入中间层,将请求封装为独立对象。就像餐厅的点餐单把顾客需求从厨师执行中解耦:
java复制public interface Command {
void execute();
void undo();
}
public class SaveCommand implements Command {
private Editor editor;
private String prevContent;
public SaveCommand(Editor editor) {
this.editor = editor;
}
@Override
public void execute() {
prevContent = editor.getContent();
editor.save();
}
@Override
public void undo() {
editor.setContent(prevContent);
}
}
完整的命令模式包含四个关键角色:
| 角色 | 职责 | 示例 |
|---|---|---|
| Command | 声明执行接口 | SaveCommand |
| ConcreteCommand | 实现具体命令逻辑 | CopyCommand |
| Invoker | 触发命令执行 | MenuItem |
| Receiver | 真正执行操作的对象 | Document |
以文本编辑器为例,我们实现可撤销的复制操作:
java复制// 接收者
public class Document {
private String content;
public void copy(String text) {
this.content = text;
System.out.println("Copied: " + text);
}
public String getContent() {
return content;
}
}
// 命令接口
public interface Command {
void execute();
void undo();
}
// 具体命令
public class CopyCommand implements Command {
private Document document;
private String prevContent;
private String newContent;
public CopyCommand(Document doc, String text) {
this.document = doc;
this.newContent = text;
}
@Override
public void execute() {
prevContent = document.getContent();
document.copy(newContent);
}
@Override
public void undo() {
document.copy(prevContent);
}
}
// 调用者
public class MenuItem {
private Command command;
public void setCommand(Command cmd) {
this.command = cmd;
}
public void click() {
command.execute();
}
}
命令模式可以组合多个操作为原子事务:
java复制public class MacroCommand implements Command {
private List<Command> commands = new ArrayList<>();
public void add(Command cmd) {
commands.add(cmd);
}
@Override
public void execute() {
commands.forEach(Command::execute);
}
@Override
public void undo() {
// 逆序执行撤销
for(int i=commands.size()-1; i>=0; i--) {
commands.get(i).undo();
}
}
}
在游戏开发中,命令模式常用于实现操作队列:
python复制class MoveCommand:
def __init__(self, unit, x, y):
self.unit = unit
self.x = x
self.y = y
self._executed = False
def execute(self):
if not self._executed:
self.unit.move_to(self.x, self.y)
self._executed = True
class CommandQueue:
def __init__(self):
self._queue = []
def add_command(self, cmd):
self._queue.append(cmd)
def process(self):
while self._queue:
cmd = self._queue.pop(0)
cmd.execute()
高效的撤销系统需要控制内存占用:
javascript复制class UndoManager {
constructor(maxStackSize = 50) {
this.undoStack = [];
this.redoStack = [];
this.maxSize = maxStackSize;
}
execute(command) {
command.execute();
this.undoStack.push(command);
if(this.undoStack.length > this.maxSize) {
this.undoStack.shift();
}
this.redoStack = []; // 清空重做栈
}
undo() {
if(this.undoStack.length > 0) {
const cmd = this.undoStack.pop();
cmd.undo();
this.redoStack.push(cmd);
}
}
}
长时间运行的编辑器需要注意:
多线程中使用命令模式需注意:
java复制public class ThreadSafeInvoker {
private final Queue<Command> queue = new ConcurrentLinkedQueue<>();
private final Executor executor = Executors.newSingleThreadExecutor();
public void submit(Command cmd) {
queue.offer(cmd);
executor.execute(() -> {
Command next = queue.poll();
if(next != null) next.execute();
});
}
}
对于复杂状态恢复,可以结合备忘录模式:
csharp复制public class Memento {
public object State { get; }
public Memento(object state) => State = state;
}
public class ComplexCommand : ICommand {
private Receiver _receiver;
private Memento _backup;
public void Execute() {
_backup = _receiver.CreateMemento();
_receiver.DoSomething();
}
public void Undo() {
_receiver.RestoreMemento(_backup);
}
}
Redux的action本质上是命令模式的变体:
typescript复制// 定义action
const undoAction = {
type: 'UNDO',
payload: {
timestamp: Date.now()
}
}
// reducer处理
function editorReducer(state, action) {
switch(action.type) {
case 'UNDO':
return {...state, content: state.history.pop()}
//...
}
}
在Qt框架中,QUndoCommand的实现展示了专业级命令模式:
cpp复制class AppendTextCommand : public QUndoCommand {
public:
AppendTextCommand(QString *doc, const QString &text)
: m_document(doc), m_text(text) {}
void undo() override { m_document->chop(m_text.length()); }
void redo() override { m_document->append(m_text); }
private:
QString *m_document;
QString m_text;
};
在以下场景可能更适合其他方案:
两种模式的区别关键点:
| 维度 | 命令模式 | 策略模式 |
|---|---|---|
| 主要目的 | 封装操作请求 | 封装算法 |
| 典型应用 | 撤销/重做系统 | 支付方式选择 |
| 生命周期 | 可能长期存在 | 通常短期使用 |
| 状态保持 | 常包含执行状态 | 通常无状态 |
在最近的项目中,我们重构了一个绘图工具的命令系统。原先的版本直接在UI事件中调用绘图方法,导致无法实现撤销功能。通过引入命令模式,不仅实现了完整的操作历史记录,还意外获得了这些优势:
一个特别有用的技巧是给命令添加时间戳和用户信息,这为后续的分析功能提供了数据基础。比如我们发现用户平均每小时触发23次撤销操作,这促使我们改进了工具的默认参数设置。