设计模式(二十):命令模式(Command Pattern)

Published on 2024-04-10 11:10 in 分类: 博客 with 狂盗一枝梅
分类: 博客

一、命令模式定义

将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。(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):创建具体命令对象并配置调用者。客户端决定哪些命令与哪些接收者相关联,并将命令对象传递给调用者。

命令模式是一个比较难的模式,接下来看一个案例来理解命令模式的使用。

二、命令模式案例

命令模式的经典案例是文本编辑器案例

image-20240409174836572

在文本编辑器中,我们可以通过选中指定文本,使用菜单中提供的复制、剪切、删除等功能操作指定文本,甚至操作失误可以通过撤销功能回滚到之前的版本。这一个个按钮上的功能,都可以理解为一个“命令”,接下来就使用命令模式来模拟实现这个案例,其类图如下所示

命令模式-文本编辑器类图

类图看起来有些复杂,先讲解下基本的实现思路:在这个应用程序中,包含着编辑器(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.


#设计模式
What do you think?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
Comments
  • Latest
  • Oldest
  • Hottest
复制 复制成功