跳至主要內容

设计模式之命令模式

钝悟...大约 10 分钟设计设计模式设计设计模式

设计模式之命令模式

意图

命令模式(Command) 是一种行为设计模式, 它可将请求转换为一个包含与请求相关的所有信息的独立对象。 该转换让你能根据不同的请求将方法参数化、 延迟请求执行或将其放入队列中, 且能实现可撤销操作。

命令模式的交互

img
img
  • Client 创建一个 ConcreteCommand 对象并指定他的 Receiver 对象。
  • 某个 Invoker 对象存储该 ConcreteCommand 对象。
  • 该 Invoker 通过调用 Command 对象的 Execute 操作来提交一个请求。若该命令是可撤销的,ConcreteCommand 就在执行 Execute 操作之前存储当前状态以用于取消该命令。
  • ConcreteCommand 对象对调用它的 Receiver 的一些操作以执行该请求。

命令模式的要点

  • 命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。
  • 每一个命令都是一个操作:请求的一方发出请求,要求执行一个操作;接收的一方收到请求,并执行操作。
  • 命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执行,以及是怎么被执行的。
  • 命令模式使请求本身成为一个对象,这个对象和其他对象一样可以被存储和传递。
  • 命令模式的关键在于引入了抽象命令接口,且发送者针对抽象命令接口编程,只有实现了抽象命令接口的具体命令才能与接收者相关联。

适用场景

  • 如果你需要通过操作来参数化对象, 可使用命令模式。
  • 如果你想要将操作放入队列中、 操作的执行或者远程执行操作, 可使用命令模式。
  • 如果你想要实现操作回滚功能, 可使用命令模式。

结构

img
img

结构说明

  1. 发送者 (Sender)——亦称 “触发者 (Invoker)”——类负责对请求进行初始化, 其中必须包含一个成员变量来存储对于命令对象的引用。 发送者触发命令, 而不向接收者直接发送请求。 注意, 发送者并不负责创建命令对象: 它通常会通过构造函数从客户端处获得预先生成的命令。

  2. 命令 (Command) 接口通常仅声明一个执行命令的方法。

  3. 具体命令 (Concrete Commands) 会实现各种类型的请求。 具体命令自身并不完成工作, 而是会将调用委派给一个业务逻辑对象。 但为了简化代码, 这些类可以进行合并。

    接收对象执行方法所需的参数可以声明为具体命令的成员变量。 你可以将命令对象设为不可变, 仅允许通过构造函数对这些成员变量进行初始化。

  4. 接收者 (Receiver) 类包含部分业务逻辑。 几乎任何对象都可以作为接收者。 绝大部分命令只处理如何将请求传递到接收者的细节, 接收者自己会完成实际的工作。

  5. 客户端 (Client) 会创建并配置具体命令对象。 客户端必须将包括接收者实体在内的所有请求参数传递给命令的构造函数。 此后, 生成的命令就可以与一个或多个发送者相关联了。

结构代码范式

Command : 用来声明执行操作的接口。

abstract class Command {
    protected Receiver receiver;
    public Command(Receiver receiver) {
        this.receiver = receiver;
    }

    public abstract void Execute();
}

ConcreteCommand : 将一个接收者对象绑定一个动作,调用接收者相应的操作,以实现 Execute。

class ConcreteCommand extends Command {
    public ConcreteCommand(Receiver receiver) {
        super(receiver);
    }

    @Override
    public void Execute() {
        receiver.Action();
    }
}

Invoker : 要求该命令执行这个请求。

class Invoker {
    private Command command;

    public Invoker(Command command) {
        this.command = command;
    }

    public void ExecuteCommand() {
        command.Execute();
    }
}

Receiver : 知道如何实施与执行一个与请求相关的操作,任何类都可能作为一个接收者。

class Receiver {
    public void Action() {
        System.out.println("执行请求");
    }
}

Client : 创建一个具体命令对象并设定它的接受者。

public class CommandPattern {
    public static void main(String[] args) {
        Receiver receiver = new Receiver();
        Command cmd = new ConcreteCommand(receiver);
        Invoker invoker = new Invoker(cmd);
        invoker.ExecuteCommand();
    }
}

伪代码

在本例中, 命令模式会记录已执行操作的历史记录, 以在需要时撤销操作。

img
img

有些命令会改变编辑器的状态 (例如剪切和粘贴), 它们可在执行相关操作前对编辑器的状态进行备份。 命令执行后会和当前点备份的编辑器状态一起被放入命令历史 (命令对象栈)。 此后, 如果用户需要进行回滚操作, 程序可从历史记录中取出最近的命令, 读取相应的编辑器状态备份, 然后进行恢复。

