前面的话
谈到命令,大部分的人脑海中会想到下面这幅画面
这在现实生活中是一副讽刺漫画,做决定的人不清楚执行决定的人有何特点,瞎指挥、外行领导内行说的就是这种,不过在软件设计领域,我们显然要为这种现象正名了,让狮王能记住所有属下的特点并直接打电话通知任务,显然是为难他了,领导们很忙,这是秘书处的工作,狮王只要做出指示“最近鼠患猖獗,该抓抓了”,那秘书们就要起草红头文件(命令)并发给相关执行部门(猫),各司其职提高效率,发布请求的(秘书处)和执行请求的(猫)分离开来,将行为(抓鼠)封装成对象(红头文件),这就是命令模式。
官方定义
将一个请求封装成对象,从而可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及执行可撤销的操作——GOF23。
将请求封装成对象,这个对象就是命令对象,在结构化程序中,请求通常是以函数的形式表现的,对于该请求中可能涉及到的执行对象,如果我们以函参的形式传递,这会造成以下几个问题
1)紧耦合,客户程序需要依赖执行对象,在上例中,上层在发布命令时需要依赖具体的属下,这会违反依赖倒置原则;把请求封装成命令对象,这些命令对象遵循共同的命令接口,这就解决了高层依赖问题。
2)函数没用强调撤销(undo)操作,函数中对对象状态的保存需要额外的业务逻辑。
3)函数的复用性及扩展型较差,这也是为什么结构化逐渐被对象语言取代的原因。
不同的请求可以对客户进行参数化,这个类似于策略模式的动态设置算法,在上例中,不同的请求被封装成了不同的命令,用户是可以自由选择当前要执行的命令是哪个。“面向接口而不是实现”的编程原则,可以在运行时根据上下文情况来选择具体命令,比如,狮王并不总和老鼠过不去,这段时间机关单位工作作风较差,迟到现象频发,狮王就会指示“多打鸣,抓四风”,这时秘书处就会起草打鸣文件并保证其能够下发执行。秘书处的职责,其实就像是触发器invoker(遥控器),他们起草何种文件并执行,就相当于触发器绑定了何种命令对象,这种绑定关系是由Client(狮王)决定的。
请求排队、记录日志、和可撤销,这三点是命令模式的典型应用,在后文会提及,这个可以从类似文本编辑软件word中做比较,这些软件的用户界面设计广泛借鉴了命令模式的特点,封装请求成命令对象后,可以把一系列的命令排队、记录、撤销等。
角色
在该模式中包含以下几个角色:
Command —— 命令接口,上例中,对应于秘书处的红头文件的模板,其中定义了具体命令所需实现的方法,如execute、undo等。
ConcreateCommand —— 具体命令,上例中,对应于抓鼠文件、打鸣文件。这些命令中通常会包含接受者的引用,比如抓鼠红头文件会对应于一个猫的引用实例,execute方法会调用猫的捕鼠方法,打鸣同理。
Client —— 创建具体命令并设置接受者,这是命令的实际制定者,对应于狮王,每个具体命令对象在创建时,其接受者就已经被定义好了,在创建具体命令时需要关心谁来执行。注意这里的Client并不是通常所说的使用用户,Client的作用类似于装载器Loader,创建不同的命令对象并将其动态绑定在invoker中。
Invoker —— 要求命令执行的对象,对应于上例的秘书处,他们的任务是确保上通下达,对于每个具体的命令都要保证被执行。
Receiver —— 接受者,命令的具体执行者,对应于上例的阿鸡阿猫,这些执行者通常会被组合在具体命令中,他们有自己的方法,这些方法通常会在具体命令的execute方法中被调用。
代码实现
实现的UML图如下所示
以上图为例,Command是命令接口,秘书类Secretary组合该接口对象,捕鼠和打鸣实现了该命令接口。由于逻辑比较简单,直接贴代码,首先是个简单到不好意思的Command接口。
<span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">public interface Command { public abstract void execute(); } </span></span></span>
具体命令包括两个,CatchMouseCommand和CrowCommand
<span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">public class CatchMouseCommand implements Command{ Cat cat; public CatchMouseCommand(Cat cat) { // TODO Auto-generated constructor stub this.cat = cat; } @Override public void execute() { // TODO Auto-generated method stub cat.CatchMouse(); } }</span></span></span>
And
<span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">public class CrowCommand implements Command{ Cock cock; public CrowCommand(Cock cock) { // TODO Auto-generated constructor stub this.cock = cock; } @Override public void execute() { // TODO Auto-generated method stub cock.Crow(); } }</span></span></span>
可以看到这两个具体命令类都组合了接受者类,Cat或Cock,简单起见,这两个接受这类只含有一个对应方法,如下
Cat
<span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">//猫类-receiver public class Cat { //捕鼠方法 public void CatchMouse() { System.out.println("Cat is catching mouse"); } }</span></span></span>
Cock
<span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">//公鸡类-receiver public class Cock { //打鸣方法 public void Crow() { System.out.println("Cock is crowing"); } }</span></span></span>
秘书类里组合了命令对象和两个方法,setCommand是由于设置具体命令,而publicCommand则类似于结构化语言中的回调,当需要执行该命令时这个方法就会调用具体命令的execute函数,秘书类作为请求发起者,并不关心具体命令由谁执行,如何执行,这就实现了请求者和实现者的解耦。invoker不关心receiver,反之亦然。
<span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">public class Secretary { Command command; public void setCommand(Command command) { this.command = command; } public void publicCommand() { this.command.execute(); } }</span></span></span>
Lion在本例中是命令的事实上的制定者,他创建了具体命令并在合适时机交由秘书完成命令发布和执行(receiver完成),软件开发实践中这个类更像是一个装载者,他建立一种映射关系,特定的invoker绑定特定的ConcreteCommand,后文会以word编辑器软件开发进行分析。
<span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">/** * Lion不是实际的使用客户,其作用类似于装载器Loader,其作用是 * 为触发器Invoker(秘书对象secretary)绑定不同的命令对象(catchMouseCommand)。 * 这就像在类似word软件的菜单中,不同的菜单项MenuItem就是不同Invoker,每个菜单项 * 都会绑定一个命令对象,甚至多个菜单项可能绑定相同的命令对象。 * 用户点击时菜单项会触发其绑定的命令对象方法。 */ public class Lion { Secretary secretary; public Lion(Secretary secretary) { // TODO Auto-generated constructor stub this.secretary = secretary; } public void createCatchMouseOrder() { Cat cat = new Cat(); CatchMouseCommand catchMouseCommand = new CatchMouseCommand(cat); secretary.setCommand(catchMouseCommand); } public void createCrowCommand() { Cock cock = new Cock(); CrowCommand crowCommand = new CrowCommand(cock); secretary.setCommand(crowCommand); } }</span></span></span>
完成了上述的基本类后,我们需要添加用户程序进行测试,如下
<span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">public class UserApplication { /** 这是通常意义上的用户程序,也就是使用命令模式的上下文环境。 * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub Secretary secretary = new Secretary(); //lion的作用类似Loader,为secretary(invoker)绑定命令对象。 Lion lion = new Lion(secretary); lion.createCatchMouseOrder(); secretary.publicCommand(); lion.createCrowCommand(); secretary.publicCommand(); } }</span></span></span>
从测试例中可以看到,lion为secretary动态绑定了不同的command,进而完成请求参数化,在本例中当secretary接受到具体的command就会publiccommand(发布执行),实际在很多场合,这个发布是由用户事件驱动的,比如某个MenuItem在初始化时绑定了对应功能,只有在用户点击该MenuItem时,才会通过该菜单项的回调方法调用publiccommand,进而完成实际的执行。
该测试例执行结果如下:
Cat is catching mouse
Cock is crowing
扩展场景
上述代码示例是命令模式的一个使用示例,仅仅解答了官方定义中的前半部分,即“请求参数化”,软件实践中命令模式应用较为广泛的场景是文本编辑器或者是IDE用户界面的开发,以此为例解释下官方定义中所说的请求排队、记录日志以及可撤销操作。
“请求排队”
如果你在一个配置一般的机子上频繁操作eclipse,通常你会看到下面这样的界面
后面一大推Waiting的任务,就是正在排队的请求,这个现象的原因是下面这种图:
我们要求IDE执行的请求被封装成命令对象放置在工作队列中,每一个空闲线程会得到并执行该命令对象,但资源有限,调度器需要限制能够使用的线程数量,当有新的线程空闲时,排队等待的命令对象才会被顺序执行,这就是命令模式在请求排队中的应用方式。
“记录请求日志”
这个主要是应用于大型数据库的管理操作中,对于本文所举的例子实际意义不大,在大型数据库的维护中,所有的操作改动都是以命令对象的方式进行的,有些改动必须是以事务的方式进行,因为这些改动彼此都是紧密联系的,对于经年累月的频繁改动,无法做到每次改动的数据库内容都做一次备份,那样需要太多资源,于是,把每次的改动命令对象以序列化方式保存(store)在磁盘上,每两个checkpoint点之间的改动,都保存起来。这样在系统发生崩溃时,通过反序列化的方式从磁盘上装载(load)这些命令对象,进而完成这些命令的undo操作,这样就完成了数据库系统恢复。
完成上述任务不仅需要命令对象支持序列化操作,而且对于Command接口也有了新的要求,如下
“可撤销undo”Ctrl+Z
还是举前文的例子,撤销操作要求对象回到命令执行前的状态,这就需要在Command接口中添加undo方法。
<span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">public interface Command { public abstract void execute(); public abstract void undo(); //新增undo接口 }</span></span>
公鸡类Cock内部需要一个表示打鸣频率的实例,假设公鸡打鸣频率有高、中、低三种,通常情况下打鸣频率为低,可能每周打鸣2次,但是命令下达后,打鸣频率明显升高,达到每周7次,这就是新的Cock类
<span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">//公鸡类-receiver public class Cock { public static final int LOW = 0; public static final int MEDIUM = 1; public static final int HIGH = 2; private int CrowFrequence; // 新添一个记录打鸣频率的状态实例,默认为LOW //打鸣方法 public void Crow() { System.out.println("Cock is crowing"); setCrowFrequence(HIGH); // 调用打鸣方法后,频率为HIGH } //Getter和 Setter函数,获取和设置当前打鸣频率 public int getCrowFrequence() { return CrowFrequence; } public void setCrowFrequence(int crowFrequence) { CrowFrequence = crowFrequence; } @Override public String toString() { // TODO Auto-generated method stub return "CrowFrequence is " + CrowFrequence; } }</span></span>
在具体命令中,需要添加一个状态来记录命令执行前的打鸣频率,在风头过后执行undo操作时,这个被记录的频率值就被恢复了。
<span style="font-family:KaiTi_GB2312;font-size:18px;">public class CrowCommand implements Command{ Cock cock; int PrevStage; //记录在命令执行前的打鸣频率状态 public CrowCommand(Cock cock) { // TODO Auto-generated constructor stub this.cock = cock; } @Override public void execute() { // TODO Auto-generated method stub PrevStage = cock.getCrowFrequence(); //执行命令前记录。 System.out.println("before execute : " + cock); cock.Crow(); System.out.println("after execute : " + cock); } @Override public void undo() { // TODO Auto-generated method stub cock.setCrowFrequence(PrevStage); //执行undo操作,设置保持的prevStage System.out.println("after undo : " + cock); } }</span>
改写测试例添加执行undo操作,得到的结果如下
这种undo情况比较简单,仅仅是保存了一个实例域,而且仅仅可以undo上一步,很多时候需要建立操作的历史记录,这样就需要保存一个执行command的链表,每一个链表中都含有可以撤销的命令及其具体实现,这个在类似word和PS之类的软件中应用的非常广泛,不做赘述。
“宏命令”
这是一种特殊的命令类,在该类中定义一连串的命令list,执行和撤销时,将该list中所有的命令都执行一遍,可以自定义添加或者删除list中的具体命令,熟练使用word的同学都用过宏命令,比如毕业论文模板对于字体、段落、页眉页脚等所有格式的要求可以被简单定义成一个宏模板,只要应用这个宏模板就可以快速自动的将当前文档设置成论文要求格式。
结构
该结构和上文uml图类似,可自行对比。官方定义
效果及注意问题点
1)命令模式将请求调用者和实际执行这解耦,符合依赖倒置原则。
2)具体的command对象可以像其它对象一样进行扩展。这是一个把函数抽象成类的特殊对象,较普通函数优势前文已述。
3)该结构符合开发封闭原则,添加新的类不需要改动原代码结构。
4)具体命令对象的智能程度如何,这个命令对象是否一定依赖于receiver完成操作,依赖程度是否会变化。本文的具体对象是完全依赖于接受者的方法。
5)命令是否有必要进行undo操作,undo操作需要保持怎么的状态值,如果状态是较为复杂的对象,需要引入更多实例进行表示,在撤销过程中是否会引起接受者内部的其它变化。
与其他模式的关系
大型文本编辑软件的菜单树是分成复杂的,除了前文提到的宏命令以外,与组合模式相结合可以实现具有多层分支结构的菜单树命令。
当用来记录接受者的复杂状态时,可以使用备忘录模式,利用该模式可以完成撤销操作后的状态恢复。
收尾
命令模式是在软件实践中应用较为广泛的一种模式,这个模式的应用场景较为特别,尤其是对于菜单树和数据库相关的功能模块中,这种模式的优点明显,它分离了任务的请求者和执行者,把行为封装成对象,从而可以完成类似请求参数化、状态保存/恢复、撤销、重做、宏命令等功能,该模式最典型的应用多在软件用户菜单树开发、事件驱动、数据库日志维护及恢复等将行为作为命令对象的场合,甚至当你为程序添加一个ActionBar.TabListener监听器对象时,框架层也用到了命令模式。欢迎分享交流,共同进步~
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
注:欢迎分享,转载请声明~~
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
例说命令模式(Command Pattern)