命令模式Command
1、意图
行为模式:这类模式负责对象间的高效沟通和职责委派。
命令模式是一种行为设计模式, 它可将请求转换为一个包含与请求相关的所有信息的独立对象。 该转换让你能根据不同的请求将方法参数化、 延迟请求执行或将其放入队列中, 且能实现可撤销操作。
问题
假如你正在开发一款新的文字编辑器, 当前的任务是创建一个包含多个按钮的工具栏, 并让每个按钮对应编辑器的不同操作。 你创建了一个非常简洁的 按钮
类, 它不仅可用于生成工具栏上的按钮, 还可用于生成各种对话框的通用按钮。
应用中的所有按钮都可以继承相同的类
尽管所有按钮看上去都很相似, 但它们可以完成不同的操作 (打开、 保存、 打印和应用等)。 你会在哪里放置这些按钮的点击处理代码呢? 最简单的解决方案是在使用按钮的每个地方都创建大量的子类。 这些子类中包含按钮点击后必须执行的代码。
但是你很快意识到这种方式有严重缺陷,创建了大量的子类, 当每次修改基类 按钮
时, 你都有可能需要修改所有子类的代码。 多个类实现同一功能。
复制按钮
CopyButton子类包含复制文字的代码是可行的。 在实现了上下文菜单、 快捷方式和其他功能后, 你要么需要将复制文字的代码复制进许多个类中, 要么需要让菜单依赖于按钮, 而后者是更糟糕的选择。
解决方案
优秀的软件设计通常会将关注点进行分离, 而这往往会导致软件的分层。 最常见的例子: 一层负责用户图像界面; 另一层负责业务逻辑。 GUI 层负责在屏幕上渲染美观的图形, 捕获所有输入并显示用户和程序工作的结果。 当需要完成一些重要内容时,GUI 层则会将工作委派给业务逻辑底层。
命令模式建议 GUI 对象不直接提交这些请求。 你应该将请求的所有细节 (例如调用的对象、 方法名称和参数列表) 抽取出来组成命令类, 该类中仅包含一个用于触发请求的方法。
命令对象负责连接不同的 GUI 和业务逻辑对象。 此后, GUI 对象无需了解业务逻辑对象是否获得了请求, 也无需了解其对请求进行处理的方式。 GUI 对象触发命令即可, 命令对象会自行处理所有细节工作。
下一步是让所有命令实现相同的接口。 该接口通常只有一个没有任何参数的执行方法, 让你能在不和具体命令类耦合的情况下使用同一请求发送者执行不同命令。 此外还有额外的好处, 现在你能在运行时切换连接至发送者的命令对象, 以此改变发送者的行为。
你可能会注意到遗漏的一块拼图——请求的参数。 GUI 对象可以给业务层对象提供一些参数。 但执行命令方法没有任何参数, 所以我们如何将请求的详情发送给接收者呢? 答案是: 使用数据对命令进行预先配置, 或者让其能够自行获取数据。
让我们回到文本编辑器。 应用命令模式后, 我们不再需要任何按钮子类来实现点击行为。 我们只需在 按钮
Button基类中添加一个成员变量来存储对于命令对象的引用, 并在点击后执行该命令即可。
你需要为每个可能的操作实现一系列命令类, 并且根据按钮所需行为将命令和按钮连接起来
其他菜单、 快捷方式或整个对话框等 GUI 元素都可以通过相同方式来实现。 当用户与 GUI 元素交互时, 与其连接的命令将会被执行。 现在你很可能已经猜到了, 与相同操作相关的元素将会被连接到相同的命令, 从而避免了重复代码。
真实世界类比
你去餐厅吃饭,服务员记下你点的食物,写在一张纸上,把它贴在墙上。过了一段时间, 厨师拿到了订单来准备食物。 厨师将做好的食物和订单一起放在托盘上。 服务员看到托盘后对订单进行检查, 确保所有食物都是你要的, 然后将食物放到了你的桌上。订单上记录了是哪个餐桌点的食物。
那张纸就是一个命令, 它在厨师开始烹饪前一直位于队列中。 命令中包含与烹饪这些食物相关的所有信息。 厨师能够根据它马上开始烹饪, 而无需跑来直接和你确认订单详情。同时命令上记录了发送者。
模式结构
1) 发送者(Invoker)对请求进行初始化, 它包含一个成员变量来存储对命令对象的引用。 发送者触发命令, 不直接向接收者发送请求。 注意, 发送者并不负责创建命令对象,命令对象是通过构造函数在客户端预先创建的。
2) 命令(Command)接口仅声明一个执行命令的方法
3) 具体命令(Concrete Commands)会实现各种类型的请求,自身并不完成工作, 而是将它们委派给一个业务逻辑对象(接收者)。 接收者执行方法所需的参数可以声明为成员变量params。 你可以将命令对象设为不可变, 仅允许通过构造函数对这些成员变量进行初始化(receiver和params)。
4) 接收者(Receiver) 类包含部分业务逻辑,几乎任何对象都可以作为接收者。绝大部分命令只处理如何将请求传递到接收者的细节, 接收者自己会完成实际的工作,reciver.operation(params)
5) 客户端(Client)会创建并配置具体命令对象。 客户端必须将接收者实体和所有请求参数传递给命令的构造函数。 此后, 生成的命令就可以与一个或多个发送者相关联了。
适合应用场景
-
如果你需要通过操作来参数化对象, 可使用命令模式。
命令模式可将特定的方法调用转化为独立的命令对象。 这一改变也带来了许多有趣的应用: 你可以将命令作为方法的参数进行传递、 将命令保存在其他对象中, 或者在运行时切换已连接的命令等。
举个例子: 你正在开发一个 GUI 组件 (例如上下文菜单), 你希望用户能够配置菜单项, 并在点击菜单项时触发操作。
-
如果你想要将操作放入队列中、 操作的执行或者远程执行操作, 可使用命令模式。
具体的操作是委派给命令对象的成员变量接收者,同其他对象一样, 命令也可以实现序列化 (序列化的意思是转化为字符串), 从而能方便地写入文件或数据库中。 一段时间后, 该字符串可被恢复成为最初的命令对象。 因此, 你可以延迟或计划命令的执行。 但其功能远不止如此! 使用同样的方式, 你还可以将命令放入队列、 记录命令或者通过网络发送命令。
-
如果你想要实现操作回滚功能, 可使用命令模式。
尽管有很多方法可以实现撤销和恢复功能, 但命令模式可能是其中最常用的一种。
为了能够回滚操作, 你需要实现已执行操作的历史记录功能。 命令历史记录是一种包含所有已执行命令对象及其相关程序状态备份的栈结构。
这种方法有两个缺点。 首先, 程序状态的保存功能并不容易实现, 因为部分状态可能是私有的。 你可以使用备忘录模式来在一定程度上解决这个问题。
其次, 备份状态(对象修改前的状态)可能会占用大量内存。 因此, 有时你需要借助另一种实现方式: 命令无需恢复原始状态, 而是执行反向操作。 反向操作也有代价: 它可能会很难甚至是无法实现。
实现方式
- 声明仅有一个执行方法的命令接口。
- 抽取请求并使之成为实现命令接口的具体命令类。 每个类都必须有一组成员变量来保存请求参数和对于实际接收者对象的引用。 所有这些变量的数值都必须通过命令构造函数进行初始化。通常是在客户端调用的时候初始化命令对象。
- 找到担任发送者职责的类。 在这些类中添加保存命令的成员变量。 发送者只能通过命令接口与其命令进行交互。 发送者自身通常并不创建命令对象, 而是通过客户端代码获取。
- 修改发送者使其执行命令, 而非直接将请求发送给接收者。
- 客户端必须按照以下顺序来初始化对象:
- 创建接收者。
- 创建命令, 如有需要可将其关联至接收者。
- 创建发送者并将其与特定命令关联。
优缺点
- 单一职责原则。 你可以解耦触发和执行操作的类。
- 开闭原则。 你可以在不修改已有客户端代码的情况下在程序中创建新的命令。
- 你可以实现撤销和恢复功能。
- 你可以实现操作的延迟执行
- 你可以将一组简单命令组合成一个复杂命令。
缺点
代码可能会变得更加复杂, 因为你在发送者和接收者之间增加了一个全新的层次-命令对象。
与其他模式的关系
-
你可以同时使用命令和备忘录模式来实现 “撤销”。 在这种情况下, 命令用于对目标对象执行各种不同的操作, 备忘录用来保存一条命令执行前该对象的状态。
-
命令和策略模式看上去很像, 因为两者都能通过某些行为来参数化对象。 但是, 它们的意图有非常大的不同。
a) 你可以使用命令来将任何操作转换为对象。 操作的参数将成为对象的成员变量。 你可以通过转换来延迟操作的执行、 将操作放入队列、 保存历史命令或者向远程服务发送命令等。
b) 另一方面, 策略通常可用于描述完成某件事的不同方式, 让你能够在同一个上下文类中切换算法。
- 原型模式可用于保存命令的历史记录。
- 你可以将访问者模式视为命令模式的加强版本, 其对象可对不同类的多种对象执行操作。
代码示例
在java中使用
复杂度:1
流行度:3
命令模式在 Java 代码中很常见。 大部分情况下, 它被用于代替包含行为的参数化 UI 元素的回调函数, 此外还被用于对任务进行排序和记录操作历史记录等。
以下是在核心 Java 程序库中的一些示例:
-
java.lang.Runnable
的所有实现 -
javax.swing.Action
的所有实现
识别方法: 命令模式可以通过抽象或接口类型 (发送者) 中的行为方法来识别, 该类型调用另一个不同的抽象或接口类型 (接收者) 实现中的方法, 该实现则是在创建时由命令模式的实现封装。 命令类通常仅限于一些特殊行为。
文字编辑器和撤销
本例中的文字编辑器在每次用户与其互动时, 都会创建一个新的命令对象。 命令执行其行为后会被压入历史堆栈。 现在, 当程序执行撤销操作时, 它就需要从历史记录中取出最近执行的命令, 然后执行反向操作或者恢复由该命令保存的编辑器历史状态。
1) commands/Command.java: 抽象基础命令
public abstract class Command {
public Editor editor;
private String backup;
Command(Editor editor) {
this.editor = editor;
}
// 备份
void backup() {
backup = editor.textField.getText();
}
// 撤销
public void undo() {
editor.textField.setText(backup);
}
// 执行
public abstract boolean execute();
}
2) 具体命令对象 commands/CopyCommand.java: 将所选文字复制到剪贴板
public class CopyCommand extends Command {
public CopyCommand(Editor editor) {
super(editor);
}
@Override
public boolean execute() {
editor.clipboard = editor.textField.getSelectedText();
return false;
}
}
3) 具体命令对象 commands/PasteCommand.java: 从剪贴板粘贴文字
public class PasteCommand extends Command {
public PasteCommand(Editor editor) {
super(editor);
}
@Override
public boolean execute() {
if (editor.clipboard == null || editor.clipboard.isEmpty()) return false;
backup();
editor.textField.insert(editor.clipboard, editor.textField.getCaretPosition());
return true;
}
}
4) 具体命令对象 commands/CutCommand.java: 将文字剪切到剪贴板
public class CutCommand extends Command {
public CutCommand(Editor editor) {
super(editor);
}
@Override
public boolean execute() {
if (editor.textField.getSelectedText().isEmpty()) return false;
backup();
String source = editor.textField.getText();
editor.clipboard = editor.textField.getSelectedText();
editor.textField.setText(cutString(source));
return true;
}
private String cutString(String source) {
String start = source.substring(0, editor.textField.getSelectionStart());
String end = source.substring(editor.textField.getSelectionEnd());
return start + end;
}
}
5) 命令历史 commands/CommandHistory.java
public class CommandHistory {
private Stack<Command> history = new Stack<>();
public void push(Command c) {
history.push(c);
}
public Command pop() {
return history.pop();
}
public boolean isEmpty() { return history.isEmpty(); }
}
编辑器 editor/Editor.java
public class Editor {
public JTextArea textField;
public String clipboard;
private CommandHistory history = new CommandHistory();
public void init() {
JFrame frame = new JFrame("Text editor (type & use buttons, Luke!)");
JPanel content = new JPanel();
frame.setContentPane(content);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
textField = new JTextArea();
textField.setLineWrap(true);
content.add(textField);
JPanel buttons = new JPanel(new FlowLayout(FlowLayout.CENTER));
JButton ctrlC = new JButton("Ctrl+C");
JButton ctrlX = new JButton("Ctrl+X");
JButton ctrlV = new JButton("Ctrl+V");
JButton ctrlZ = new JButton("Ctrl+Z");
Editor editor = this;
ctrlC.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
executeCommand(new CopyCommand(editor));
}
});
ctrlX.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
executeCommand(new CutCommand(editor));
}
});
ctrlV.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
executeCommand(new PasteCommand(editor));
}
});
ctrlZ.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
undo();
}
});
buttons.add(ctrlC);
buttons.add(ctrlX);
buttons.add(ctrlV);
buttons.add(ctrlZ);
content.add(buttons);
frame.setSize(450, 200);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
private void executeCommand(Command command) {
if (command.execute()) {
history.push(command);
}
}
private void undo() {
if (history.isEmpty()) return;
Command command = history.pop();
if (command != null) {
command.undo();
}
}
}
客户端代码
public class Demo {
public static void main(String[] args) {
Editor editor = new Editor();
editor.init();
}
}
OutputDemo.png: 执行结果