命令模式(学习笔记)
1. 意图
将请求转换为一个包含与请求相关的所有信息的独立对象。该转换让你能根据不同的请求将方法参数化、延迟请求执行或将其放入队列中,且能实现可撤销操作
2. 动机
假如开发一款新的文字编辑器,当前的任务是创建一个包含多个按钮的工具栏,并让每个按钮对应编辑器的不同操作。创建了一个非常简洁的按钮
类,它不仅可用于生成工具栏上的按钮,还可用于生成各种对话框的通用按钮。尽管所有按钮看上去都很相似, 但它们可以完成不同的操作 (打开、 保存、 打印和应用等)。 问题是在哪里放置这些按钮的点击处理代码呢? 最简单的解决方案是在使用按钮的每个地方都创建大量的子类。 这些子类中包含按钮点击后必须执行的代码。
但是这种方式有严重的缺陷。首先,创建了大量的子类,当每次修改基类按钮时,都有可能需要修改所有子类的代码。简单来说,GUI 代码以一种拙劣的方式依赖于业务逻辑中的不稳定代码(违背了依赖倒置原则)。更棘手的是,复制/粘贴文字等操作可能会在多个地方被调用。例如用户可以点击工具栏上小小的 “复制” 按钮,或者通过上下文菜单复制一些内容,又或者直接使用键盘上的 Ctrl+C
。我们的程序最初只有工具栏,因此可以使用按钮子类来实现各种不同操作。换句话来说,复制按钮
CopyButton子类包含复制文字的代码是可行的。在实现了上下文菜单、快捷方式和其他功能后,要么需要将操作代码复制进许多个类中,要么需要让菜单依赖于按钮,而后者是更糟糕的选择
优秀的软件设计通常会将变化的部分进行封装,而这往往会导致软件的分层。最常见的例子:一层负责用户图像界面;另一层负责业务逻辑。GUI 层负责在屏幕上渲染美观的图形,捕获所有输入并显示用户和程序工作的结果。当需要完成一些重要内容时(比如计算月球轨道或撰写年度报告),GUI 层则会将工作委派给业务逻辑底层。在代码中就是,一个 GUI 对象传递一些参数来调用一个业务逻辑对象。这个过程通常被描述为一个对象发送请求给另一个对象。
命令模式建议 GUI 对象不直接提交这些请求。 应该将请求的所有细节 (例如调用的对象、 方法名称和参数列表) 抽取出来组成命令类, 该类中仅包含一个用于触发请求的方法。GUI 对象触发命令即可,命令对象会自行处理所有细节工作。所有命令实现相同的接口。该接口通常只有一个没有任何参数的执行方法,让你能在不和具体命令类耦合的情况下使用同一请求发送者执行不同命令。此外还有额外的好处,现在你能在运行时切换连接至发送者的命令对象,以此改变发送者的行为。
3. 适用性
- 如果需要通过操作来参数化对象,可以使用命令模式
命令模式可将特定的方法调用转化为独立对象。故而可以将命令作为方法的参数进行传递、将命令保存在其他对象中,或者在运行时切换已连接的命令等。
- 如果想要将操作放入队列中或者远程执行操作,可使用命令模式
同其他对象一样,命令也可以实现序列化(序列化的意思是转化为字符串),从而能方便地写入文件或数据库中。一段时间后,该字符串可被恢复成为最初的命令对象。因此,你可以延迟或计划命令的执行。但其功能远不止如此!使用同样的方式,你还可以将命令放入队列、记录命令或者通过网络发送命令
- 如果你想要实现操作回滚功能, 可使用命令模式
尽管有很多方法可以实现撤销和恢复功能,但命令模式可能是其中最常用的一种。为了能够回滚操作,你需要实现已执行操作的历史记录功能。命令历史记录是一种包含所有已执行命令对象及其相关程序状态备份的栈结构。这种方法有两个缺点:
首先,程序状态的保存功能并不容易实现,因为部分状态可能是私有的。你可以使用备忘录模式来在一定程度上解决这个问题。
其次,备份状态可能会占用大量内存。因此,有时你需要借助另一种实现方式:命令无需恢复原始状态,而是执行反向操作。反向操作也有代价:它可能会很难甚至是无法实现
- 支持修改日志,这样在系统崩溃时,修改可以被重做一遍。在command接口中添加装载操作和存储操作,可以用来保持一个一致的修改日志。从崩溃中恢复的过程包括从磁盘中重新读入记录下的命令并用Execute操作重新执行它们
- 用构建在原语操作上的高层操作构建一个系统。这样一种结构在支持事物的信息系统中很常见。一个事务封装了对数据的一组变动。Command模式提供了对事务进行建模的方法。Command有一个公共接口,使得你可以用同一种方式调用所有的事务。同时,使用该模式也易于添加新事务以扩展系统
4. 结构
5. 效果
1. Command模式将调用操作的对象与知道如何实现该操作的对象解耦(单一职责原则)
2. 实现撤销和恢复功能
3. 实现操作的延迟执行
4. 可以将多个命令装配成一个组合命令。一般来说,组合命令是Composite模式的一个实例
5. 可以在不修改客户端代码的情况下,在程序中创建新的命令(开闭原则)
6. 代码实现
commands/Command.java: 抽象基础命令
package command.commands; import command.editor.Editor; /** * @author GaoMing * @date 2021/7/25 - 20:05 */ 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(); }
commands/CopyCommand.java: 将所选文字复制到剪贴板
package command.commands; import command.editor.Editor; /** * @author GaoMing * @date 2021/7/25 - 20:06 */ public class CopyCommand extends Command { public CopyCommand(Editor editor) { super(editor); } @Override public boolean execute() { editor.clipboard = editor.textField.getSelectedText(); return false; } }
commands/PasteCommand.java: 从剪贴板粘贴文字
package command.commands; import command.editor.Editor; /** * @author GaoMing * @date 2021/7/25 - 20:06 */ 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; } }
commands/CutCommand.java: 将文字剪切到剪贴板
package command.commands; import command.editor.Editor; /** * @author GaoMing * @date 2021/7/25 - 20:06 */ 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; } }
commands/CommandHistory.java: 命令历史
package command.commands; import java.util.Stack; /** * @author GaoMing * @date 2021/7/25 - 20:06 */ 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: 文字编辑器的 GUI
package command.editor; import command.commands.*; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; /** * @author GaoMing * @date 2021/7/25 - 20:06 */ 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(); } } }
Demo.java: 客户端代码
package command; import command.editor.Editor; /** * @author GaoMing * @date 2021/7/25 - 20:05 */ public class Demo { public static void main(String[] args) { Editor editor = new Editor(); editor.init(); } }
运行结果
7. 与其他模式的关系
-
责任链模式、命令模式、中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式:
- 责任链按照顺序将请求动态传递给一系列的潜在接收者,直至其中一名接收者对请求进行处理
- 命令在发送者和请求者之间建立单向连接
- 中介者清除了发送者和请求者之间的直接连接,强制它们通过一个中介对象进行间接沟通
- 观察者允许接收者动态地订阅或取消接收请求 - 可以同时使用命令和备忘录模式来实现“撤销”。在这种情况下,命令用于对目标对象执行各种不同的操作,备忘录用来保存一条命令执行前该对象的状态
- 原型模式可用于保存命令的历史记录
- 可以将访问者模式视为命令模式的加强版本,其对象可对不同类的多种对象执行操作
-
命令和策略模式看上去很像,因为两者都能通过某些行为来参数化对象。但是,它们的意图有非常大的不同:
- 可以使用命令来将任何操作转换为对象。操作的参数将成为对象的成员变量。你可以通过转换来延迟操作的执行、将操作放入队列、保存历史命令或者向远程服务发送命令等
- 策略通常可用于描述完成某件事的不同方式,让你能够在同一个上下文类中切换算法 -
责任链的管理者可使用命令模式实现。在这种情况下,你可以对由请求代表的同一个上下文对象执行许多不同的操作
还有另外一种实现方式,那就是请求自身就是一个命令对象。在这种情况下,你可以对由一系列不同上下文连接而成的链执行相同的操作
8. 已知应用
使用示例:命令模式在 Java 代码中很常见。大部分情况下,它被用于代替包含行为的回调函数,此外还被用于对任务进行排序和记录操作历史记录等
以下是在核心 Java 程序库中的一些示例:
java.lang.Runnable 的所有实现
javax.swing.Action 的所有实现
识别方法:命令模式可以通过抽象或接口类型(发送者)中的行为方法来识别,该类型调用另一个不同的抽象或接口类型(接收者)实现中的方法,该实现则是在创建时由命令模式的实现封装。