客户端代码 (GUI 元素和命令历史等) 没有和具体命令类相耦合, 因为它通过命令接口来使用命令。 这使得你能在无需修改已有代码的情况下在程序中增加新的命令。

// 命令基类会为所有具体命令定义通用接口。
abstract class Command is
    protected field app: Application
    protected field editor: Editor
    protected field backup: text

    constructor Command(app: Application, editor: Editor) is
        this.app = app
        this.editor = editor

    // 备份编辑器状态。
    method saveBackup() is
        backup = editor.text

    // 恢复编辑器状态。
    method undo() is
        editor.text = backup

    // 执行方法被声明为抽象以强制所有具体命令提供自己的实现。该方法必须根
    // 据命令是否更改编辑器的状态返回 true 或 false。
    abstract method execute()


// 这里是具体命令。
class CopyCommand extends Command is
    // 复制命令不会被保存到历史记录中,因为它没有改变编辑器的状态。
    method execute() is
        app.clipboard = editor.getSelection()
        return false

class CutCommand extends Command is
    // 剪切命令改变了编辑器的状态,因此它必须被保存到历史记录中。只要方法
    // 返回 true,它就会被保存。
    method execute() is
        saveBackup()
        app.clipboard = editor.getSelection()
        editor.deleteSelection()
        return true

class PasteCommand extends Command is
    method execute() is
        saveBackup()
        editor.replaceSelection(app.clipboard)
        return true

// 撤销操作也是一个命令。
class UndoCommand extends Command is
    method execute() is
        app.undo()
        return false


// 全局命令历史记录就是一个堆桟。
class CommandHistory is
    private field history: array of Command

    // 后进...
    method push(c: Command) is
        // 将命令压入历史记录数组的末尾。

    // ...先出
    method pop():Command is
        // 从历史记录中取出最近的命令。


// 编辑器类包含实际的文本编辑操作。它会担任接收者的角色:最后所有命令都会
// 将执行工作委派给编辑器的方法。
class Editor is
    field text: string

    method getSelection() is
        // 返回选中的文字。

    method deleteSelection() is
        // 删除选中的文字。

    method replaceSelection(text) is
        // 在当前位置插入剪贴板中的内容。

// 应用程序类会设置对象之间的关系。它会担任发送者的角色:当需要完成某些工
// 作时,它会创建并执行一个命令对象。
class Application is
    field clipboard: string
    field editors: array of Editors
    field activeEditor: Editor
    field history: CommandHistory

    // 将命令分派给 UI 对象的代码可能会是这样的。
    method createUI() is
        // ...
        copy = function() { executeCommand(
            new CopyCommand(this, activeEditor)) }
        copyButton.setCommand(copy)
        shortcuts.onKeyPress("Ctrl+C", copy)

        cut = function() { executeCommand(
            new CutCommand(this, activeEditor)) }
        cutButton.setCommand(cut)
        shortcuts.onKeyPress("Ctrl+X", cut)

        paste = function() { executeCommand(
            new PasteCommand(this, activeEditor)) }
        pasteButton.setCommand(paste)
        shortcuts.onKeyPress("Ctrl+V", paste)

        undo = function() { executeCommand(
            new UndoCommand(this, activeEditor)) }
        undoButton.setCommand(undo)
        shortcuts.onKeyPress("Ctrl+Z", undo)

    // 执行一个命令并检查它是否需要被添加到历史记录中。
    method executeCommand(command) is
        if (command.execute)
            history.push(command)

    // 从历史记录中取出最近的命令并运行其 undo(撤销)方法。请注意,你并
    // 不知晓该命令所属的类。但是我们不需要知晓,因为命令自己知道如何撤销
    // 其动作。
    method undo() is
        command = history.pop()
        if (command != null)
            command.undo()

与其他模式的关系

案例

使用示例:命令模式在 Java 代码中很常见。 大部分情况下, 它被用于代替包含行为的参数化 UI 元素的回调函数, 此外还被用于对任务进行排序和记录操作历史记录等。

以下是在核心 Java 程序库中的一些示例:

识别方法: 命令模式可以通过抽象或接口类型 (发送者) 中的行为方法来识别, 该类型调用另一个不同的抽象或接口类型 (接收者) 实现中的方法, 该实现则是在创建时由命令模式的实现封装。 命令类通常仅限于一些特殊行为。

参考资料

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.7