设计模式拾荒之命令模式

  1. 参考书籍: 《Design Patterns: Elements of Reusable Object-Oriented Software》

设计模式用前须知

  • 设计模式种一句出现频率非常高的话是,“ 在不改动。。。。的情况下, 实现。。。。的扩展“ 。
  • 对于设计模式的学习者来说,充分思考这句话其实非常重要, 因为这句往往只对框架/ 工具包的设计才有真正的意义。因为框架和工具包存在的意义,就是为了让其他的程序员予以利用, 进行功能的扩展,而这种功能的扩展必须以不需要改动框架和工具包中代码为前提
  • 对于应用程序的编写者, 从理论上来说, 所有的应用层级代码至少都是处于可编辑范围内的, 如果不细加考量, 就盲目使用较为复杂的设计模式, 反而会得不偿失, 毕竟灵活性的获得, 也是有代价的。

命令模式(Command Pattern)

  • 设计意图
    • 把一个命令封装成一个对象,作为参数传递给其他对象,使得该对象可以改变其 可以发送不同的请求, 将请求排队, 将请求撤销。
    • 命令模式可以被看作是面向过程编程语言中的回调函数在面向对象语言中的实现方式。
  • GoF 举例

    • 命令模式最常见的例子就是我们平时编写用户界面所使用的工具包和类库, 以 Java 为例, 支持以如下的方式绑定 事件(Event) 与 行为/命令(Command)

          //让按钮具备关闭窗口的功能
          button.addActionListener(new ActionListener()
          {
              public void actionPerformed(ActionEvent e)
              {
                   System.out.println("按钮执行关闭窗口的功能");
                   System.exit(0);
              }
          });
    • 上面的代码呈现的是应用程序员使用图形界面类库的实现界面时的场景, 而从图形界面类库编写者角度来思考, 该如何编写代码才能支持如上的写法呢?

    • 在上述的例子中, 一个点击事件( ClickedEvent) 本质上是一个请求: 要求对用户点击行为进行相应的请求。 对于类库的编写者来说, 如何响应用户点击的请求, 是需要留给应用程序员去实现的, 所以对于类库的设计者来说,在这种场景下, 他们并不知道一个请求的接受者( 点击事件被绑定在哪些组件上), 也不知道响应某个请求要进行哪些操作。
  • 解决方案:

    • 命令模式使得类库中的对象可以把请求本身封装成一个对象, 从而向未具体指定的应用程序对象发送请求。 这个请求可以像普通的对象一样被存储或者传递。
      • Tips: 这里的描述可能会使大部分程序员困惑,因为平时编程时, 我们接触的请求似乎都是以对象的形式存在的, 例如 HttpRequest 中封装了各种 Http 请求的参数。 但这里的所说的请求其实来源于面向对象编程语言中一些学术化的术语, 在面向对象语言中, 对象之间的交互其实是通过方法调用完成的。 换句话说, 对象 A 如果想与对象 B 发生交互, 要求 B 对 A 的一个请求作出响应, 其具体方式是对象 A 首先得持有对象 B 的引用, 然后对象 A 调用 B.method(), 我们管这个过程可以看做 A 向 B 发送了一个特定的请求,得到了mthod () 执行内容的响应 ,可能有返回值, 也可能没有返回值。
    • 命令模式的核心是一个抽象类 Command , 它声明了一个用于执行具体指令的接口。 在最简单的情况下, 这个接口包含了一个抽象的 execute() 方法。 Command 的实现类会实现 execute 的具体内容。 此时, 当Command 实现类 ConcreteCommand 被实例化时, ConcreteCommand 就成为了某个请求的接受者, 具体如何响应请求, 是 execute() 方法内容决定的。
    • 下面通过图形界面类库的菜单模块实现过程来更加详细地展现这一过程。
    • 菜单中的每一选项就是一个 MenuItem 实例。 Application 类创建了这些菜单以及菜单项以及界面的其他部分。 Application 类还需要持有用户打开的文档类 Document 对象的引用 .
    • Application 为每个 MenuItem配置一个具体的 Command 实现类。 当用户选中一个菜单项之后, MenuItem 会调用他的 command 对象的 execute 方法。 execute 方法会执行相应的操作。
    • MenuItem 并不知道他们使用的是 Command 哪一个子类 。 Command 子类可以存储请求的 receiver , 然后通过 receiver 调用一个或更多的操作。

      • Tips: 这里的 receiver 容易让人困惑, 很多人会觉得 MenuItem 才是一个请求的 receiver。 这里需要回想上文提到过的, 面向对象编程语言中的术语问题。 以文章一开始所举的关闭窗口按钮的 Java 代码为例。 代码中只是输出了一行文字表示对应的功能得到了执行, 但如果实际编写起来, 必然需要获得一个代表窗口对象的实例, 如 window 对象, 在其中调用 window.close() 方法。 在这整个过程中, 窗口关闭按钮点击的请求最终的响应对象其实是 window 这个对象。 把例子中的 window 对象和 receiver 对应起来 , 就不难理解 receiver 的职责了。
        //让按钮具备关闭窗口的功能
           button.addActionListener(new ActionListener()
           {
               public void actionPerformed(ActionEvent e)
               {
                    window.close(); // window 是这个 button 点击请求最终的 receiver 
                    System.exit(0);
               }
           });
    • 回到上图所示的例子中, PaseteCommand 所接收到的点击请求的最终 receiver 是 Document 对象, Document 会通过 paste() 方法响应这个请求。

  • OpenCommand 的 execute 操作和之前的 PasteCommand 不同, 里面包含了一些列的操作
  • 当一个 Command 中的 execute 可能需要执行一系列的操作时, 我们可以定义上图的宏命令 MacroCommand 结构来予以灵活的支持。 MacroCommand 对象可以包含多个 Command 对象的引用, 支持任意数量指令的调用 。 注意到, MacroCommand 对象并没有明确的 receiver, 因为它所持有 commands 引用可以自行指定其各自的 receiver。

