王大虎

王大虎: 15.3 命令模式的应用

 

王大虎

15.3.1 命令模式的优点

 

·类间解耦

调用者角色与接收者角色之间没有任何依赖关系,调用者实现功能时只须调用Command抽象类的execute方法就可以,不需要了解到底是哪个接收者执行。

·可扩展性

Command的子类可以非常容易地扩展,而调用者Invoker和高层次的模块Client不产生严重的代码耦合。

·命令模式结合其他模式会更优秀

命令模式可以结合责任链模式,实现命令族解析任务;结合模板方法模式,则可以减少Command子类的X问题。

 

 

15.3.2 命令模式的缺点

 

命令模式也是有缺点的,请看Command的子类:如果有N个命令,问题就出来了,Command的子类就可不是几个,而是N个,这个类X得非常大,这个就需要读者在项目中慎重考虑使用。

 

王大虎

15.3.3 命令模式的使用场景

 

只要你认为是命令的地方就可以采用命令模式,例如,在GUI开发中,一个按钮的点击是一个命令,可以采用命令模式;模拟DOS命令的时候,当然也要采用命令模式;触发-反馈机制的处理等。

 

 

15.4 命令模式的扩展

 

15.4.1 未讲完的故事

 

上面的例子我们还没有说完。想想看,客户要求增加一项需求,那是不是页面也增加,同时功能也要增加呢?如果不使用命令模式,客户就需要先找需求组,然后找美工组,再找代码组……你想让客户跳楼啊!使用命令模式后,客户只管发命令模式,例如,需要增加一项需求,没问题,我内部调动三个组通力合作,然后把结果反馈给你,这也正是客户需要的。那这个要怎么修改呢?想想看,很简单的!在AddRequirementCommand类的execute方法中增加对PageGroup和CodePage的调用就可以了,修改后的代码如代码清单15-19所示。

代码清单15-19 修改后的增加需求

 

王大虎

 

