命令

将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。

命令模式(Command)是指,把请求封装成一个命令,然后执行该命令。

在使用命令模式前,我们先以一个编辑器为例子,看看如何实现简单的编辑操作:

  1. public class TextEditor {
  2. private StringBuilder buffer = new StringBuilder();
  3. public void copy() {
  4. ...
  5. }
  6. public void paste() {
  7. String text = getFromClipBoard();
  8. add(text);
  9. }
  10. public void add(String s) {
  11. buffer.append(s);
  12. }
  13. public void delete() {
  14. if (buffer.length() > 0) {
  15. buffer.deleteCharAt(buffer.length() - 1);
  16. }
  17. }
  18. public String getState() {
  19. return buffer.toString();
  20. }
  21. }

我们用一个StringBuilder模拟一个文本编辑器,它支持copy()paste()add()delete()等方法。

正常情况,我们像这样调用TextEditor

  1. TextEditor editor = new TextEditor();
  2. editor.add("Command pattern in text editor.\n");
  3. editor.copy();
  4. editor.paste();
  5. System.out.println(editor.getState());

这是直接调用方法,调用方需要了解TextEditor的所有接口信息。

如果改用命令模式,我们就要把调用方发送命令和执行方执行命令分开。怎么分?

解决方案是引入一个Command接口:

  1. public interface Command {
  2. void execute();
  3. }

调用方创建一个对应的Command,然后执行,并不关心内部是如何具体执行的。

为了支持CopyCommandPasteCommand这两个命令,我们从Command接口派生:

  1. public class CopyCommand implements Command {
  2. // 持有执行者对象:
  3. private TextEditor receiver;
  4. public CopyCommand(TextEditor receiver) {
  5. this.receiver = receiver;
  6. }
  7. public void execute() {
  8. receiver.copy();
  9. }
  10. }
  11. public class PasteCommand implements Command {
  12. private TextEditor receiver;
  13. public PasteCommand(TextEditor receiver) {
  14. this.receiver = receiver;
  15. }
  16. public void execute() {
  17. receiver.paste();
  18. }
  19. }

最后我们把CommandTextEditor组装一下,客户端这么写:

  1. TextEditor editor = new TextEditor();
  2. editor.add("Command pattern in text editor.\n");
  3. // 执行一个CopyCommand:
  4. Command copy = new CopyCommand(editor);
  5. copy.execute();
  6. editor.add("----\n");
  7. // 执行一个PasteCommand:
  8. Command paste = new PasteCommand(editor);
  9. paste.execute();
  10. System.out.println(editor.getState());

这就是命令模式的结构:

  1. ┌──────┐ ┌───────┐
  2. Client│─ ─>│Command
  3. └──────┘ └───────┘
  4. ┌──────────────┐
  5. ├─>│ CopyCommand
  6. ├──────────────┤
  7. editor.copy() │─
  8. └──────────────┘
  9. ┌────────────┐
  10. ┌──────────────┐ ─>│ TextEditor
  11. └─>│ PasteCommand └────────────┘
  12. ├──────────────┤
  13. editor.paste()│─
  14. └──────────────┘

有的童鞋会有疑问:搞了一大堆Command,多了好几个类,还不如直接这么写简单:

  1. TextEditor editor = new TextEditor();
  2. editor.add("Command pattern in text editor.\n");
  3. editor.copy();
  4. editor.paste();

实际上,使用命令模式,确实增加了系统的复杂度。如果需求很简单,那么直接调用显然更直观而且更简单。

那么我们还需要命令模式吗?

答案是视需求而定。如果TextEditor复杂到一定程度,并且需要支持Undo、Redo的功能时,就需要使用命令模式,因为我们可以给每个命令增加undo()

  1. public interface Command {
  2. void execute();
  3. void undo();
  4. }

然后把执行的一系列命令用List保存起来,就既能支持Undo,又能支持Redo。这个时候,我们又需要一个Invoker对象,负责执行命令并保存历史命令:

  1. ┌─────────────┐
  2. Client
  3. └─────────────┘
  4. ┌─────────────┐
  5. Invoker
  6. ├─────────────┤ ┌───────┐
  7. List commands│─ ─>│Command
  8. invoke(c) └───────┘
  9. undo() ┌──────────────┐
  10. └─────────────┘ ├─>│ CopyCommand
  11. ├──────────────┤
  12. editor.copy() │─
  13. └──────────────┘
  14. ┌────────────┐
  15. ┌──────────────┐ ─>│ TextEditor
  16. └─>│ PasteCommand └────────────┘
  17. ├──────────────┤
  18. editor.paste()│─
  19. └──────────────┘

可见,模式带来的设计复杂度的增加是随着需求而增加的,它减少的是系统各组件的耦合度。

练习

给命令模式新增Add和Delete命令并支持Undo、Redo操作。

命令 - 图1下载练习:命令模式练习 (推荐使用IDE练习插件快速下载)

小结

命令模式的设计思想是把命令的创建和执行分离,使得调用者无需关心具体的执行过程。

通过封装Command对象,命令模式可以保存已执行的命令,从而支持撤销、重做等操作。

读后有收获可以支付宝请作者喝咖啡,读后有疑问请加微信群讨论:

命令 - 图2命令 - 图3