应用场景

命令模式(Command Pattern) 应当在你希望做到如下目标时使用:

  • 希望给对象参数化地传递一些行为。 对于熟悉回调函数的人来说, 就是希望在面向对象编程语言中想要使用回调函数时。
  • 在不同的时刻指定, 执行,排队请求(注意这里的请求是面向对象领域中的那个术语) 。 一个 Command 对象可以拥有和其对应请求完全独立的生命周期。 如果一个请求最终的 receiver 可以用不依赖本进程内存空间的方式表示, 你可以把一个命令转移到另外一个进程中去执行 ( RPC 的思想)
  • 支持撤销 。 Command 类的 execute 操作可以将执行的状态信息存储在command 对象中, 从而提供撤销操作所需要进行的逆操作信息。 为此, Command 抽象类需要添加一个 unexecute 方法来消除调用 execute 方法所产生的效果。 执行过的操作可以存储在一个历史列表 history list 中, 通过遍历这个list, 依次执行 execute 操作或者 unexecute 操作, 可以实现不受限制层级的撤销和重做。
  • 支持命令执行后导致的状态变化进行日志记录 。 这些日志可以被用于系统崩溃后的恢复。 通过为 Command 接口定义 load, store 操作, 可以持久化地记录系统变化。 系统崩溃恢复时, 可以把尚未执行的命令从持久化的日志中把所有的 commands 读取出来 ,重新予以执行
  • 把一个围绕高层级命令定义的系统以基础的命令结构化的组织起来。 这种结构在支持“事务”的信息系统中非常常见。 “事务”会把对于数据的多个改动封装起来, 集体通过或集体撤销。 命令模式 (Command Pattern) 提供了一个在应用层面, 模拟数据库“事务”的方式。 命令模式也使得系统很容易扩展实现新的事务。

结构图

  • 这里面最需要理解的就是 receiver 的角色, receiver 是一个请求最终的响应者。 客户端发起一个请求以后, 中间其实会经过多个对象的依次调用 Invoker-> Command( ConcreteCommand) . execute() -> receiver.action()
  • 为了更加清晰, 下面把命令模式里各个角色与之前的菜单例子中类的对应关系罗列出来
    • Command 类无须对应
    • ConreteCommand –> PasteCommand / OpenCommand
    • Client –> Application
    • Invoker –> MenuItem
    • Receiver–> Document, Application

交互方式

总结

命令模式看起来简单,但是理解起来并不容易, 对于其中请求的发送和接收, 都需要从面向对象领域里的对象交互层面来理解, 如果简单的对应到日常的编程经验中, 则很容易发生误解。

  • 命令模式解耦了将 操作的调用者 与 操作最终的执行者 解耦。
  • 命令模式中的 Command 可以被组装成一个复合 Command 。 一个例子就是上文提到过的 MacroCommand 。 更宽泛的说, 复合命令其实就是组合模式(Composite Pattern)的一个应用。

你可能感兴趣的:(设计模式)