一、命令模式定义
将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。(Encapsulate a request as an object,thereby letting you parameterize clients with different requests,queue or log requests,and support undoable operations.)
现实世界中有很多命令模式的例子,可能在用的时候你都没意识到,比如:外卖点餐。
为什么外卖点餐是个命令模式的体现呢?
中午饿了,我打开外卖APP,点了一个黄焖鸡米饭外卖,点完外卖付完款,对方餐厅就出了一张小票,小票上内容有谁点了什么菜,服务员拿着这张小票到厨房贴在墙上,墙上可能已经贴了很多小票了,厨师根据这些小票内容做菜。
在这个过程中,我们将我们的请求:黄焖鸡米饭 具象成了一个订单小票,那张纸就是一个命令, 它在厨师开始烹饪前一直位于队列中。 命令中包含与烹饪这些食物相关的所有信息。 厨师能够根据它马上开始烹饪, 而无需跑来直接和你确认订单详情。
命令模式的类图如下所示
- 命令(Command):定义了执行操作的接口。通常包含一个执行方法(如
execute()
),用于触发相关操作。 - 具体命令(Concrete Command):实现了命令接口,将操作绑定到特定的接收者对象。具体命令类通常包含一个或多个接收者对象,并将请求参数传递给相应的操作。
- 接收者(Receiver):执行实际操作的对象。命令模式将请求发送给接收者,并由接收者执行相应的操作。
- 调用者(Invoker):负责发送命令并触发相关操作。调用者并不直接执行操作,而是将命令对象传递给接收者,并调用命令对象的执行方法来触发操作。
- 客户端(Client):创建具体命令对象并配置调用者。客户端决定哪些命令与哪些接收者相关联,并将命令对象传递给调用者。
命令模式是一个比较难的模式,接下来看一个案例来理解命令模式的使用。
二、命令模式案例
命令模式的经典案例是文本编辑器案例
在文本编辑器中,我们可以通过选中指定文本,使用菜单中提供的复制、剪切、删除等功能操作指定文本,甚至操作失误可以通过撤销功能回滚到之前的版本。这一个个按钮上的功能,都可以理解为一个“命令”,接下来就使用命令模式来模拟实现这个案例,其类图如下所示
类图看起来有些复杂,先讲解下基本的实现思路:在这个应用程序中,包含着编辑器(Editor)、按钮(Buttons)等组件,但是本程序仅为了演示命令模式的使用,所以并没有实现UI,只是在命令行中运行打印相关数据,Buttons、Shortcuts并没有实现,本来比如doCopy、doCut等方法是Buttons或者Shortcuts触发运行的,这里直接将它写到Application中,在上面类图中使用虚线框框了起来。
Editor数据约定如下:
1、Editor初始化就有“你好”字符串默认值
2、Editor的getSelection默认返回“你好”
3、Editor的deleteSelection默认删除文本内的最后一个“你好”字符串
4、Editor的replaceSelection默认在文本最后追加text文本
1、源码实现
定义命令基类Command,为具体基类定义通用接口
/**
* @author kdyzm
* @date 2024/4/9
*/
public abstract class Command {
protected Application app;
protected Editor editor;
protected String backup;
public Command(Application app, Editor editor) {
this.app = app;
this.editor = editor;
}
/**
* 备份编辑器状态
*/
public void saveBackup() {
backup = editor.text;
}
/**
* 恢复编辑器状态
*/
public void undo() {
editor.text = backup;
System.out.println("执行撤销命令后:" + editor.text);
}
/**
* 执行方法被声明为抽象以强制所有具体命令提供自己的实现。
* 该方法必须根据命令是否更改编辑器的状态返回 true 或 false。
*/
abstract boolean execute();
}
定义编辑器类Editor,模拟实现编辑器的底层实现逻辑
/**
* @author kdyzm
* @date 2024/4/9
*/
public class Editor {
public String text = "你好";
/**
* 返回选中的文字
*/
public String getSelection() {
System.out.println("获取到选中的文字:你好");
return "你好";
}
/**
* 删除选中的文字
*/
public void deleteSelection() {
int index = this.text.lastIndexOf("你好");
if (index != -1) {
this.text = this.text.substring(0, index);
System.out.println("删除选中的文字后:" + this.text);
}
}
/**
* 在当前位置插入剪切板中的内容
*/
public void replaceSelection(String text) {
this.text = this.text + text;
System.out.println("插入剪切板中的内容后:" + this.text);
}
}
复制命令CopyCommand,注意复制命令不会被保存到历史记录中,因为它没有改变编辑器的状态。
/**
* @author kdyzm
* @date 2024/4/9
*/
public class CopyCommand extends Command {
public CopyCommand(Application app, Editor editor) {
super(app, editor);
}
/**
* 复制命令不会被保存到历史记录中,因为它没有改变编辑器的状态。
*/
@Override
boolean execute() {
System.out.println("执行复制命令【start】");
app.clipboard = editor.getSelection();
System.out.println("执行复制命令【end】" + "\n");
return false;
}
}
剪切命令CutCommand
/**
* @author kdyzm
* @date 2024/4/9
*/
public class CutCommand extends Command {
public CutCommand(Application app, Editor editor) {
super(app, editor);
}
/**
* 剪切命令改变了编辑器的状态,因此它必须被保存到历史记录中。
*/
@Override
boolean execute() {
System.out.println("执行剪切命令【start】");
saveBackup();
app.clipboard = editor.getSelection();
editor.deleteSelection();
System.out.println("执行剪切命令【end】" + "\n");
return true;
}
}
粘贴命令PasteCommand
/**
* @author kdyzm
* @date 2024/4/9
*/
public class PasteCommand extends Command {
public PasteCommand(Application app, Editor editor) {
super(app, editor);
}
@Override
boolean execute() {
System.out.println("执行粘贴命令【start】");
saveBackup();
editor.replaceSelection(app.clipboard);
System.out.println("执行粘贴命令【end】" + "\n");
return true;
}
}
撤销命令UndoCommand
/**
* @author kdyzm
* @date 2024/4/9
*/
public class UndoCommand extends Command {
public UndoCommand(Application app, Editor editor) {
super(app, editor);
}
@Override
boolean execute() {
System.out.println("执行撤销命令【start】");
app.undo();
System.out.println("执行撤销命令【end】" + "\n");
return false;
}
}
保存命令历史记录CommandHistory,它只记录会影响Editor内容的操作
/**
* 全局命令历史记录就是一个堆栈
* @author kdyzm
* @date 2024/4/9
*/
public class CommandHistory {
private final Stack<Command> history = new Stack<>();
/**
* 将命令压入历史记录的末尾
*/
public void push(Command command) {
history.push(command);
}
/**
* 从历史记录中取出最近的一条命令
*/
public Command pop() {
return history.pop();
}
}
最后,应用程序类Application
/**
* @author kdyzm
* @date 2024/4/9
*/
public class Application {
public String clipboard;
public Editor editor;
public CommandHistory history;
public Application() {
this.editor = new Editor();
this.history = new CommandHistory();
}
/**
* 执行一个命令并检查它是否需要被添加到历史记录中。
*/
public void executeCommand(Command command) {
if (command.execute()) {
history.push(command);
}
}
/**
* 从历史记录中取出最近的命令并运行其 undo(撤销)方法。请注意,你并
* 不知晓该命令所属的类。但是我们不需要知晓,因为命令自己知道如何撤销其动作。
*/
public void undo() {
Command command = history.pop();
if (Objects.nonNull(command)) {
command.undo();
}
}
/**
* 模拟拷贝动作,包含按钮点击和快捷键Ctrl+C
*/
public void doCopy() {
Command command = new CopyCommand(this, editor);
executeCommand(command);
}
/**
* 模拟剪切动作,包含按钮点击和快捷键Ctrl+X
*/
public void doCut() {
CutCommand cutCommand = new CutCommand(this, editor);
executeCommand(cutCommand);
}
/**
* 模拟粘贴动作,包含按钮点击和快捷键Ctrl+V
*/
public void doPaste() {
PasteCommand pasteCommand = new PasteCommand(this, editor);
executeCommand(pasteCommand);
}
/**
* 模拟撤销动作,包含按钮点击和快捷键Ctrl+Z
*/
public void doUndo() {
UndoCommand undoCommand = new UndoCommand(this, editor);
executeCommand(undoCommand);
}
}
最后,启动类
/**
* @author kdyzm
* @date 2024/4/9
*/
public class Main {
public static void main(String[] args) {
//应用程序启动初始化编辑器内文字:你好
Application app = new Application();
//复制
app.doCopy();//你好
//粘贴
app.doPaste();//你好你好
//剪切
app.doCut();//你好
//粘贴
app.doPaste();//你好你好
//撤销
app.doUndo();//你好
//撤销
app.doUndo();//你好你好
}
}
控制台输出
执行复制命令【start】
获取到选中的文字:你好
执行复制命令【end】
执行粘贴命令【start】
插入剪切板中的内容后:你好你好
执行粘贴命令【end】
执行剪切命令【start】
获取到选中的文字:你好
删除选中的文字后:你好
执行剪切命令【end】
执行粘贴命令【start】
插入剪切板中的内容后:你好你好
执行粘贴命令【end】
执行撤销命令【start】
执行撤销命令后:你好
执行撤销命令【end】
执行撤销命令【start】
执行撤销命令后:你好你好
执行撤销命令【end】
最终结果输出“你好你好”,符合预期结果
2、案例总结
本案例稍微有些复杂,类也比较多,可能会迷惑,这么多类,它们分别对应着命令模式中的什么角色?
- 命令(Command):对应着Command类
- 具体命令(Concrete Command):对应着CopyCommand、CutCommand、PasteCommand以及UndoCommand类
- 接受者(Receiver):对应着Editor类,各个命令的最终实现基本上都体现在了Editor类上的变化上。
- 调用者(Invoker):对应着Application类,每个命令都在这里发出的,具体到doCopy、doPaste等方法。
本案例中,将复制、粘贴等按钮事件或者快捷键对应的动作(请求)封装成了一个个命令Command,由Application类执行命令,命令执行最后由接受者Editor类做底层动作,执行完毕,命令的execute方法若返回true则将当前命令加入栈中(CommandHistory),撤销动作则是从栈中弹出最近一条命令,并将其保存的内容还原到Editor。
END.
注意:本文归作者所有,未经作者允许,不得转载