Java设计模式(18)-命令模式
(图片来源于网络) 试想一个去餐厅吃饭的场景,假设餐厅还没有智能化的系统,流程基本上是这样:客人坐下后,服务员会拿出菜单让客人点菜,每点一个菜品就在小本上记录下来,等客人点餐完毕后,服务员将所记录的客人已点菜品清单交给厨房,由厨师来负责烹饪。等待客人用菜完成后,服务员可以拿出记录的点菜清单,然后交给餐厅前台收银员收款结账。这整个过程中,有这么几个角色:客人、服务员、厨师、收银员,现在我们用代码来描述这个场景,怎么来设计呢? 1. 传统的点菜系统设计 传统的方式来设计,我们可能会很容易想到如下的类图关系: Figure 1. 传统方式设计的点菜系统类图 客人直接依赖服务员,使用其点菜服务来点菜,而服务员直接依赖厨师类,通知其完成做菜功能。客人用餐完成后,可以去前台结账。服务员所记录的点菜清单很好的为厨师和收银人员提供了依据:厨师可以借助点菜清单知道自己需要做什么菜,收银人员可以借助点菜清单来计算收款数目。 整个设计初看没有问题,各个功能都实现并且运行的很好。但是,如果某些客人点完菜过后发现,自己点的菜品太多了,可能吃不完而造成粮食的浪费。我们需要系统支持客人取消点菜的功能,一旦客人发现菜品过剩,就可以申请取消,以发扬勤俭节约的美德。 好吧,我们只需要在服务员类上添加"取消点菜"的功能,当服务员收到取消点菜的请求时,他可以在点菜清单上将取消的菜品划掉,然后再交给厨师一个新的点菜清单。此时的类图看起来像这样: Figure 2. 添加了取消点菜功能的类图设计 虽然功能实现了,但是客人、服务员和厨师三者的关系是紧耦合的,随着客人需求的更改,服务员和厨师都需要更改。如果厨师有多个人,每个人负责不同的菜品类别,如炒菜厨师只负责炒菜,而蒸菜、汤品、面食都由不同的厨师负责。那么,客人所点的菜品可能会由多个厨师来完成制作,此时服务员可能会手忙脚乱,因为服务员与厨师紧耦合,客人新增点菜、取消点菜等等请求都需要服务员去与厨师交涉,一旦弄错了对应关系就会造成严重的后果。 其实,服务员与厨师的关系可能是通知和被通知的关系,如果能将这种关系抽象出来,那么服务员和厨师的关系不就解耦了吗?可以将服务员通知厨师看成是给厨师下达一个命令,这个命令有多种支持的操作,如:执行命令、撤销命令、重做命令等等,执行命令时,可以记录日志,以便查阅,就如同服务员手上的"点菜清单",随时都能查看客户点了什么菜、取消了什么菜。 服务员发出命令,我们称之他为请求者,而厨师我们称之为接收者,这种将命令请求者和命令接收者之间添加一层命令抽象的设计模式就是本文要讨论的命令模式。 2. 什么是命令模式 命令模式:将一个请求封装为一个对象,从而可以用不同的请求对客户进行参数化,并且可以对请求排队或记录日志,还支持可以撤销的操作。 Figure 3. 命令模式类图 从图上可以看出,命令模式有几个角色: 请求者(Invoker): 请求的发起者,内部聚合了很多命令对象,可以调用这些命令对象执行相关请求,但是不直接依赖接收者(Receiver); 抽象命令(Abstract Command): 定义了命令通用方法的接口或者抽象类(通常包含execute方法表示执行命令),如执行、撤销、重做等; 具体命令(Concrete Command): 实现或者继承抽象命令,但是自己不直接实现业务逻辑,而是内部聚合多个Receiver,调用其相关方法来完成命令的具体业务实现; 接收者(Receiver): 真正实现了命令的相关业务功能,被具体命令对象依赖; 命令模式常见的场景 遥控器,可以控制家用电器的开关、模式切换、频道切换等,可以使用命令模式来设计,遥控器作为请求者(Invoker),电器作为接收者(Receiver); 前文所述的到餐厅点菜,就可以使用命令模式设计,服务员作为请求者(Invoker),而厨师作为命令接收者(Receiver),抽象命令定义点菜、取消点菜等方法; GUI软件系统界面功能的设计,界面包含多个功能按钮,点击后分别实现不同的功能,此时可以使用命令模式,按钮作为请求者(Invoker),系统后端实现按钮点击后真正功能的类作为接收者(Receiver),而抽象命令可以定义按钮的执行方法,如下图所示: Figure 4. GUI对象将命令委派给命令对象 命令模式的优点 完全符合单一职责原则,每一个具体命令(Concrete Command)负责单独的命令逻辑; 完全符合开闭原则,可以在不修改已有客户端代码的情况下在程序中扩展新的命令; 通过引入中间层(命令层)降低了系统的耦合度,便于命令的扩展; 可以实现命令的撤销和恢复功能;可以实现命令的排队和延迟执行; 可以实现将多个命令组合成复杂的命令。 命令模式的缺点 增加了命令层,系统复杂度增加,带来系统理解上的困难; 具体命令类的数量增加导致系统的类数量增加,系统的开发和维护成本增高。 3. 命令模式代码 接下来看看命名模式的基本代码实现: 抽象命名接口(Abstract Command): interface Command { void execute(); } ...