常用设计模式之命令模式解析

今天是设计模式学习系列的第6篇,命令模式

带着问题出发

  1. 什么是命令模式?
  2. 命令模式的好处是什么?
  3. 命令模式的使用场景?

命令模式解析

简单的来说,命令模式就是 将 请求的调用者请求的接收执行者 分隔开来。

举一个生活中常见的例子,比如你去餐馆点餐,你选好菜品下单,这时候服务员过来,拿着菜单啥都不用管,直接把菜单给厨房就好了,厨房做好菜,服务员返回给顾客。

在这个例子中,服务员就是请求的调用者,她不用管具体的订单是什么,只知道拿到订单就给厨师(调用执行方法),而厨师就是请求的接收执行者,顾客就作为命令创建者,将命令设置到调用者中(服务员),那么稍后顾客就可以要求调用者去执行命令,即将订单交给厨房。

通过上面的例子,你应该对命令模式内的类和对象是如何互动的有一定的了解了,下面就给出命令模式的细节概念:

命令模式 将 “请求” 封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。

这一模式的关键是一个抽象的 Command 类,它定义了一个执行操作的接口。其最简单的形式是一个抽象的 execute 操作。 具体的 Command 子类将接收者作为其一个实例变量,并实现 execute 操作,指定接收者采取的动作,而接收者有执行该请求所需的具体信息。由于这个对象只暴露一个 execute 方法,当此方法被调用,接收者就会进行这些动作。从外面来看,其他对象不知道是哪个接收者进行了哪些操作,只知道调用 execute 方法,请求的目的就能达到。

常用设计模式之命令模式解析_第1张图片

  • Client:
    这个客户负责创建一个 ConcreteCommand 并且设置其接收者。

  • Receiver:
    接收者知道如何进行必要的工作,实现这个请求。任何类都可以当接收者。

  • ConcreteCommand:
    这是一个 “虚” 实现,即绑定了动作和接收者的对应关系,当调用者调用 execute 就可以发出请求,然后由 ConcreteCommand 调用接收者的一个或多个动作。

  • Invoker:
    这个调用者持有一个命令对象,并在某个时间点调用命令对象的 execute 方法,将请求付诸实践。

  • Command:
    定义命令对象接口,生命执行的方法,调用者就只要持有这个类型的对象即可,不用管具体的实现类。

Brain Power

学习了命令模式,然后我们再来看文章开头提出的第二个问题,命令模式的好处是什么?

你应该有答案了,就是 支持请求调用者和请求接收者之间解耦。

可撤销操作

可撤销操作的意思就是:放弃该操作,回到未执行该操作前的状态。即每个命令都要支持撤销操作,回滚到执行之前的状态。

有两种基本的思路来实现可撤销的操作:

  • 一种是补偿式,又称反操作式
    比如被撤销的操作是加的功能, 那撤销的实现就变成减的功能;同理被撤销的操作是打开的功能,那么撤销的实现就变成关闭的功能。对应到我们命令模式中就是,在 Command 中加入一个 undo 方法,不管 execute刚才做什么, undo都会倒转过来。
  • 另外一种方式是存储恢复式
    意思就是把操作前的状态记录下来,然后要撤销操作的时候就直接恢复回去就可以了。

还有一种场景就是撤销按钮可以一直按,比如按一次回退到上一个状态,按两次就是上上一个状态,这个又该怎么实现呢?

其实非常简单,在调用者中不要只记录最后一个被执行的命令,而是使用一个堆栈结构,每执行一个命令就放入堆栈,然后每按一下撤销按钮,就从堆栈中取出最上面的命令,执行 undo 方法。

命令模式的使用场景

这个部分,主要列举下命令模式的更多用途,现实中我们见到的餐厅点餐、以及遥控器的按钮设置等都是命令模式可以大展拳脚的地方。

命令模式的关键之处就是把请求封装成为对象,也就是命令对象(包含一个接收者和一组动作),然后将它传来传去,就像是一般的对象一样。现在,即使在命令对象被创建许久之后,运算依然可以被调用。事实上,它甚至可以在不同的线程中被调用。我们可以利用这样的特性衍生一些应用,例如:线程池、工作队列、日志请求等。

队列请求

想象有一个工作队列:你在某一端添加命令,然后另一端则是线程。线程进行下面的动作:从队列中取出一个命令,调用它的execute()方法,等待这个调用完成,然后将此命令对象丢弃,再取出下一个命令…
请注意,工作队列类和进行计算的命令对象之间是完全解耦的。此刻线程可能在进行财务运算,下一刻却在读取网络数据。工作队列对象不在乎到底做些什么,它们只知道取出命令对象,然后调用其execute()方法。类似地,它们只要是实现命令模式的对象,就可以放入队列里,当线程可用时,就调用此对象的execute()方法。

日志请求

某些应用需要我们将所有的动作都记录在日志中,并能在系统死机后,重新调用这些动作恢复到之前的状态。一般通过增加两个方法(store()、load()),命令模式能够支持这一点。

要怎么做呢?比如数据库写入更新操作,每次数据变更时,由于要组织索引结构比较耗时,因此不会每次执行动作发生改变时都去快速的存储。通过使用记录日志,我们可以将上次检查点(checkpoint) 之后的所有操作顺序写入日志,如果系统出现状况,我们就可以从磁盘日志中重新加载这些命令对象,然后成批去调用这些对象的 execute 方法来恢复这些操作。

总结

  1. 命令模式将发出请求的对象和执行请求的对象解耦;
  2. 再被解耦的两者之间是通过命令对象进行沟通的。命令对象封装了接收者和一个或一组动作;
  3. 调用者通过命令对象的 execute() 发出请求,这会使得接收者的动作被调用从而达成要求;
  4. 调用者可以接受命令对象当做参数,并且可以在运行时动态的进行;
  5. 命令支持撤销,做法就是通过实现一个 undo() 方法来回到执行 execute( 方法前的状态;
  6. 宏命令是命令的一种延伸,即包括一组命令,允许一次性调用。宏方法也可以支持撤销;
  7. 实际操作中,很有可能使用“聪明” 命令对象,即直接实现了请求,不需要封装接收者进行委托;
  8. 命令也可以用来实现日志和事务系统;

命令模式如果你还没有理解,可以查阅示例教学代码,关注公众号,回复 “设计模式”即可获取。

参考资料

  1. 《Head First 设计模式》
  2. 《设计模式:可复用面向对象软件的基础》

你可能感兴趣的:(设计模式,设计模式,命令模式,java)