命令模式

Published on 2016 - 07 - 14

项目经理也难当

我是公司的项目经理,在国内做项目,项目经理需要什么都懂,什么都管。做好了,项目经理能分到一杯羹;做不好,都是项目经理的责任。这几乎是绝对的,我带过很多项目,行政命令一压下来,那就一条道:做完做好!

虽然我们公司是一个集团公司,但是我们部门的业绩是独立核算的,也就是说,我们部门不仅可以为集团公司服务,还可以为其他甲方服务,赚取更多的外快。2007年,我曾负责一个比较小的项目(但是项目的合同金额可不少)——为某家旅行社建立一套内部管理系统。该旅行社的门店比较多,员工也比较多,这个内部管理系统用来管理客户、旅游资源、票务以及内部事务,整体上类似于一个小型的MIS系统。客户的需求比较明确,因为他们曾经自己购买了一套内部管理系统,这次变动基本上是翻版;而且这家旅行社也有自己的IT部门,技术人员之间语言相通,比较好相处,也没有交流鸿沟。

该项目的成员分工采用了常规的分工方式,分为需求组(Requirement Group,RG)、美工组(Page Group,PG)、代码组(我们内部还有一个比较优雅的名字:逻辑实现组,这里使用大家经常称呼的名称,即Code Group,简称CG),加上我这个项目经理正好十个人。刚开始,客户(也就是旅行社,甲方)很乐意和我们每个组探讨,比如和需求组讨论需求、和美工讨论页面、和代码组讨论实现,告诉他们修改、删除、增加各种内容等。这是一种比较常见的甲乙方合作模式,甲方深入到乙方的项目开发中,我们可以使用类图来表示这个过程,如图1所示。

这个类图很简单,客户和三个组都有交流,这也合情合理。那我们看看这个过程的实现,首先看抽象类Group,如代码清单1所示。

public abstract class Group {
     //甲乙双方分开办公,如果你要和某个组讨论,你首先要找到这个组
     public abstract void find();
     //被要求增加功能
     public abstract void add();
     //被要求删除功能
     public abstract void delete();
     //被要求修改功能
     public abstract void change();
     //被要求给出所有的变更计划
     public abstract void plan();
}

大家看抽象类中的每个方法,其中的每个都是一个命令语气——“找到它,增加,删除,给我计划!”这些都是命令,给出命令然后由相关的人员去执行。我们再看3个实现类,其中的需求组最重要,需求组RequirmentGroup类如代码清单2所示。

public class RequirementGroup extends Group {
     //客户要求需求组过去和他们谈
     public void find() {
             System.out.println("找到需求组...");
     }
     //客户要求增加一项需求
     public void add() {
             System.out.println("客户要求增加一项需求...");
     }
     //客户要求修改一项需求
     public void change() {
             System.out.println("客户要求修改一项需求...");
     }
     //客户要求删除一项需求
     public void delete() {
             System.out.println("客户要求删除一项需求...");
     }
     //客户要求给出变更计划
     public void plan() {
             System.out.println("客户要求需求变更计划...");
     }
}

需求组有了,我们再看美工组。美工组也很重要,是项目的脸面,客户最终接触到的还是界面。美工组PageGroup类如代码清单3所示。

public class PageGroup extends Group {     
     //首先这个美工组应该能找到吧,要不你跟谁谈?
     public void find() {
             System.out.println("找到美工组...");
     }
     //美工被要求增加一个页面
     public void add() {
             System.out.println("客户要求增加一个页面...");
     }
     //客户要求对现有界面做修改
     public void change() {
             System.out.println("客户要求修改一个页面...");
     }
     //甲方是老大,要求删除一些页面
     public void delete() {
             System.out.println("客户要求删除一个页面...");
     }
     //所有的增、删、改都要给出计划
     public void plan() {
             System.out.println("客户要求页面变更计划...");
     }
}

最后看代码组。这个组的成员一般比较沉闷,不多说话,但多做事儿,这是这个组的典型特点。代码组CodeGroup类如代码清单4所示。

public class CodeGroup extends Group {
     //客户要求代码组过去和他们谈
     public void find() {
             System.out.println("找到代码组...");
     }
     //客户要求增加一项功能
     public void add() {
             System.out.println("客户要求增加一项功能...");
     }
     //客户要求修改一项功能
     public void change() {
             System.out.println("客户要求修改一项功能...");
     }
     //客户要求删除一项功能
     public void delete() {
             System.out.println("客户要求删除一项功能...");
     }
     //客户要求给出变更计划
     public void plan() {
             System.out.println("客户要求代码变更计划...");
     }
}

整个项目的3个支柱都已经产生了,那看客户怎么和我们谈。客户刚开始提交了他们自己写的一份比较完整的需求,需求组根据这份需求写了一份分析说明书,客户看后,要求增加需求,该场景如代码清单5所示。

