设计模式:命令模式

意图

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

命令模式结构

命令设计模式的结构

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

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

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

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

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

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

命令模式适合应用场景

  • 如果你需要通过操作来参数化对象, 可使用命令模式。

命令模式可将特定的方法调用转化为独立对象。 这一改变也带来了许多有趣的应用: 你可以将命令作为方法的参数进行传递、 将命令保存在其他对象中, 或者在运行时切换已连接的命令等。

举个例子: 你正在开发一个 GUI 组件 (例如上下文菜单), 你希望用户能够配置菜单项, 并在点击菜单项时触发操作。

  • 如果你想要将操作放入队列中、 操作的执行或者远程执行操作, 可使用命令模式。

同其他对象一样, 命令也可以实现序列化 (序列化的意思是转化为字符串), 从而能方便地写入文件或数据库中。 一段时间后, 该字符串可被恢复成为最初的命令对象。 因此, 你可以延迟或计划命令的执行。 但其功能远不止如此! 使用同样的方式, 你还可以将命令放入队列、 记录命令或者通过网络发送命令。

  • 如果你想要实现操作回滚功能, 可使用命令模式。

尽管有很多方法可以实现撤销和恢复功能, 但命令模式可能是其中最常用的一种。

为了能够回滚操作, 你需要实现已执行操作的历史记录功能。 命令历史记录是一种包含所有已执行命令对象及其相关程序状态备份的栈结构。

这种方法有两个缺点。 首先, 程序状态的保存功能并不容易实现, 因为部分状态可能是私有的。 你可以使用备忘录模式来在一定程度上解决这个问题。

其次, 备份状态可能会占用大量内存。 因此, 有时你需要借助另一种实现方式: 命令无需恢复原始状态, 而是执行反向操作。 反向操作也有代价: 它可能会很难甚至是无法实现。

命令模式优缺点

优点:

  •  单一职责原则。 你可以解耦触发和执行操作的类。
  •  开闭原则。 你可以在不修改已有客户端代码的情况下在程序中创建新的命令。
  •  你可以实现撤销和恢复功能。
  •  你可以实现操作的延迟执行。
  •  你可以将一组简单命令组合成一个复杂命令。

缺点:

  • 代码可能会变得更加复杂, 因为你在发送者和接收者之间增加了一个全新的层次。

与其他模式的关系

  • 责任链模式、 命令模式、 中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式:

    • 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
    • 命令在发送者和请求者之间建立单向连接。
    • 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。
    • 观察者允许接收者动态地订阅或取消接收请求。
  • 责任链的管理者可使用命令模式实现。 在这种情况下, 你可以对由请求代表的同一个上下文对象执行许多不同的操作。

    还有另外一种实现方式, 那就是请求自身就是一个命令对象。 在这种情况下, 你可以对由一系列不同上下文连接而成的链执行相同的操作。

  • 你可以同时使用命令和备忘录模式来实现 “撤销”。 在这种情况下, 命令用于对目标对象执行各种不同的操作, 备忘录用来保存一条命令执行前该对象的状态。

  • 命令和策略模式看上去很像, 因为两者都能通过某些行为来参数化对象。 但是, 它们的意图有非常大的不同。

    • 你可以使用命令来将任何操作转换为对象。 操作的参数将成为对象的成员变量。 你可以通过转换来延迟操作的执行、 将操作放入队列、 保存历史命令或者向远程服务发送命令等。

    • 另一方面, 策略通常可用于描述完成某件事的不同方式, 让你能够在同一个上下文类中切换算法。

  • 原型模式可用于保存命令的历史记录。

  • 你可以将访问者模式视为命令模式的加强版本, 其对象可对不同类的多种对象执行操作。

在 Java 中使用模式

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

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

示例代码

命令 (Command)

public abstract class Command {
  public final 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();
}

 具体命令 (Concrete Commands)

public class CopyCommand extends Command {
  public CopyCommand(Editor editor) {
    super(editor);
  }

  @Override
  public boolean execute() {
    editor.clipboard = editor.textField.getSelectedText();
    return false;
  }
}
public class CutCommand extends Command {
  public CutCommand(Editor editor) {
    super(editor);
  }

  @Override
  public boolean execute() {
    if (editor.textField.getSelectedText() == null || 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;
  }
}
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;
  }
}

 接收者 (Receiver) 

public class Editor {
  public JTextArea textField;
  public String clipboard;
  private final 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(e -> executeCommand(new CopyCommand(editor)));
    ctrlX.addActionListener(e -> executeCommand(new CutCommand(editor)));
    ctrlV.addActionListener(e -> executeCommand(new PasteCommand(editor)));
    ctrlZ.addActionListener(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();
    }
  }
}

客户端 (Client)

public class Client {
  public static void main(String[] args) {
    Editor editor = new Editor();
    editor.init();
  }
}