设计模式(十五)行为型:命令模式详解
设计模式(十五)行为型:命令模式详解
命令模式(Command Pattern)是 GoF 23 种设计模式中的行为型模式之一,其核心价值在于将“请求”封装为一个独立的对象,从而使请求的发送者与接收者解耦,并支持请求的参数化、队列化、日志记录、撤销/重做等高级功能。它通过引入“命令对象”作为中间层,将调用操作的行为抽象化,使得系统可以动态地配置、组合、调度和管理操作。命令模式是实现“开闭原则”和“单一职责原则”的典范,广泛应用于图形界面操作(菜单、按钮)、事务处理、宏命令、远程调用、任务调度、撤销机制、工作流引擎等需要灵活控制行为的场景,是构建可扩展、可维护、可回溯系统的基石。
一、详细介绍
命令模式解决的是“调用者与执行者之间存在紧耦合,导致难以扩展、复用或控制操作”的问题。在传统设计中,调用者(如按钮)直接调用接收者(如文档)的方法(如 save()
),导致调用者依赖于具体接收者和方法,难以实现通用控制逻辑(如撤销、记录、延迟执行)。
命令模式的核心思想是:“一切皆对象”,将“操作”本身封装为一个对象(命令对象),该对象包含执行该操作所需的所有信息(接收者、方法、参数)。调用者不再直接调用方法,而是持有命令对象并调用其 execute()
方法。命令对象负责调用接收者的具体行为。
该模式包含以下核心角色:
- Command(命令接口):定义执行操作的接口,通常包含
execute()
方法。有时也包含undo()
、redo()
方法用于撤销/重做。 - ConcreteCommand(具体命令类):实现
Command
接口,绑定一个具体的接收者对象,并在execute()
中调用接收者的相应方法。它封装了“做什么”和“由谁做”。 - Receiver(接收者):真正执行操作的对象,包含业务逻辑的具体实现。命令对象会调用接收者的功能方法。
- Invoker(调用者):持有命令对象,通过调用命令的
execute()
方法来执行请求。调用者不关心命令的具体内容,只与Command
接口交互。 - Client(客户端):创建接收者、具体命令对象,并将命令对象注入到调用者中。客户端负责组装命令与调用者的关系。
命令模式的关键优势:
- 解耦调用者与接收者:调用者不依赖具体接收者,只依赖命令接口。
- 支持操作的参数化:调用者可以接受任何实现了
Command
接口的对象。 - 支持操作的队列化与日志化:命令对象可被存储在队列中,实现异步执行或日志记录。
- 支持撤销/重做:通过在命令中保存状态,实现
undo()
和redo()
。 - 支持宏命令(组合命令):多个命令可组合成一个复合命令,实现批处理。
- 支持事务性操作:一组命令可作为一个事务执行,失败时整体回滚。
与“策略模式”相比,命令模式封装的是行为的调用,策略模式封装的是算法的实现;命令关注“执行什么操作”,策略关注“如何完成任务”。与“观察者模式”相比,命令是主动触发,观察者是被动通知。
二、命令模式的UML表示
以下是命令模式的标准 UML 类图:
#mermaid-svg-DAkl9wANDtZTLugK {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-DAkl9wANDtZTLugK .error-icon{fill:#552222;}#mermaid-svg-DAkl9wANDtZTLugK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DAkl9wANDtZTLugK .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-DAkl9wANDtZTLugK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DAkl9wANDtZTLugK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DAkl9wANDtZTLugK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DAkl9wANDtZTLugK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DAkl9wANDtZTLugK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DAkl9wANDtZTLugK .marker.cross{stroke:#333333;}#mermaid-svg-DAkl9wANDtZTLugK svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DAkl9wANDtZTLugK g.classGroup text{fill:#9370DB;fill:#131300;stroke:none;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-DAkl9wANDtZTLugK g.classGroup text .title{font-weight:bolder;}#mermaid-svg-DAkl9wANDtZTLugK .nodeLabel,#mermaid-svg-DAkl9wANDtZTLugK .edgeLabel{color:#131300;}#mermaid-svg-DAkl9wANDtZTLugK .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-DAkl9wANDtZTLugK .label text{fill:#131300;}#mermaid-svg-DAkl9wANDtZTLugK .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-DAkl9wANDtZTLugK .classTitle{font-weight:bolder;}#mermaid-svg-DAkl9wANDtZTLugK .node rect,#mermaid-svg-DAkl9wANDtZTLugK .node circle,#mermaid-svg-DAkl9wANDtZTLugK .node ellipse,#mermaid-svg-DAkl9wANDtZTLugK .node polygon,#mermaid-svg-DAkl9wANDtZTLugK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DAkl9wANDtZTLugK .divider{stroke:#9370DB;stroke:1;}#mermaid-svg-DAkl9wANDtZTLugK g.clickable{cursor:pointer;}#mermaid-svg-DAkl9wANDtZTLugK g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-DAkl9wANDtZTLugK g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-DAkl9wANDtZTLugK .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-DAkl9wANDtZTLugK .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-DAkl9wANDtZTLugK .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-DAkl9wANDtZTLugK .dashed-line{stroke-dasharray:3;}#mermaid-svg-DAkl9wANDtZTLugK #compositionStart,#mermaid-svg-DAkl9wANDtZTLugK .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-DAkl9wANDtZTLugK #compositionEnd,#mermaid-svg-DAkl9wANDtZTLugK .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-DAkl9wANDtZTLugK #dependencyStart,#mermaid-svg-DAkl9wANDtZTLugK .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-DAkl9wANDtZTLugK #dependencyStart,#mermaid-svg-DAkl9wANDtZTLugK .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-DAkl9wANDtZTLugK #extensionStart,#mermaid-svg-DAkl9wANDtZTLugK .extension{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-DAkl9wANDtZTLugK #extensionEnd,#mermaid-svg-DAkl9wANDtZTLugK .extension{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-DAkl9wANDtZTLugK #aggregationStart,#mermaid-svg-DAkl9wANDtZTLugK .aggregation{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-DAkl9wANDtZTLugK #aggregationEnd,#mermaid-svg-DAkl9wANDtZTLugK .aggregation{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-DAkl9wANDtZTLugK .edgeTerminals{font-size:11px;}#mermaid-svg-DAkl9wANDtZTLugK :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;}implementshas ahas acreatescreatesconfigures«interface»Command+execute()+undo()ConcreteCommand-receiver: Receiver+execute()+undo()Receiver+action()Invoker-command: Command+setCommand(command: Command)+executeCommand()+undoCommand()Client+main(args: String[])
图解说明:
Command
定义统一接口。ConcreteCommand
持有Receiver
引用,在execute()
中调用其方法。Invoker
持有Command
引用,通过executeCommand()
触发执行。Client
负责创建所有对象并建立关联。
三、一个简单的Java程序实例及其UML图
以下是一个文本编辑器中“撤销功能”的示例,展示如何使用命令模式实现 Insert
和 Delete
操作的撤销。
Java 程序实例
import java.util.Stack;// 接收者:文本编辑器文档class TextDocument { private StringBuilder content = new StringBuilder(); public void insert(String text, int position) { content.insert(position, text); System.out.println(\"📝 插入: \\\"\" + text + \"\\\" at position \" + position); } public void delete(int start, int length) { String deleted = content.substring(start, start + length); content.delete(start, start + length); System.out.println(\"✂️ 删除: \\\"\" + deleted + \"\\\" from \" + start + \" to \" + (start + length - 1)); } public String getContent() { return content.toString(); }}// 命令接口interface Command { void execute(); void undo();}// 具体命令:插入文本class InsertCommand implements Command { private TextDocument document; private String text; private int position; // 保存执行前的状态用于撤销 private String previousContent; public InsertCommand(TextDocument document, String text, int position) { this.document = document; this.text = text; this.position = position; } @Override public void execute() { previousContent = document.getContent(); // 保存状态 document.insert(text, position); } @Override public void undo() { System.out.println(\"↩️ 撤销插入操作\"); // 恢复到插入前的内容 // 实际项目中可能需更精确的定位,此处简化 document = new TextDocument(); document.insert(previousContent, 0); }}// 具体命令:删除文本class DeleteCommand implements Command { private TextDocument document; private int start; private int length; private String deletedText; // 保存删除的内容用于撤销 public DeleteCommand(TextDocument document, int start, int length) { this.document = document; this.start = start; this.length = length; } @Override public void execute() { // 先保存要删除的文本 deletedText = document.getContent().substring(start, start + length); document.delete(start, length); } @Override public void undo() { System.out.println(\"↩️ 撤销删除操作\"); // 重新插入被删除的文本 document.insert(deletedText, start); }}// 调用者:编辑器工具栏/快捷键class EditorInvoker { private Command currentCommand; private Stack<Command> commandHistory = new Stack<>(); public void setCommand(Command command) { this.currentCommand = command; } public void executeCommand() { if (currentCommand != null) { currentCommand.execute(); commandHistory.push(currentCommand); // 记录用于撤销 currentCommand = null; } } public void undo() { if (!commandHistory.isEmpty()) { Command lastCommand = commandHistory.pop(); lastCommand.undo(); } else { System.out.println(\"📭 无可撤销的操作\"); } }}// 客户端使用示例public class CommandPatternDemo { public static void main(String[] args) { System.out.println(\"📝 文本编辑器 - 命令模式实现撤销功能\\n\"); // 创建接收者 TextDocument document = new TextDocument(); // 创建调用者 EditorInvoker invoker = new EditorInvoker(); // 客户端创建命令并执行 System.out.println(\"👉 执行插入操作:\"); Command insertCmd = new InsertCommand(document, \"Hello\", 0); invoker.setCommand(insertCmd); invoker.executeCommand(); System.out.println(\"当前内容: \\\"\" + document.getContent() + \"\\\"\\n\"); System.out.println(\"👉 执行删除操作:\"); Command deleteCmd = new DeleteCommand(document, 0, 3); invoker.setCommand(deleteCmd); invoker.executeCommand(); System.out.println(\"当前内容: \\\"\" + document.getContent() + \"\\\"\\n\"); System.out.println(\"👉 撤销删除操作:\"); invoker.undo(); System.out.println(\"当前内容: \\\"\" + document.getContent() + \"\\\"\\n\"); System.out.println(\"👉 撤销插入操作:\"); invoker.undo(); System.out.println(\"当前内容: \\\"\" + document.getContent() + \"\\\"\\n\"); System.out.println(\"👉 再次撤销(无操作):\"); invoker.undo(); }}
实例对应的UML图(简化版)
#mermaid-svg-lkdzyHLEplXf0vjx {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-lkdzyHLEplXf0vjx .error-icon{fill:#552222;}#mermaid-svg-lkdzyHLEplXf0vjx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lkdzyHLEplXf0vjx .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-lkdzyHLEplXf0vjx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lkdzyHLEplXf0vjx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lkdzyHLEplXf0vjx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lkdzyHLEplXf0vjx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lkdzyHLEplXf0vjx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lkdzyHLEplXf0vjx .marker.cross{stroke:#333333;}#mermaid-svg-lkdzyHLEplXf0vjx svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lkdzyHLEplXf0vjx g.classGroup text{fill:#9370DB;fill:#131300;stroke:none;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-lkdzyHLEplXf0vjx g.classGroup text .title{font-weight:bolder;}#mermaid-svg-lkdzyHLEplXf0vjx .nodeLabel,#mermaid-svg-lkdzyHLEplXf0vjx .edgeLabel{color:#131300;}#mermaid-svg-lkdzyHLEplXf0vjx .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-lkdzyHLEplXf0vjx .label text{fill:#131300;}#mermaid-svg-lkdzyHLEplXf0vjx .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-lkdzyHLEplXf0vjx .classTitle{font-weight:bolder;}#mermaid-svg-lkdzyHLEplXf0vjx .node rect,#mermaid-svg-lkdzyHLEplXf0vjx .node circle,#mermaid-svg-lkdzyHLEplXf0vjx .node ellipse,#mermaid-svg-lkdzyHLEplXf0vjx .node polygon,#mermaid-svg-lkdzyHLEplXf0vjx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lkdzyHLEplXf0vjx .divider{stroke:#9370DB;stroke:1;}#mermaid-svg-lkdzyHLEplXf0vjx g.clickable{cursor:pointer;}#mermaid-svg-lkdzyHLEplXf0vjx g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-lkdzyHLEplXf0vjx g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-lkdzyHLEplXf0vjx .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-lkdzyHLEplXf0vjx .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-lkdzyHLEplXf0vjx .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-lkdzyHLEplXf0vjx .dashed-line{stroke-dasharray:3;}#mermaid-svg-lkdzyHLEplXf0vjx #compositionStart,#mermaid-svg-lkdzyHLEplXf0vjx .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-lkdzyHLEplXf0vjx #compositionEnd,#mermaid-svg-lkdzyHLEplXf0vjx .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-lkdzyHLEplXf0vjx #dependencyStart,#mermaid-svg-lkdzyHLEplXf0vjx .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-lkdzyHLEplXf0vjx #dependencyStart,#mermaid-svg-lkdzyHLEplXf0vjx .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-lkdzyHLEplXf0vjx #extensionStart,#mermaid-svg-lkdzyHLEplXf0vjx .extension{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-lkdzyHLEplXf0vjx #extensionEnd,#mermaid-svg-lkdzyHLEplXf0vjx .extension{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-lkdzyHLEplXf0vjx #aggregationStart,#mermaid-svg-lkdzyHLEplXf0vjx .aggregation{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-lkdzyHLEplXf0vjx #aggregationEnd,#mermaid-svg-lkdzyHLEplXf0vjx .aggregation{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-lkdzyHLEplXf0vjx .edgeTerminals{font-size:11px;}#mermaid-svg-lkdzyHLEplXf0vjx :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;}implementsimplementshas ahas ahas acreatescreatescreatesusesTextDocument-content: StringBuilder+insert(text: String, position: int)+delete(start: int, length: int)+getContent()«interface»Command+execute()+undo()InsertCommand-document: TextDocument-text: String-position: int-previousContent: String+execute()+undo()DeleteCommand-document: TextDocument-start: int-length: int-deletedText: String+execute()+undo()EditorInvoker-currentCommand: Command-commandHistory: Stack+setCommand(command: Command)+executeCommand()+undo()Client+main(args: String[])
运行说明:
TextDocument
是接收者,执行实际的插入和删除。InsertCommand
和DeleteCommand
封装操作及撤销所需的状态。EditorInvoker
作为调用者,执行命令并维护历史栈。- 客户端通过配置命令对象实现撤销功能。
四、总结
命令模式使用建议:
- 可结合工厂模式或注解自动注册命令。
- 使用泛型减少命令类数量。
- 在 Java 中,
Runnable
接口是命令模式的简化版。 - Spring 的
@Transactional
方法可视为命令的事务封装。
架构师洞见:
命令模式是“行为可序列化”与“操作可管理”的哲学体现。在现代架构中,其思想已演变为事件溯源(Event Sourcing)、CQRS(命令查询职责分离) 和消息队列的核心。例如,在事件溯源中,每个状态变更都记录为一个“事件命令”;在 CQRS 中,命令模型负责处理写操作,查询模型负责读操作;在微服务中,命令通过消息队列异步传递,实现解耦与弹性。未来趋势是:命令模式将与AI Agent结合,Agent 的“行动计划”可视为命令序列;在低代码平台中,用户拖拽操作生成命令对象;在区块链中,每笔交易本质是一个不可变的命令;在边缘计算中,命令可被缓存并在网络恢复后执行。
掌握命令模式,有助于设计出可追溯、可恢复、可扩展的系统。作为架构师,应在涉及“操作历史”、“事务控制”或“异步处理”的场景中主动引入命令模式。命令不仅是模式,更是系统可管理性的基石——它告诉我们:真正的控制力,来自于将“行为”本身作为一等公民进行建模与管理。