文章目录
简介
命令是一种行为设计模式, 它能把请求转换为一个包含与请求相关的所有信息 的独立对象。这个转换能让你把请求方法参数化、延迟请求执行或把请求放在队列里,并且能实现可撤销操作。
问题
假如你正在开发一款文字编辑器,当前的任务是创建一个包含多个按钮的工具栏,并让每个按钮对应编辑器的不同操作。你创建了一 个非常简洁的 按钮 类,它不仅可用于创建工具栏上的按钮对象,还可用 于创建各种对话框中的按钮对象。

尽管所有按钮看上去都很相似,但它们可以完成不同的操作(打开 保存、打印和应用等)。这时你会把这些按钮的点击处理代码放在哪里呢?最简单的解决方案是在使用按钮的每个地方都创建大量的子类。这些 子类中包含按钮点击后必须执行的代码。

因为复制/粘贴文字等操作可能会在多个地方被调 用。比如用戶可以点击工具栏上的"复制"按钮,或者可以通过上下文菜单复制内容,也可以直接使用键盘上的 Ctrl+C 。
我们的程序最初只有工具栏,因此可以使用按钮子类来实现各种不同 操作。换句话说, 复制按钮 CopyButton 子类包含复制文字处理的代码是可行的。但是如果想使用上下文菜单或者快捷方式进行复制,你要么是把复制处理代码copy到多个类里,要么是让菜单依赖于CopyButton按钮,显然这两个都不好。
解决
优秀的软件设计通常会把关注点分离,即对软件进行分层。比如GUI应用,GUI层负责渲染用戶图像界面并和用户交互,业务逻辑层负责处理业务逻辑。 GUI 层会捕获所有输入并把工作委派给业务逻辑层。
这在代码中看上去就像这样:一个 GUI 对象传递一些参数来调用一个 业务逻辑对象。也可以说是发送请求,如下图,不同的GUI操作对应相同的操作。
而命令模式建议 GUI 对象不直接提交这些请求。而是把请求的所有细节(比如调用的对象、方法名称和参数列表)抽取出来组成命令类,在类里只包含一个用于触发请求的方法。
命令对象负责连接不同的 GUI 和业务逻辑对象。这样,GUI 对象就不需要了解业务逻辑对象有没有获得请求,也没必要了解它是怎么处理请求的。GUI 对象触发命令就可以了。命令对象会自行处理所有细节工作。如下图,相同的操作被抽取成命令对象,命令对象将GUI层和业务逻辑层联系起来。

下一步是让所有命令实现相同的接口。这个接口通常只有一个没有任何参数的执行方法,让你能在不跟具体命令类耦合的情况下使用同一请求发送者执行不同命令。而且,现在你就能通过在运行时切换连接到发送者的命令对象, 来动态改变发送者的行为。
你可能会注意到我们好像遗漏了请求的参数。GUI 对象可以给业 务层对象提供一些参数。但执行命令方法没有任何参数,所以我们怎么把请求的详情发送给接收者呢?
答案是 使用数据对命令进行预先配置, 或者让命令本身能够自行获取数据。

让我们回到文本编辑器。应用命令模式后,我们不再需要任何按钮子 类来实现点击行为。我们只需在按钮 Button 基类里添加一个成员变量来存储对于命令对象的引用, 并在点击后执行该命令就可以了。
你需要为每个可能的操作实现一系列命令类,并且根据按钮所需行为 把命令和按钮连接起来。
其他菜单、快捷方式或整个对话框等 GUI 元素都可以通过相同方式来实现。当用戶与 GUI 元素交互时,和gui连接的命令就会被执行。讲到这里 你很可能已经猜到了,与相同操作相关的元素会被连接到相同的命令, 从而避免了重复代码。
最后,命令成为了减少 GUI 和业务逻辑层之间耦合的中间层。
代码
java
// 1.Receiver 编辑器核心
class Editor {
private StringBuilder content = new StringBuilder();
public String getSelection() {
return content.toString(); // 实际应按选区范围获取
}
public void deleteSelection() {
content.setLength(0); // 清空选区
}
public void replaceSelection(String text) {
content.replace(0, content.length(), text); // 替换选区
}
public String getContent() {
return content.toString();
}
}
// 2.Command 接口层
interface Command {
void execute();
}
// 3.具体命令实现
class CopyCommand implements Command {
private Editor editor;
private String clipboard; // 仿系统剪贴板
public CopyCommand(Editor editor) {
this.editor = editor;
}
@Override
public void execute() {
clipboard = editor.getSelection(); // 获取选区存入剪贴板
}
}
class PasteCommand implements Command {
private Editor editor;
private String clipboard;
public PasteCommand(Editor editor, String clipboard) {
this.editor = editor;
this.clipboard = clipboard;
}
@Override
public void execute() {
editor.replaceSelection(clipboard); // 用剪贴板内容覆盖选区
}
}
// 4.Invoker触发器
class Toolbar {
private Command command;
public void setCommand(Command cmd) { // 动态绑定命令
this.command = cmd;
}
public void onClick() {
if(command != null) command.execute();
}
}
// 5.Client组装结构
public class App {
public static void main(String[] args) {
// 创建核心模块
Editor doc = new Editor();
doc.replaceSelection("Hello");
// 建立命令纽带
Toolbar copyBtn = new Toolbar();
copyBtn.setCommand(new CopyCommand(doc)); // 绑定复制行为
Toolbar pasteBtn = new Toolbar();
pasteBtn.setCommand(new PasteCommand(doc, "World")); // 粘贴命令携带参数
// 用户点击按钮时
copyBtn.onClick(); // 将Hello存入剪贴板
pasteBtn.onClick(); // 文档内容变为World
}
}
核心设计优势
- 解耦交互:按钮(Invoker)只需触发 execute(),无需知道具体操作
- 可扩展性:新增撤销命令只需增加 UndoCommand 类,无需修改已有逻辑
- 行为参数化:可把复制/粘贴命令存入历史栈实现撤销重做
总结

- 发送者(Sender)⸺亦称"触发者(Invoker)"⸺类负责对请求进行初始化,其中必须包含一个成员变量来存储对于命令对象的 引用。发送者触发命令,而不是向接收者直接发送请求。注意, 发送者并不负责创建命令对象:它通常会通过构造函数从客戶端那边获得预先生成的命令对象。
- 命令(Command)接口通常仅声明一个执行命令的方法。
- 具体命令(Concrete Commands)会实现各种类型的请求。具体命令自身并不完成工作, 而是会把调用委派给一个业务逻辑对象 。但为了简化代码, 这些类可以合并。 而接收对象执行方法所需要的参数可以声明为具体命令的成员变量。你可以把命令对象设为不可变,只允许通过构造函数对这些成员变量进行初始化。
- 接收者(Receiver)类包含部分业务逻辑。几乎任何对象都可以作为接收者。绝大部分命令只要关心怎么把请求传递到接收者,接收者自己会完成实际的工作。
- 客户端(Client)会创建并配置具体命令对象。客戶端必须把 包括 接收者实体在内的所有请求参数传递给命令的构造函数。 之后生成的命令就可以跟一个或多个发送者相关联了。