设计模式(二十):命令模式(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.


#设计模式
目录