前言
上一篇,我们详细讲解了职责链模式,其主要目的是分离职责,动态组合。所谓职责分离,是为了将复杂功能拆分成若干个小功能模块,从而规划定义各职责类,同时由于粒度较小,各职责类的复用性也就随之增大;而动态组合是在形成各职责类的前提下,根据需要动态地组合各职责对象,形成应对特定功能需求的职责链,进而在请求的发送者与请求的处理者之间形成松耦合状态,因为发送者无需知道请求最终将由哪个具体的处理者处理,当然也无需知道,唯一清楚地是当前请求一定会被“正确”处理,至于请求在职责链的传递工作完全将由职责链自身来保证。详细的介绍大家可以参考上一篇博文对职责链模式的介绍,今天,我们将继续讲述另一行为型模式——命令模式。
动机
在日常的软件系统中,行为的请求者与行为的实现者之间经常呈现了一种“紧耦合”的状态,也就是两者几乎合二为一,不分彼此。这样一种处理方式并非无法接受,毕竟它简单、明了,但是在某些情景下,需要对行为或者请求进行“记录、撤消\重做、事务”等操作时,这种对变化先天没有抵御能力的紧耦合状态是无法较好地满足这些额外要求的。面对这样一种需求,通过一种怎样的方式能将“行为的请求者”与“行为的实现者”之间进行解耦操作,使二者处于完全松耦合的状态?命令模式是时候粉墨登场呢!
意图
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤消的操作。
结构图
- 命令(Command)角色:定义命令的接口,声明执行的方法。
- 具体命令(ConcreteCommand)角色:命令接口实现对象,是“虚”的实现,通常它会持有命令的接收者,通过调用接收者相应的功能方法来执行当前命令所要完成的操作。
- 接收者(Receiver)角色:真正执行命令的对象。任何类都可以成为一个接收者,只要它能够实现命令要求实现的相应功能即可。
- 请求者(Invoker)角色:要求命令对象执行相关请求的对象,通常会持有命令对象,可以是多个命令对象。这是客户端真正触发命令并要求命令执行相应操作的入口点。
- 客户端(Client)角色:创建具体的命令对象,并设置命令对象的接收者。注意,这里的客户端并不是我们通常所指的客户端,而是指组装命令和接收者的地方,把这个Client称为装配者或者意义会更明了,真正使用命令的客户端是从Invoker来触发执行的,而不是从这个Client端命令的调用。
代码示例
1: public interface Command{
2: public void Execute();
3: }
4:
5: public class ConcreteCommand implements Command{
6: private Receiver receiver=null;
7: private String State;
8:
9: public ConcreteCommand(Receiver receiver){
10: this.receiver=receiver;
11: }
12:
13: public void Execute(){
14: //将请求转调给接收者对象的相应方法,让接收者来真正执行功能
15: receiver.Action();
16: }
17: }
18:
19: public class Receiver{
20: //示意性的方法
21: public void Action(){}
22: }
23:
24: public class Invoker{
25: private Command command=null;
26:
27: public void setCommand(Command command){
28: this.command=command;
29: }
30:
31: public void runCommand(){
32: //调用命令对象的执行方法
33: command.Execute();
34: }
35: }
36: public class Client{
37: public static void main(String[] args){
38:
39: //注意以下四行代码其实就是完成命令和接收的组装过程====
40: Receiver receiver=new Receiver();
41: Command command=new ConcreteCommand(receiver);
42:
43: Invoker invoker=new Invoker();
44: invoker.setCommand(command);
45: //组装过程结束=============================
46:
47: //下面是常规意义上的客户端调用请求者的地方,这里只是简单示意下
48: invoker.runCommand();
49: }
50: }
下面是命令模式的调用顺序时序图:
从上述示意图代码和时序图中,我们可以大致知道命令模式的通过实现手法和内部调用机制。首先创建命令接口,声明命令执行的方法,这是所有具体命令应该实现的接口;其次在各个具体命令对象里需要持有一个功能实现者,也就是接收者对象,将命令的执行委派给真正可以完成此功能的接收者来实现;然后,创建命令请求者,同理在其中持有一个或者多个具体的命令对象,当用户要求相关命令需要被执行时,请求者对象就是入口点,这点可以从时序图上清楚地看到。在实现的开发过程中,Client和Invoker可以融合古玩上,客户在使用命令模式的时候,先进行命令对象和接收者对象的组装,组装完成后,就可以调用命令执行请求呢。
综上所述,命令模式主要完成了对命令请求者(Invoker)和命令实现者(Receiver)的解耦操作。请求究竟由谁处理?如何处理?请求者是不知道的,它只管发出命令,剩下的事情就无需操心呢。而之所以请求者不需要操心得益于用于衔接请求者与接收者两者的命令对象的存在,即相关命令的实现者由对应的命令对象来维持,请求者只需要持有特定命令对象即可,也就是通过引入命令对象这一中间层,来解耦命令请求者与实现者的紧耦合关系。当然,增加了命令对象中间层,除了上述益处外,还可以用于实现命令的撤消\重做等相关操作,这点下面会有详细说明。
现实场景
对于命令模式的现实场景举例,不禁让我回想起《西游记》中“玉帝召唤美猴王上天”的情节:由于各种原因,玉帝老儿无奈地命令太白金星到美涟洞召唤美猴王上天接受封赐,这样太白金星就拿着圣旨前去召唤美猴王呢,至于后事我们就不关心呢:)在这样的一个神话情景中,抽象起来,玉帝就是系统的客户端Client,太白金星就是请求者Invoker,圣旨就是命令对象,而美猴王就是命令的真正实现者,也即是接收者Receiver呢。我们可以通过UML来形象地说明下上述情节,相信大家就会一目了然呢。
上图是不是将命令模式的精髓展现地一露无余呢?
从命令模式的意图一节我们可以知道,命令模式还可以用于实现请求的撤消\重做的要求。所谓的撤消,就是放弃该操作,回到未执行该操作前的状态,而重做自然就是再次执行当前操作呢,主要用于恢复的目的。下面,我们通过实现一个简单的计算器程序来说明命令模式是如何实现对各种运算的撤消\重做功能要求的。直接上代码吧!
1: public interface Command{
2: public void Execute();
3: public void UnExecute();
4: }
5:
6: public class AddCommand implements Command{
7: private Calculator calculator=null;
8: private long operand;
9:
10: public AddCommand(Calculator calculator,long operand){
11: this.calculator=calculator;
12: this.operand=operand;
13: }
14:
15: public void Execute(){
16: calculator.Calculate('+', operand);
17: }
18:
19: public void UnExecute(){
20: calculator.Calculate('-', operand);
21: }
22: }
23:
24: public class SubCommand implements Command{
25: private Calculator calculator=null;
26: private long operand;
27:
28: public SubCommand(Calculator calculator,long operand){
29: this.calculator=calculator;
30: this.operand=operand;
31: }
32:
33: public void Execute(){
34: calculator.Calculate('-', operand);
35: }
36:
37: public void UnExecute(){
38: calculator.Calculate('+', operand);
39: }
40: }
41:
42: public class MulCommand implements Command{
43: private Calculator calculator=null;
44: private long operand;
45:
46: public MulCommand(Calculator calculator,long operand){
47: this.calculator=calculator;
48: this.operand=operand;
49: }
50:
51: public void Execute(){
52: calculator.Calculate('*', operand);
53: }
54:
55: public void UnExecute(){
56: calculator.Calculate('/', operand);
57: }
58: }
59:
60: public class DevCommand implements Command{
61: private Calculator calculator=null;
62: private long operand;
63:
64: public DevCommand(Calculator calculator,long operand){
65: this.calculator=calculator;
66: this.operand=operand;
67: }
68:
69: public void Execute(){
70: if(operand!=0)
71: calculator.Calculate('/', operand);
72: }
73:
74: public void UnExecute(){
75: calculator.Calculate('*', operand);
76: }
77: }
78:
79: public class Calculator{
80: private long total=0;
81:
82: public void Calculate(char operator,long operand){
83: switch (operator) {
84: case '+':total+=operand;break;
85: case '-':total-=operand;break;
86: case '*':total*=operand;break;
87: case '/':total/=operand;break;
88: }
89: }
90: }
91:
92: public class Invoker{
93: private ArrayList<Command> undoCmds=new ArrayList<Command>();
94: private ArrayList<Command> redoCmds=new ArrayList<Command>();
95: private Calculator calculator=new Calculator();
96:
97: public void Compute(char operator,long operand){
98: Command command=null;
99: switch (operator) {
100: case '+':command=new AddCommand(calculator,operand);break;
101: case '-':command=new SubCommand(calculator,operand);break;
102: case '*':command=new MulCommand(calculator,operand);break;
103: case '/':command=new DevCommand(calculator,operand);break;
104: }
105: //将当前命令对象放入撤销命令集合,以便之后撤销调用
106: undoCmds.add(command);
107: command.Execute();
108: }
109:
110: public void Undo(int levels){
111: Command undoCommand=null;
112: int num=undoCmds.size();
113: for(int i=0;i<levels;i++){
114: if(num>=1){
115: undoCommand=undoCmds.remove(--num);
116: }
117: //将当前命令对象放入重做命令集合,以便之后重做调用
118: redoCmds.add(undoCommand);
119: undoCommand.UnExecute();
120: }
121: }
122:
123: public void Redo(int levels){
124: Command redoCommand=null;
125: int num=redoCmds.size();
126: for(int i=0;i<levels;i++){
127: if(num>=1){
128: redoCommand=redoCmds.remove(--num);
129: }
130: redoCommand.Execute();
131: undoCmds.add(redoCommand);
132: }
133: }
134: }
135: public class Client{
136: public static void main(String[] args){
137: Invoker invoker=new Invoker();
138:
139: invoker.Compute('+', 2);
140: invoker.Compute('*', 6);
141: invoker.Compute('-', 4);
142: invoker.Compute('/', 2);
143:
144: invoker.Undo(2);
145: invoker.Compute('+', 5);
146: invoker.Undo(1);
147: invoker.Redo(2);
148: }
149: }
从上述示例代码程序上看,我们创建了四种运算命令,即加减乘除。支持简单的撤消和重做操作,这里只是为了说明命令模式如何支持撤消\重做操作,大家不要纠结于程序的实现细节哈,有一点需要提醒的是,上述示例代码进行撤消\重做时,调用Compute方法之后紧接着只能调用Undo或者Compute方法哦,因为在调用Compute方法之后当前已经是最新状态,不应该存在重做的必要,想想word里的撤消\重做功能就清楚呢(有点啰嗦呢:))。
这方面还有很多例子,比如服务器的日志系统,如果系统支持撤消\重做,那么当出现故障时,我们可以通过撤消\重做功能将服务器从错误状态中恢复过来,尽可能降低灾难的风险和损失。做法和上述示例类似,就不再重复举例呢。
实现要点
- 命令对象功能范畴:命令对象能力有大有小,一个极端的情况是仅确定一个接收者和执行该请求的动作,另一个极端的情况是它自己实现所有的功能,根本无需接收者对象,这两者有好有坏,视情况而定。
- 支持撤消和重做:如果命令对象提供方法逆转它们操作的执行,那么应该支持撤消和重做功能,也就需要维持历史命令集合呢,像上文计算机器示例一样,同时保存消命令集和重做命令集,以在必要的时候能够获取到相应的命令对象。
- 避免撤消操作的错误积累:由于命令重复的执行、撤消和重复执行的过程可能会积累错误,以至一个应用的状态最终偏离初始值,这就有必要在命令对象中存入更多的信息以保证这些对象可被精确地复原成它们的初始状态,这里可用备忘录模式来让命令对象访问这些信息而不暴露其他对象的内部信息。(备忘录模式后面会有详细介绍,这里大家只需要记住即可)
运用效果
- 命令模式将调用操作的对象与知道如何实现该操作的对象解耦,也就是说将请求者与请求实现者解耦。
- 命令模式是头等的对象。它们可像其他的对象一样被操纵和扩展。
- 可将多个命令装配成一个复合命令,这点可以通过我们前面讲过的组合模式来完成。
- 添加新的命令对象比较容易,只需要实现命令接口即可,因为这不涉及改变已有的类。
- 如果将功能切分地太细或者应用场景并不适合使用命令模式时,可能会产生过多命令类,难以管理。
适用性
- 如果需要抽象出需要执行的动作,并参数化这些对象,可以选用命令模式。将这些动作抽象成命令,然后实现命令的参数化配置。
- 如果需要在不同的时刻指定、排列和执行请求,可以选用命令模式,将这些请求封装成命令,然后实现将请求队列化。
- 如果需要支持撤消\重做操作,可以选用命令模式,通过管理这些命令对象,很容易实现命令的恢复和重做功能。
- 在需要事务的系统中,可以选用命令模式。命令模式提供了对事务进行建模的方法,其实命令模式本身就有一个别名为Transaction。
相关模式
- 命令模式和组合模式:两者可以组合使用。通过组合模式将多个命令对象组装成复杂的命令对象,完成相对复杂的功能请求。
- 命令模式与备忘录模式:在实现撤销\重做功能时,可以通过备忘录模式来储存相关信息,保证信息的不泄露,同时保证每个命令对象只能访问到与自身相关的状态信息。
总结
命令模式的本质是:封装请求。命令模式的关键就是把请求封装成命令对象,然后就可以对这个对象进行一系列的处理呢,比如可撤消操作、日志请求和命令对象的参数化配置等功能处理。不过凡事都有两面性,不要随意使用命令模式,只有当符合前文提及的适用场景时,才应该考虑使用命令模式,否则也只能事倍功半而已,而且命令模式也极易产生数量众多的命令类对象,难以管理。好呢,对命令模式的讲解就到此为止吧,下篇我们将继续讲述另一种行为型模式——解释器模式,个人认为其在众多模式中为比较难理解的一种设计模式,让我们一起完成“攻坚”任务吧,敬请期待!
参考资料:
- 程杰著《大话设计模式》一书
- 陈臣等著《研磨设计模式》一书
- GOF著《设计模式》一书
- Terrylee .Net设计模式系列文章
- 吕震宇老师 设计模式系列文章