public class Client {
     public static void main(String[] args) {
             //首先客户找到需求组说,过来谈需求,并修改
             System.out.println("-----------客户要求增加一项需求---------------");
             Group rg = new RequirementGroup();
             //找到需求组
             rg.find();
             //增加一个需求
             rg.add();
             //要求变更计划
             rg.plan();
     }
}

运行的结果如下所示:

-------------客户要求增加一项需求-----------------
找到需求组...
客户要求增加一项需求...
客户要求需求变更计划...

客户的需求暂时满足了,过了一段时间,客户又要求“界面多画了一个,过来谈谈”,于是又有一次场景变化,如代码清单6所示。

public class Client {
     public static void main(String[] args) {
             //首先客户找到美工组说,过来谈页面,并修改
             System.out.println("----------客户要求删除一个页面--------------");
             Group pg = new PageGroup();
             //找到需求组
             pg.find();
             //删除一项需求
             pg.delete();
             //要求变更计划
             pg.plan();
     }
}

运行结果如下所示:

-------------客户要求删除一个页面-----------------
找到美工组...
客户要求删除一个页面...
客户要求页面变更计划...

好了,界面也谈过了,应该没什么大问题了吧。过了一天后,客户又让代码组过去,说是数据库设计问题,然后又叫美工组过去,布置了一堆命令……这个就不一一写了,大家应该能够体会得到!问题来了,我们修改可以,但是每次都是叫一个组去,布置个任务,然后出计划,每次都这样,如果让你当甲方,你烦不烦?而且这种方式很容易出错误,并且还真发生过。客户把美工叫过去了,要删除,可美工说需求是这么写的,然后客户又命令需求组过去,一次次地折腾之后,客户也烦躁了,于是直接抓住我这个项目经理说:“我不管你们内部怎么安排,你就给我找个接头负责人,我告诉他怎么做,删除页面,增加功能,你们内部怎么处理我不管,我就告诉他我要干什么就成了……”

我一听,好啊,这也正是我想要的,我们项目组的兄弟们也已经受不了了,于是我改变了一下我的处理方式,如图2所示。

在原有的类图上增加了一个Invoker类,其作用是根据客户的命令安排不同的组员进行工作,例如,客户说“界面上删除一条记录”,Invoker类接收到该String类型命令后,通知美工组PageGroup开始delete,然后再找到代码组CodeGroup后台不要存到数据库中,最后反馈给客户一个执行计划。这是一个挺好的方案,但是客户的命令是一个String类型的,这有非常多的变化,仅仅通过一个字符串来传递命令并不是一个非常好的方案,因为在系统设计中,字符串没有约束力,根据字符串判断相关的业务逻辑不是一个优秀的解决方案。那怎么才是一个优秀的方案呢?解决方案是:对客户发出的命令进行封装,每个命令是一个对象,避免客户、负责人、组员之间的交流误差,封装后的结果就是客户只要说一个命令,我的项目组就立刻开始启动,不用思考、解析命令字符串,如图3所示。

Command抽象类只有一个方法execute,其作用就是执行命令,子类非常坚决地实现该命令,与军队中类似,上级军官给士兵发布命令:爬上这个旗杆!然后士兵回答:Yes,Sir!完美的项目也与此类似,客户发送一个删除页面的命令,接头负责人Invoker接收到命令后,立刻执行DeletePageCommand的execute方法。对类图中增加的几个类说明如下。

  • Command抽象类:客户发给我们的命令,定义三个工作组的成员变量,供子类使用;定义一个抽象方法execute,由子类来实现。
  • CInvoker实现类:项目接头负责人,setComand接收客户发给我们的命令,action方法是执行客户的命令(方法名写成是action,与command的execute区分开,避免混淆)。

其中,Command抽象类是整个扩展的核心,其源代码如代码清单7所示。

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类,如代码清单8所示。

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

页面变更也是比较频繁发生的,定义一个删除页面的命令 DeletePageCommand类,如代码清单9所示。

public class DeletePageCommand extends Command {
     //执行删除一个页面的命令
     public void execute() {
             //找到页面组
             super.pg.find();
             //删除一个页面
             super.rg.delete();
             //给出计划
             super.rg.plan();
     }
}

Command抽象类可以有N个子类,如增加一个功能命令(AddFunCommand),删除一份需求命令(DeleteRequirementCommand)等,这里就不再描述了,只要是由客户产生、时常性的行为都可以定义为一个命令,其实现类都比较简单,读者可以自行扩展。

客户发送的命令已经确定下来,我们再看负责人Invoker,如代码清单10所示。

public class Invoker {
     //什么命令
     private Command command;
     //客户发出命令
     public void setCommand(Command command){
             this.command = command;
     }
     //执行客户的命令
     public void action(){
             this.command.execute();
     }
}

这更简单了,负责人只要接到客户的命令,就立刻执行。我们模拟增加一项需求的过程,如代码清单11所示。

public class Client {
     public static void main(String[] args) {
             //定义我们的接头人
             Invoker xiaoSan = new Invoker();  //接头人就是小三
             //客户要求增加一项需求
             System.out.println("------------客户要求增加一项需求---------------");
             //客户给我们下命令来
             Command command = new AddRequirementCommand();
             //接头人接收到命令
             xiaoSan.setCommand(command);
             //接头人执行命令
             xiaoSan.action();
     }
}

