第六章 命令模式

在本章,我们将把封装带到一个全新的境界:把方法调用(method invocation)封装起来。
没错,通过封装方法调用,我们可以把运算块包装成形。所以调用此运算的对象不需要关心事情是如何进行的,只要知道如何使用包装成形的方法来完成它就可以。
通过封装方法调用,也可以做一些很聪明的事情,例如记录日志,或者重复使用这些封装来实现撤销(undo)

举例说明

最近鸡哥气象站向我展示并简单介绍了新扩张的气象站。我必须说,我对于该软件架构的印象非常深刻,所以想邀请你为我们设计一个家电自动化遥控器的API。

附上一个创新控制器的原型以供你研究。这个遥控器具有七个可编程的插槽(每个都可以指定到一个不同的家电装置),每个插槽都有对应的开关按钮。这个遥控器还具备一个整体的撤销按钮。

我也在资料里面附带一个案例类,这些类是由多家厂商开发出来的,用来控制家电自动化装置,例如电灯、风扇、热水器、音响设备和其他类似的可控制装置。

希望你能够创建一组控制遥控器的API,让每个插槽都能够控制一个或一组装置。请注意,能够控制目前的装置和任何未来可能出现的装置,这一点是很重要的。

让硬件解脱!让我们看看这个遥控器……

从家电自动化公司取得的厂商类

看一下厂商的类
看看以下厂商类,可以使你对即将设计的对象接口有一些想法。

看起来类好像不少,但接口各有差异。麻烦还不只是这样,这些类以后还会越来越多。所以设计一个遥控器API变得很有挑战性。让我们继续设计吧!

讨论

看了上面类以后我们可能会有以下问题

A:有新的设计任务来了。根据我初次观察的结果,目前有一个附着开和关按钮的简单遥控器,还有一套五花八门的厂商类。
B:是的,有许多的类都具备on()和off()方法,除此之外,还有一些方法像是dim()、setTemperature()、setVolumn()、setDirection().

A:还不只这样,听起来似乎将来还会有更多的厂商类,而且每个类还会有各式各样的方法。
B:我认为要把它看成分离的关注点,这很重要:遥控器应该知道如何解读按钮被按下的动作,然后发出正确的请求,但是遥控器不需知道这些家电自动化的细节,比如 如何打开热水器。

A:听起来好像是个不错的设计方式。但如果遥控器很笨,只知道如何做出一般的要求,那又怎能设计出让这个遥控器能够调用一些诸如打开电灯或车库门的动作呢?
B:我不确定该怎么做,但是我们不必让遥控器知道太多厂商类的细节

A:你的意思是……
B:我们不想让遥控器包含一大堆if语句,例如“if slot1==Lightthen light.on(), else if slot1 == Hottub then hottob. jetsOn()”。大家都知道这样的设计很糟糕。

A:我同意你的说法。只要有新的厂商类进来,就必须修改代码,这会造成潜在的错误,而且工作没完没了。
C:我不小心听到了你们的对话。从第1章开始,我就努力地学习设计模式。有一个模式就叫做“命令模式”,可能对你们有帮助。

A:是吗?再多说一些来听听。
C:命令模式可将“动作的请求者”从“动作的执行者”对象中解耦。在你们的例子中,请求者可以是遥控器,而执行者对象就是厂商类其中之一的实例。

B:这怎么可能?怎么能将它们解耦?毕竟,当我按下按钮时,遥控器必须把电灯打开
C:在你的设计中采用“命令对象”就可以办到。利用命令对象,把请求(例如打开电灯)封装成一个特定对象(例如客厅电灯对象)。所以,如果对每个按钮都存储一个命令对象,那么当按钮被按下的时候,就可以请命令对象做相关的工作,遥控器并不需要知道工作内容是什么,
只要有个命令对象能和正确的对象沟通,把事情做好就可以了。所以,看吧,遥控器和电灯对象解耦了。

A:的确听起来像是一个正确的方向。
B:我仍然无法理解这个模式怎么工作。
C:由于对象之间是如此的解耦,要描述这个模式实际的工作并不容易。

A:听听我的想法是否正确:使用这个模式,我们能够创建一个API,将这些的命令对象加载到按钮插槽,让遥控器的代码尽量保持简单。而把家电自动化的工作和进行该工作的对象一起封装在命令对象中。
C:是的,我也这么认为。我也认为这个模式可以同时帮你设计“撤销按钮”我还没研究到这部分。

命令模式 餐厅

回到命令模式的简单介绍
如同C所说的,仅仅通过听别人口述的方式来了解命令模式、确实有点困难。但是别害怕,有一些朋友正准备帮助我们;还记得第1章里出现的友好餐厅吗?餐厅可以帮助我们了解命令模式。

所以,让我们再度回到餐厅,研究顾客、女服务员、订单,以及快餐厨师之间的交互。通过这样的互动,你将体会到命令模式所涉及的对象,也会知道它们之间如何被解耦。之后,我们就可以解决遥控器API了。

进人对象村餐厅…
我们都知道餐厅是怎么工作的:

让我们更详细地研究这个交互过程……
….既然餐厅是在对象村,所以让我们也来思考对象和方法的调用关系

餐厅的角色和职责

一张订单封装了准备餐点的请求