public class AddRequirementCommand extends Command { //执行增加一项需求的命令 public void execute() { //找到需求组 super.rg.find(); //增加一份需求 super.rg.add(); //页面也要增加 super.pg.add(); //功能也要增加 super.cg.add(); //给出计划 super.rg.plan(); } }

 

* * *

 

看看,是不是解决问题了?客户Client只需要发布命令,至于如何执行这个命令,是协调一个对象,还是两个对象,都不需要关心,命令模式做了一层非常好的封装。

 

 

15.4.2 反悔问题

 

我们的例子说到这里是不是应该真的结束了?不,还有一个问题会经常发生的:客户发出命令,要撤回,怎么办?就类似你使用Ctl+Z组合键(undo功能),发出一个命令,在没有执行(这时只要重新setCommand就可以了)或执行后撤回(执行后撤回是状态变更)该怎么实现呢?

有两种方法可以解决,一是结合备忘录模式还原最后状态,该方法适合接收者为状态的变更情况,而不适合事件处理;二是通过增加一个新的命令,实现事件的回滚。例子中的“删除一个页面”就需要一个反命令:撤销刚刚删除页面的命令,那客户发出这样一个命令,我们该怎么处理呢?

我们这样思考,反命令也是一个命令,那就是Command的一个子类,它实现的功能就是恢复刚刚删除的页面,然后我们再思考,谁能恢复删除的页面呢?当然是页面组了,于是作为接收者的页面组必须还有一个方法恢复最后删除的页面,也就是日志的回滚机制了,指定一个页面,回滚回去。分析完毕,我们来看实现,注意:以下为示意代码,请读者自行在应用中进行实现。修正后的Group如代码清单15-20所示。

代码清单15-20 修改后的Group类

 

王大虎

 

public abstract class Group { //甲乙双方分开办公,你要和那个组讨论,你首先要找到这个组 public abstract void find(); //被要求增加功能 public abstract void add(); //被要求删除功能 public abstract void delete(); //被要求修改功能 public abstract void change(); //被要求给出所有的变更计划 public abstract void plan(); //每个接收者都要对直接执行的任务可以回滚 public void rollBack(){ //根据日志进行回滚 } }

王大虎
王大虎

 

 

 

仅仅增加了一个rollBack的方法,每个接收者都可以对自己实现的任务进行回滚。怎么回滚?根据事务日志进行回滚!新增加的一个命令CancelDeletePageCommand实现撤销刚刚发出的删除命令,如代码清单15-21所示。

代码清单15-21 撤销命令

 

* * *

 

public class CancelDeletePageCommand extends Command { //撤销删除一个页面的命令 public void execute() { super.pg.rollBack(); } }

 

* * *

 

然后就是用Invoker进行调用了,客户选择了执行这个撤销动作,就可以进行撤销操作,该示意代码确实比较简单,真正实现起来那是异常复杂的,为什么呢?事务日志处理是非常繁琐的处理机制,想想数据库的日志处理吧,你就能想象出这个日志有多复杂!

 

王大虎

15.5 最佳实践

 

各位读者可能已经发觉了这样的问题:在我们旅行社的例子中,我们的Receiver角色(也就是Group的三个实现类)并没有暴露给Client,而在通用的类图和源码中却出现了Client类对Receiver角色的依赖,这是为什么呢?

如果你发现了这个问题,则说明你阅读得非常仔细,好习惯!每一个模式到实际应用的时候都有一些变形,命令模式的Receiver在实际应用中一般都会被封装掉(除非非常必要,例如撤销处理),那是因为在项目中:约定的优先级最高,每一个命令是对一个或多个Receiver的封装,我们可以在项目中通过有意义的类名或命令名处理命令角色和接收者角色的耦合关系(这就是约定),减少高层模块(Client类)对低层模块(Receiver角色类)的依赖关系,提高系统整体的稳定性。因此,建议大家在实际的项目开发时采用封闭Receiver的方式(当然了,仁者见仁,智者见智),减少Client对Reciver的依赖,该方案只是对Commandd抽象类及其子类有一定的修改,Command类如代码清单15-22所示。

代码清单15-22 完美的Command类

 

* * *

 

public abstract class Command { //定义一个子类的全局共享变量 protected final Receiver receiver; //实现类必须定义一个接收者 public Command(Receiver _receiver){ this.receiver = _receiver; } //每个命令类都必须有一个执行命令的方法 public abstract void execute(); }

 

* * *

 

在Command父类中声明了一个接收者,通过构造函数约定每个具体命令都必须指定接收者,当然根据开发场景要求也可以有多个接收者,那就需要用集合类型。我们来看具体命令,如代码清单15-23所示。

代码清单15-23 具体的命令

 

* * *

 

public class ConcreteCommand1 extends Command { //声明自己的默认接收者 public ConcreteCommand1(){ super(new ConcreteReciver1()); } //设置新的接收者 public ConcreteCommand1(Receiver _receiver){ super(_receiver); } //每个具体的命令都必须实现一个命令 public void execute() { //业务处理 super.receiver.doSomething(); } } public class ConcreteCommand2 extends Command { //声明自己的默认接收者 public ConcreteCommand2(){ super(new ConcreteReciver2()); } //设置新的接收者 public ConcreteCommand2(Receiver _receiver){ super(_receiver); } //每个具体的命令都必须实现一个命令 public void execute() { //业务处理 super.receiver.doSomething(); } }

 

* * *

 

这确实简化了很多,每个命令完成单一的职责,而不是根据接收者的不同完成不同的职责。在高层模块的调用时就不用考虑接收者是谁的问题,如代码清单15-24所示。

代码清单15-24 场景类

 

* * *

 

public class Client { public static void main(String[] args) { //首先声明调用者Invoker Invoker invoker = new Invoker(); //定义一个发送给接收者的命令 Command command = new ConcreteCommand1(); //把命令交给调用者去执行 invoker.setCommand(command); invoker.action(); } }

 

王大虎

 

高层次的模块不需要知道接收者,Perfect!读者可以在实际应用中采用该模式,看看威力如何。

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注