-
项目经理也难当
作者用他自己一次给某家旅行社建立内部管理系统的经理来引入这个设计模式,客户需求比较明确,而且也有自己的技术人员,交流比较容易,该项目的成员分为需求组(Requirement Group,RG)、美工组(Page Group,PG)、代码组(Code Group,CG),加上作者是项目经理。刚开始,客户(旅行社方)乐意和每个组探讨,比如和需求组讨论需求,和美工组讨论页面,和代码组讨论实现,告诉他们增删改查的内容。我们可以使用类图来表示这个过程,如图15-1:
客户和三个组都有交流,这也是符合目前情景的。那我们看实现过程,首先看抽象类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();
}
抽象方法中的每个方法,都是给出命令,然后由相关的人员去执行。我们再看三个实现类,其中需求组最重要,需求组RequirmentGroup类代码如下:
public class RequirementGroup extends Group {
//客户要求需求组过去和他们谈
@Override
public void find() {
System.out.println("找到需求组。。。");
}
@Override
public void add() {
System.out.println("客户要求增加一项需求。。。");
}
@Override
public void delete() {
System.out.println("客户要求删除一项需求。。。");
}
@Override
public void change() {
System.out.println("客户要求修改一项需求。。。");
}
@Override
public void plan() {
System.out.println("客户要求给出需求变更计划。。。");
}
}
需求组有了,我们在看美工组。美工组是项目的脸面,客户最终接触到的还是界面。美工组PageGroup类代码如下:
public class PageGroup extends Group {
@Override
public void find() {
System.out.println("找到美工组。。。");
}
@Override
public void add() {
System.out.println("客户要求增加一个页面。。。");
}
@Override
public void delete() {
System.out.println("客户要求增加一个页面。。。");
}
@Override
public void change() {
System.out.println("客户要求增加一个页面。。。");
}
@Override
public void plan() {
System.out.println("客户要求给出页面变更计划。。。");
}
}
最后看代码组,完成功能的具体实现且加班较多的苦逼组,代码如下:
public class CodeGroup extends Group {
@Override
public void find() {
System.out.println("找到代码组。。。");
}
@Override
public void add() {
System.out.println("客户要求增加一项功能。。。");
}
@Override
public void delete() {
System.out.println("客户要求删除一项功能。。。");
}
@Override
public void change() {
System.out.println("客户要求修改一项功能。。。");
}
@Override
public void plan() {
System.out.println("客户要求给出代码变更计划。。。");
}
}
整个项目组都已经产生了,那看客户怎么和三个小组交流。客户刚开始提交了他们自己写的一份比较完整的需求,需求根据这份需求写了一份分析说明书,客户看后,要求增加需求,代码如下:
public class Client {
public static void main(String[] args) {
System.out.println("---客户增加一项需求---");
Group rg = new RequirementGroup();
rg.find();
rg.add();
rg.plan();
}
}
(这里的Group为啥子用抽象类不用接口)
客户需求暂时满足了,过了一段时间又要求增加一个页面,又去找美工组,然后过段时间又找代码组过去说数据库设计有问题,要知道需求多次改变是常态,那每次都得找不同的小组去沟通,不只是客户烦,开发人员也想锤客户,而且还容易出错,美工组要加页面,代码组也得做调整,然后需求组还得跟进这个变化,然后都受不了了,改变一下处理方式,如图15-2:
在原有的类图上增加一个Invoker类,其作用是根据客户的命令安排不同的组员进行工作,例如,客户说"界面上删除一条记录",Invoker类接收到该String类型的命令后,通知美工组PageGroup开始delete,然后在找代码组CodeGroup后台不要存数据库,最后反馈给客户一个执行计划,因为在系统设计中,字符串没有约束力,根据字符串来传递一个命令并不是一个优秀的解决方案。那怎么处理呢?解决方案是:对客户发出的命令进行封装,每个命令是一个对象,避免客户、负责人、组员之间的交流误差,封装后的结果就是客户只要说一个命令,项目组就立刻开始启动,不用解析命令字符串,如图15-3:
Command抽象类只有一个方法execute,其作用就是执行命令,子类非常坚决地实现该命令 ,客户端发送一个删除页面的命令,接头人Invoker接收到命令后,立刻执行DeletePageCommand的execute方法。对类图中增加的几个类说明如下:
- Command抽象类:客户发给我们的命令,定义三个工作组的成员变量,供子类使用:定义一个抽象方法execute,由子类来实现。
-Invoker实现类:项目接头负责人,setCommand接收客户发给我们的命令,action方法是执行客户的命令(方法名写成是action,与command的execute区分开,避免混淆)。
其中,Command抽象类是整个扩展的核心,其代码如下:
public abstract class Command {
//把三组都定义好,子类可以直接使用
protected RequirementGroup rg = new RequirementGroup();
protected PageGroup pg = new PageGroup();
protected CodeGroup cg = new CodeGroup();
public abstract void execute();
}
抽象类很简单,具体的实现类只要实现execute方法就可以了。在一个项目中,需求增加是很常见的,那就把"增加需求"定义为一个命令AddRequirementCommand类,代码如下:
public class AddRequirementCommand extends Command {
@Override
public void execute() {
//找到需求组
super.rg.find();
//增加一份需求
super.rg.add();
//给出计划
super.rg.plan();
}
}
页面变更也比较频繁,定义一个删除页面的命令DeletePageCommand类,代码如下:
public class DeletePageCommand extends Command {
@Override
public void execute() {
//找到美工组
super.pg.find();
//删除一个页面
super.pg.delete();
//给出计划
super.pg.plan();
}
}
Command抽象类可以有N个子类,如增加一个功能命(AddFunCommad),删除一份需求命令(DeleteRequirementCommand)等,这里就不在描述了,只要是由客户产生、时常性的行为都可以定义为一个命令,其实现类都比较简单(这里会不会一个业务模块导致产生很多这个命令类,那也蛮麻烦的)
客户发送的命令已经确定下来,我们再看负责人Invoker,代码如下:
public class Invoker {
//什么命令
private Command command;
//客户发送命令
public void setCommand(Command command){
this.command=command;
}
//执行客户命令
public void action(){
this.command.execute();
}
}
这个就比开始那种模式简单多了,负责人只要接到客户的命令,就立刻执行,不用再让客户去一个个沟通了,我们模拟增加一项 需求的过程,代码如下:
public class Client {
public static void main(String[] args) {
//定义负责人
Invoker keji = new Invoker();
//客户要求增加一项需求
System.out.println("-----客户要求增加一项需求---");
//客户下的命令
Command command = new AddRequirementCommand();
keji.setCommand(command);
keji.action();
}
}
明显看这个场景类简单了很多,客户只要给命令,项目组马上执行。如果客户要求删除一个页面,我们只需要修改一下命令就好了。而且客户也不用知道具体是有那个小组完成的,只需要知道修改结果就好了,高内聚的要求体现出来了,这就是命令模式。
-
命令模式的定义
命令模式是一个高内聚的模式,其定义为:
Encapsulate a request as an object,thereby letting you parameterize clients with different requests,queue or log requests,and support undoable operations.(将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复工功能)
命令模式的通用类图如图15-4:
在该类图中,我们看到三个角色:
- Receive接收者角色
该角色就是干活的角色,命令传递到这里是应该被执行的,具体到我们上面的例子中就是Group的三个实现类。 - Command命令角色
需要执行的所有命令都在这里声明。 - Invoker调用者角色
接收到命令,并执行命令。在例子中就是负责人的角色。
命令模式比较简单,但是在项目中非常频繁的使用,因为他的封装性非常好,把请求方(Invoker)和执行方(Recevier)分开了,扩展性也有很好的保障,通用代码比较简单。
Recevier类的代码如下:
public abstract class Recevier {
//抽象接收者,定义每个接收者都必须完成的业务
public abstract void action();
}
这个Recevier是一个抽象类,那是因为接收者可以有多个,就像例子中的Group一样,其具体的接收者代码如下,实现各自的业务就好了:
public class ConcreteReciver1 extends Recevier {
@Override
public void action() {
}
}
public class ConcreteReciver2 extends Recevier {
@Override
public void action() {
}
}
接收者可以是N个,这要依赖业务的具体定义。命令角色是命令模式的核心,其抽象的命令类代码如下:
public abstract class Command {
//每个命令类都必须由一个执行命令的方法
public abstract void execute();
}
根据业务的需求,具体的命令类也可以有N个,其实现代码如下:
public class ConcreteCommand1 extends Command {
private Recevier recevier;
public ConcreteCommand1(Recevier recevier){
this.recevier = recevier;
}
@Override
public void execute() {
this.recevier.action();
}
}
这里命令类中,通过构造函数定义了该命令是针对哪一个接收者发出的,定义一个命令接收的主体。调用者非常简单,仅实现命令的传递,代码如下:
public class Invoker {
private Command command;
public void setCommand(Command command){
this.command = command;
}
public void action(){
this.command.execute();
}
}
调用者不管什么命令都要接收执行,那我们看看高层模块如何调用命令模式,代码如下:
public class Client {
public static void main(String[] args) {
Invoker invoker = new Invoker();
Recevier recevier = new ConcreteReciver1();
Command command = new ConcreteCommand1(recevier);
invoker.setCommand(command);
invoker.action();
}
}
一个完整的命令模式就此完成,我们可以在此基础上进行扩展。
-
命令模式的应用
3.1 命令模式的优点
- 类间解耦
调用者与接收者角色之间没有任何依赖,调用者实现时只需要调用Command抽象类的execute方法就可以,不需要了解到底是哪个接收者执行。 - 可扩展性
Command的子类可以非常容易的扩展,而调用者Invoker和高层次的模块Client不产生严重的代码耦合(可以说Invoker基本上不用修改,扩展命令只需要增加Command的实现类和接收者就好了)。 - 命令模式结合其他模式会更优秀
命令模式可以结合责任链模式,实现命令族解析任务;结合模板方法模式,则可以减少Command子类膨胀的问题。(命令模式是怎么结合这两个设计模式的还需要看看实际的例子,我百度好像没看到很明显的文章写这两个例子的,可能Command使用模板方法模式比较容易理解,因为command的子类都基本实现execute的方法来让接收者完成相关的业务逻辑)
-
命令模式的扩展
4.1 未讲完的故事
上面的例子我们继续看看,客户要增加一项需求,那是不是页面也增加,同时功能也要增加,如果没有使用命令模式就需要找各个小组去沟通,使用命令模式后,只需要发布命令就好了,三个小组怎么合作客户不关心,他只关心结果是否完成,那我们在命令中把需要调用的小组都加上就好了。
客户只需要发布命令,至于执行过程就不管了,命令模式做了很好的封装。
4.2反悔问题
客户对发布的命令要撤回,发出一个命令,在没有执行的时候好说,如果执行了命令,那么需要撤回怎么办?一种是结合备忘录模式还原最后状态,该方法适合接收者为状态的变更情况,不适合事件处理;二是通过增加一个新的命令,实现事件回滚;这个回滚就比较麻烦了,还得通过操作日志等去还原,这个就不在这里看了,书中只是在接收者中加了一个rollback方法来表示执行回滚命令,这个和其他命令都差不多,只不过具体实现比较麻烦,个人认为算不上什么命令模式的扩展。
-
最佳实践
我们上面的旅行社例子中的Recevier角色并没有暴露给Clinet中,但是在通用类图和源码中确出现了Client对Recevier的依赖;在每一个模式到实际应用的时候都会有一些调整,命令模式的Reveciver在实际应用中一般都会被封装掉,那是因为在项目中:约定的优先级最高,每一个命令是对一个或多个Reveciver的封装,我们可以在项目中通过有意义的类名或命令名处理命令和接收者角色的耦合关系(这就是约定)(有时候约定可以减少很多不必要的麻烦,有些地方通过约定,而不去使用技术来限制,通过技术限制一些规则肯定会产生一些与业务不太相关的不太必要的代码,而且还不一定能限制的住,就拿那个单例模式举例,创建对象方式很多,并不是将构造函数私有化就好了,但是通过约定就会知道不去创建那个对象就好了,没有开发人员会和自己过不去),减少高层模块对低层模块的依赖,提高系统整体的稳定性。这个调整在命令构造函数中指定对应接收者就好了,一个无参构造默认接收者,一个有参构造指定需要的接收者。(很多框架使用起来很简单,因为你可以根据自己的需要去定制自己的规则,也可以不需要设置直接使用默认的规则)
内容来自《设计模式之禅》