把订单想象成一个用来请求准备餐点的对象,和一般的对象一样,订单对象可以被传递:从女服务员传递到订单柜台或者从女服务员传递到接替下一班的女服务员。订单的接口只包含一个方法,也就是orderUp()。
这个方法封装了准备餐点所需的动作。订单内有一个到“需要进行准备工作的对象”(也就是厨师)的引用。这一切都被封装起来,所以女服务员不需要知道订单上有什么,也不需要知道是谁来准备餐点;
她只需要将订单放到订单窗口,然后喊一声“订单来了”就可以了。

女招待的工作是接受订单,然后调用订单的orderUp()方法。
女服务员的工作很简单:接下顾客的订单,继续帮助下一个顾客,然后将一定数量的订单放到订单柜台,并调用orderUp()方法,让人来准备餐点。如同在对象村讨论过的,女服务员其实不必担心订单的内容是什么,或者由谁来准备餐点。她只需要知道,订单有一个orderUp()方法可以调用,这就够了。

快餐厨师具备准备餐点的知识。
快餐厨师是一种对象,他真正知道如何准备餐点。一旦女招待调用orderUpO方法,快餐厨师就接手,实现需要创建餐点的所有方法。请注意,女招待和厨师之间是彻底的解耦:女招待的订单封装了餐点的细节,她只要调用每个订单的方法即可,而厨师看了订单就知道该做些什么餐点;
厨师和女招待之间从来不需要直接沟通。

餐厅是命令模式的模型

把餐厅想成是OO设计模式的一种模型,而这个模型允许将“发出请的对象”和“接受与执行这些请求的对象”分隔开来。比方说,对遥控器API,我们需要分隔开“发出请求的按钮代码”和“执行请求的厂商特定对象”。万一遥控器的每个插槽都持有一个像餐厅订单那样的对象,会怎么样?那么,当一个按钮被按下,只要调用该对象的orderUp()方法,电灯就开了,而遥控器不需要知道事情是怎么发生的,也不需要知道涉及哪些对象。
现在我们就把餐厅的对话换成命令模式……

好了,我们已经花了很多时间在对象村餐厅,也清楚地知道各种角色的特性和他们的职责。现在我们要重新绘制餐厅图以反映出命令模式。所有的角色依然不变,只有名字改变了。

第一个命令对象

是我们建立第一个命令对象的时候了!现在开始写一些遥控器的代码。虽然我加还没搞清楚如何设计遥控器的APi,但自下而上建造一些东西,可能会有所帮助

实现命令接口
首先,让所有的命令对象实现相同的包含一个方法的接口。在餐厅的例子中!我们称此方法为orderUp(),然而,现在改为一般惯用的名称execute()。这就是命令接口:

1
2
3
4
5
6
7
8
public interface Command 
{
/// <summary>
/// 简单!只需要一个方法:execute()
/// </summary>
public void Execute();
}

实现一个打开电灯的命令

现在,假设想实现一个打开电灯的命令。根据厂商所提供的类,Light类有两个方法:on()和off()。下面是如何将它实现成一个命令:

  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Light 
{
public void On() { Debug.Log("开灯"); }

public void Off() { Debug.Log("关灯"); }
}
public class LightOnCommand : Command
{
public Light light;

/// <summary>
/// 构造器被传入了某个电灯(比方说:客厅的电灯),以便让这个命令控制,然后记录在实例变量中。一旦调用execute()就由这个电灯对象成为接收者负责接受请求。
/// </summary>
/// <param name="light"></param>
public LightOnCommand(Light light)
{
this.light = light;
}

/// <summary>
/// 这个execute()方法调用接收对象(我们正在控制的电灯)的On方法
/// </summary>
public void Execute()
{
light.On();
}
}

现在有了LightOnCommad类,让我们看看如何使用它……

使用命令对象

好了,让我们把这一切简化:假设我们有一个遥控器,它只有一个按钮和对应的插槽,可以控制一个装置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SimpleRemoteControl
{
//有一个插槽持有命令,而这个命令控制着一个装置。
Command slot;

public SimpleRemoteControl() { }

/// <summary>
/// 这个方法用来设置插槽控制的命令。如果这段代码的客户想要改变遥控器按钮的行为,可以多调用这个方法。
/// </summary>
/// <param name="command"></param>
public void SetCommand(Command command)
{
slot = command;
}

/// <summary>
/// 当按下按钮时,这个方法就会被调用使得当前命令衔接插槽,并调用它的execute()方法。
/// </summary>
public void ButtonWasPressed()
{
slot.Execute();
}
}

遥控器使用的简单测试
下面只有一点点代码,用来测试上面的简单遥控器。我们来看看这个代码,并指出它和命令模式图的对应关系:

好了,现在来实现GarageDoorOpenCommand类,类图如下

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class GarageDoor
{
public void Up() { Debug.Log("打开车库门"); }

public void Down() { }

public void Stop() { }

public void LightOn() { }

public void LightOff() { }
}


public class GarageDoorOpenCommand
{
private GarageDoor garageDoor;

public GarageDoorOpenCommand(GarageDoor garageDoor)
{
this.garageDoor = garageDoor;
}

public void Execute()
{
garageDoor.Up();
}
}

现在你已经有了一个类,下面代码的输出会是什么? 继续往下看