运行结果如下所示:

-------------客户要求增加一项需求-----------------
找到需求组...
客户要求增加一项需求...
客户要求需求变更计划...

是不是我们的场景类简单了很多?客户只要给命令,我马上执行。简单!非常简单!那我们看看,如果客户要求删除一个页面,我们的修改有多大,如代码清单12所示。

public class Client {
     public static void main(String[] args) {
             //定义我们的接头人
             Invoker xiaoSan = new Invoker();  //接头人就是小三
             //客户要求增加一项需求
             System.out.println("------------客户要求删除一个页面---------------");
             //客户给我们下命令来
             //Command command = new AddRequirementCommand();
             Command command = new DeletePageCommand();
             //接头人接收到命令
             xiaoSan.setCommand(command);
            //接头人执行命令
             xiaoSan.action();
     }
}

运行结果如下所示:

-------------客户要求删除一个页面-----------------
找到美工组...
客户要求删除一项需求...
客户要求需求变更计划...

看到上面用粗体显示的代码了吗?只修改了这么多,是不是很简单,而且客户也不用知道到底由谁来修改,高内聚的要求体现出来了,这就是命令模式。

命令模式的定义

命令模式是一个高内聚的模式,其定义为:Encapsulate a request as an object,thereby letting you parameterize clients with different requests,queue or log requests,and support undoable operations.(将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。)

命令模式的通用类图如图4所示。

在该类图中,我们看到三个角色:

  • Receive接收者角色

该角色就是干活的角色,命令传递到这里是应该被执行的,具体到我们上面的例子中就是Group的三个实现类。

  • Command命令角色

需要执行的所有命令都在这里声明。

  • Invoker调用者角色

接收到命令,并执行命令。在例子中,我(项目经理)就是这个角色。

命令模式比较简单,但是在项目中非常频繁地使用,因为它的封装性非常好,把请求方(Invoker)和执行方(Receiver)分开了,扩展性也有很好的保障,通用代码比较简单。我们先阅读一下Receiver类,如代码清单13所示。

public abstract class Receiver {
     //抽象接收者,定义每个接收者都必须完成的业务
     public abstract void doSomething();
}

很奇怪,为什么Receiver是一个抽象类?那是因为接收者可以有多个,有多个就需要定义一个所有特性的抽象集合——抽象的接收者,其具体的接收者如代码清单14所示。

public class ConcreteReciver1 extends Receiver{    
     //每个接收者都必须处理一定的业务逻辑
     public void doSomething(){
     }
}
public class ConcreteReciver2 extends Receiver{ 
     //每个接收者都必须处理一定的业务逻辑
     public void doSomething(){
     }
}

接收者可以是N个,这要依赖业务的具体定义。命令角色是命令模式的核心,其抽象的命令类如代码清单15所示。

public abstract class Command {
     //每个命令类都必须有一个执行命令的方法
     public abstract void execute();
}

根据环境的需求,具体的命令类也可以有N个,其实现类如代码清单16所示。

public class ConcreteCommand1 extends Command {
     //对哪个Receiver类进行命令处理
     private Receiver receiver; 
     //构造函数传递接收者
     public ConcreteCommand1(Receiver _receiver){
             this.receiver = _receiver;
     }
     //必须实现一个命令
     public void execute() {
             //业务处理
             this.receiver.doSomething();
     }
}
public class ConcreteCommand2 extends Command {
     //哪个Receiver类进行命令处理
     private Receiver receiver;
     //构造函数传递接收者
     public ConcreteCommand2(Receiver _receiver){
             this.receiver = _receiver;
     }
     //必须实现一个命令
     public void execute() {
             //业务处理
             this.receiver.doSomething();
     }
}

定义了两个具体的命令类,读者可以在实际应用中扩展该命令类。在每个命令类中,通过构造函数定义了该命令是针对哪一个接收者发出的,定义一个命令接收的主体。调用者非常简单,仅实现命令的传递,如代码清单17所示。

public class Invoker {
     private Command command;
     //受气包,接受命令
     public void setCommand(Command _command){
             this.command = _command;
     }
     //执行命令
     public void action(){
             this.command.execute();
     }
}

调用者就像是一个受气包,不管什么命令,都要接收、执行!那我们来看高层模块如何调用命令模式,如代码清单18所示。

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

一个完整的命令模式就此完成,读者可以在此基础上进行扩展。

命令模式的应用

命令模式的优点

  • 类间解耦

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

  • 可扩展性

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

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

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

命令模式的缺点

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

命令模式的使用场景

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

最佳实践

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

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

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

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

这确实简化了很多,每个命令完成单一的职责,而不是根据接收者的不同完成不同的职责。在高层模块的调用时就不用考虑接收者是谁的问题,如代码清单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!读者可以在实际应用中采用该模式,看看威力如何。

参考文档