引言
提起Command模式,我想没有什么比遥控器的例子更能说明问题了,本文将通过它来一步步实现GOF的Command模式。
我们先看下这个遥控器程序的需求:假如我们需要为家里的电器设计一个远程遥控器,通过这个控制器,我们可以控制电器(诸如灯、风扇、空调等)的开
关。我们的控制器上有一系列的按钮,分别对应家中的某个电器,当我们在遥控器上按下“On”时,电器打开;当我们按下“Off”时,电器关闭。
好了,让我们开始Command 模式之旅吧。
HardCoding的实现方式
控制器的实现
一般来说,考虑问题通常有两种方式:从最复杂的情况考虑,也就是尽可能的深谋远虑,设计之初就考虑到程序的可维护性、扩展性;还有就是从最简单的情
况考虑,不考虑扩展,客户要求什么,我们就做个什么,至于以后有什么新的需求,等以后再说。当然这两种方式各有优劣,本文我们从最简单的情况开始考虑。
我们假设控制器只能控制
三个电器,分别是:灯、电扇、门(你就当是电子门好了^^)。那么我们的控制器应该有三组,共六个按钮,每一组按钮分别有“On”,“Off”按钮。同
时,我们规定,第一组按钮对应灯,第二组按钮对应电扇,第三组则对应门,那么控制器应该就像这样:
类的设计
好了,控制器大致是这么个样子了,那么 灯、电扇、门又是什么样子呢?如果你看过前面几节的模式,你可能会以为此时又要为它们创建一个基类或者接口,然后供它们继承或实现。现在让我们先看看我们想要控制的电器是什么样子的:
很抱歉,你遗憾地发现,它们的接口完全不同
,我们没有办法对它们进行抽象,但是因为我们此刻仅考虑客户最原始的需求(最简单的情况),那么我们大可以直接将它们复合到 遥控器(ControlPanel) 中
NOTE:
关于接口,有狭义的含义:就是一个声明为interface的类型。还有一个广义的含义:就是对象暴露给外界的方法、属性,所以一个抽象类也可以称作一个接口。这里,说它们的接口不同,意思是说:这三个电器暴露给外界的方法完全不同。
注意到,PressOn方法,它代表着某一个按键被按下,并接受一个int类型的参数:SlotNo,它代表是第几个键被按下。显然,SlotNo的取值为0到2。对于PressOff则是完全相同的设计。
代码实现
namespace
Command {
// 定义灯
public
class
Light
{
public
void
TurnOn(){
Console
.WriteLine("The light is
turned on."
);
}
public
void
TurnOff() {
Console
.WriteLine("The light is
turned off."
);
}
}
// 定义风扇
public
class
Fan
{
public
void
Start() {
Console
.WriteLine("The fan is
starting."
);
}
public
void
Stop() {
Console
.WriteLine("The fan is
stopping."
);
}
}
// 定义门
public
class
Door
{
public
void
Open() {
Console
.WriteLine("The door is
open for
you."
);
}
public
void
Shut() {
Console
.WriteLine("The door is
closed for
safety"
);
}
}
// 定义遥控器
public
class
ControlPanel
{
private
Light
light;
private
Fan
fan;
private
Door
door;
public
ControlPanel(Light light, Fan fan, Door door) {
this
.light = light;
this
.fan = fan;
this
.door = door;
}
// 点击On按钮时的操作。slotNo,第几个按钮被按
public
void
PressOn(int
slotNo){
switch
(slotNo) {
case
0:
light.TurnOn();
break
;
case
1:
fan.Start();
break
;
case
2:
door.Open();
break
;
}
}
// 点击Off按钮时的操作。
public
void
PressOff(int
slotNo) {
switch
(slotNo) {
case
0:
light.TurnOff();
break
;
case
1:
fan.Stop();
break
;
case
2:
door.Shut();
break
;
}
}
}
class
Program
{
static
void
Main(string
[] args) {
Light
light = new
Light
();
Fan
fan = new
Fan
();
Door
door = new
Door
();
ControlPanel
panel = new
ControlPanel
(light, fan, door);
panel.PressOn(0); // 按第一个On按钮,灯被打开了
panel.PressOn(2); // 按第二个On按钮,门被打开了
panel.PressOff(2); // 按第二个Off按钮,门被关闭了
}
}
}
输出为:
The light is
turned on.
The door is
open for
you.
The door is
closed for
safety
存在问题
这个解决方案虽然能解决当前的问题,但是几乎没有任何扩展性可言。或者说,被调用者(Receiver:灯、电扇、门)与它们的调用者(Invoker:遥控器)是紧耦合的。遥控器不仅需要确切地知道它能控制哪些电器,并且需要知道这些电器由哪些方法可供调用。
- 如果我们需要调换一下按钮所控制的电器的次序,比如说我们需要让按钮1不再控制灯,而是控制门,那么我们需要修改 PressOn 和 PressOff 方法中的Switch语句。
- 如果我们需要给遥控器多添一个按钮,以使它多控制一个电器,那么遥控器的字段、构造函数、PressOn、PressOff方法都要修改。
- 如果我们不给遥控器多添按钮,但是要求它可以控制10个或者电器,换言之,就是我们可以动态分配某个按钮控制哪个电器,这样的设计看上去简直无法完成。
HardCoding 的另一实现
新设计方案
在考虑新的方案以前,我们先回顾前面的设计,第三个问题似乎暗示着我们的遥控器不够好,思考一下,我们发现可以这样设计遥控器:
对比一下,我们看到可以通过左侧可以上下活动的阀门来控制当前遥控器控制的是哪个电器(按照图中当前显示,控制的是灯),在选定了阀门后,我们可以
再通过On,Off按钮来对电器进行控制。此时,我们需要多添一个方法,通过它来控制阀门(进而选择想要控制的电器)。我们管这个方法叫做
SetDevice()。那么我们的设计变成下图所示:
NOTE:
在图中,以及现实世界中,阀门所能控制的电器数总是有限的,但在程序中,可以是无限的,就看你有多少个诸如light的电器类了
注意到几点变化:
- 因为我们假设遥控器可以控制的电器是无限多的,所以这里不能指定具体电器类型,因为在C#中所有类型均继承自Object,我们将SetDevice()方法接受的参数设置成为Object。
- ControlPanel不知道它将控制哪个类,所以图中ControlPanel和Light、Door、Fan没有联系。
- PressOn()和PressOff()方法不再需要参数,因为很明显,只有一组On和Off按钮。
代码实现
namespace
Command {
public
class
Light
{ // 略 }
public
class
Fan
{ // 略 }
public
class
Door
{ // 略 }
// 定义遥控器
public
class
ControlPanel
{
private
Object
device;
// 点击On按钮时的操作。
public
void
PressOn() {
Light
light = device as
Light;
if
(light != null
) light.TurnOn();
Fan
fan = device as
Fan;
if
(fan != null
) fan.Start();
Door
door = device as
Door;
if
(door != null
) door.Open();
}
// 点击Of按钮时的操作。
public
void
PressOff() {
Light
light = device as
Light;
if
(light != null
) light.TurnOff();
Fan
fan = device as
Fan;
if
(fan != null
) fan.Stop();
Door
door = device as
Door;
if
(door != null
) door.Shut();
}
// 设置阀门控制哪个电器
public
void
SetDevice(Object device) {
this
.device = device;
}
}
class
Program
{
static
void
Main(string
[] args) {
Light
light = new
Light
();
Fan
fan = new
Fan
();
ControlPanel
panel = new
ControlPanel
();
panel.SetDevice(light); // 设置阀门控制灯
panel.PressOn(); // 打开灯
panel.PressOff(); // 关闭灯
panel.SetDevice(fan); // 设置阀门控制电扇
panel.PressOn(); // 打开门
}
}
}
存在问题
我们首先可以看到,这个方案似乎解决了第一种设计的大多数问题,除了一点点瑕疵:
- 尽管我们可以控制任意多的设备,但是我们每添加一个可以控制的设备,仍需要修改PressOn()和PressOff()方法。
- 在PressOn()和PressOff()方法中,需要对所有可能控制的电器进行类型转换,无疑效率低下。
封装调用
问题分析
我们的处境似乎一筹莫展,想不到更好的办法来解决。这时候,让我们先回头再观察一下ControlPanel的PressOn()和PressOff()代码。
// 点击On按钮时的操作。
public
void
PressOn() {
Light
light = device as
Light;
if
(light != null
) light.TurnOn();
Fan
fan = device as
Fan;
if
(fan != null
) fan.Start();
Door
door = device as
Door;
if
(door != null
) door.Open();
}
我们发现PressOn()和PressOff()方法在每次添加新设备时需要作修改,而实际上改变的是对对象方法的调用,因为不管有多少个if语句,只会调用其中某个不为null的对象的一个方法。
然后我们再回顾一下OO的思想,Encapsulate what varies(封装变化)。我们想是不是应该有办法将这变化的这部分(方法的调用)封装起来呢?
在考虑如何封装之前,我们假设已经有一个类,把它封装起来了,我们管这个类叫做Command,那么这个类该如何使用呢?
我们先考虑一下它的构成,因为它要封装各个对象的方法,所以,它应该暴露出一个方法,这个方法既可以代表 light.TurnOn(),也可以代表fan.Start(),还可以代表door.Open(),让我们给这个方法起个名字,叫做Execute()。
好了,现在我们有了Command类,还有了一个万金油的Execute()方法,现在,我们修改PressOn()方法,让它通过这个Command类来控制电器(调用各个类的方法)。
// 点击On按钮时的操作。
public
void
PressOn() {
command.Execute();
}
哇,是不是有点简单的过分了!?但就是这么简单,可我们还是发现了两个问题:
- Command应该能知道它调用的是哪个电器类的哪个方法,这暗示我们Command类应该保存对于具体电器类的一个引用。
- 我们的ControlPanel应该有两个Command,一个Command对应于所有开启的操作(我们管它叫onCommand),一个Command对应所有关闭的操作(我们管它叫offCommand)。
同时,我们的SetDevice(object)方法,也应该改成SetCommand(onCommand,offCommand)。好了,现在让我们看看新版ControlPanel 的全景图吧。
Command类型的实现
显然,我们应该能看出:onCommand实体变量(instance
variable)和offCommand变量属于Command类型,同时,上面我们已经讨论过Command类应该具有一个Execute()方法,
除此以外,它还需要可以保存对各个对象的引用,通过Execute()方法可以调用其引用的对象的方法。
那么我们按照这个思路,来看下开灯这一操作(调用light对象的TurnOn()方法)的Command对象应该是什么样的:
public
class
LightOnCommand
{
Light
light;
public
Command(Light light){
this
.light = light;
}
public
void
Execute(){
light.TurnOn();
}
}
再看下开电扇(调用fan对象的Start()方法)的Command对象应该是什么样的:
public
class
FanStartCommand
{
Fan
fan;
public
Command(Fan fan){
this
.fan = fan;
}
public
void
Execute(){
fan.Start();
}
}
这样显然是不行的,它没有解决任何的问题,因为FanStartCommand和LightOnCommand是不同的类型,而我们的
ControlPanel要求对于所有打开的操作应该只接受一个类型的Command的。但是经过我们上面的讨论,我们已经知道所有的Command都有
一个Execute()方法,我们何不定义一个接口来解决这个问题呢?
OK,现在我们已经完成了全部的设计,让我们先看一下最终的UML图,再进行代码实现吧(简单起见,只加入了灯和电扇)。
我们先看下这张图说明了什么,以及发生的顺序:
- ConsoleApplication,也就是我们的应用程序,它创建电器Fan、Light对象,以及LightOnCommand和FanStartCommand。
- LightOnCommand、FanStartCommand实现了ICommand接口,它保存着对于Fan和Light的引用,并通过Execute()调用Fan和Light的方法。
- ControlPanel复合了Command对象,通过调用Command的Execute()方法,间接调用了Light的TurnOn()方法或者是Fan的Stop()方法。
它们之间的时序图是这样的:
可以看出:通过引入Command对象,ControlPanel对于它实际调用的对象Fan或者Light是一无所知的,它只知道当On按下的时
候就调用onCommand的Execute()方法;当Off按下的时候就调用offCommand的Execute()方法。Light和Fan当然
更不知道谁在调用它。通过这种方式,我们实现了调用者(Invoker,遥控器ControlPanel) 和
被调用者(Receiver,电扇Fan等)的解耦。如果将来我们需要对这个ControlPanel进行扩展,只需要再添加一个实现了ICommand
接口的对象就可以了,对于ControlPanel无需做任何修改。
代码实现
namespace
Command {
// 定义空调,用于测试给遥控器添新控制类型
public
class
AirCondition
{
public
void
Start() {
Console
.WriteLine("The AirCondition is
turned on."
);
}
public
void
SetTemperature(int
i) {
Console
.WriteLine("The temperature is
set to "
+ i);
}
public
void
Stop() {
Console
.WriteLine("The AirCondition is
turned off."
);
}
}
// 定义Command接口
public
interface
ICommand
{
void
Execute();
}
// 定义开空调命令
public
class
AirOnCommand
: ICommand
{
AirCondition
airCondition;
public
AirOnCommand(AirCondition airCondition) {
this
.airCondition = airCondition;
}
public
void
Execute() { //注意,你可以在Execute()中添加多个方法
airCondition.Start();
airCondition.SetTemperature(16);
}
}
// 定义关空调命令
public
class
AirOffCommand
: ICommand
{
AirCondition
airCondition;
public
AirOffCommand(AirCondition airCondition) {
this
.airCondition = airCondition;
}
public
void
Execute() {
airCondition.Stop();
}
}
// 定义遥控器
public
class
ControlPanel
{
private
ICommand
onCommand;
private
ICommand
offCommand;
public
void
PressOn() {
onCommand.Execute();
}
public
void
PressOff() {
offCommand.Execute();
}
public
void
SetCommand(ICommand onCommand,ICommand offCommand) {
this
.onCommand = onCommand;
this
.offCommand = offCommand;
}
}
class
Program
{
static
void
Main(string
[] args) {
// 创建遥控器对象
ControlPanel
panel = new
ControlPanel
();
AirCondition
airCondition = new
AirCondition
(); //创建空调对象
// 创建Command对象,传递空调对象
ICommand
onCommand = new
AirOnCommand
(airCondition);
ICommand
offCommand = new
AirOffCommand
(airCondition);
// 设置遥控器的Command
panel.SetCommand(onCommand, offCommand);
panel.PressOn(); //按下On按钮,开空调,温度调到16度
panel.PressOff(); //按下Off按钮,关空调
}
}
}
Command 模式
实际上,我们上面做的这一切,实现了另一个设计模式:Command模式。现在又到了给出官方定义的时候了。每次到了这部分我就不知道该怎么写了,写的人太多了,资料也太多了,我相信你看到这里对Command模式已经比较清楚了,所以我还是一如既往地从简吧。
Command模式的正式定义:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。
它的 静态图 是这样的:
它的 时序图 是这样的:
可以和我们前面的图对比一下,对于这两个图,除了改了个名字外基本没变,我就不再说明了,也留给你一点思考的空间。
总结
本文简单地介绍了GOF的Commmand模式,我们通过一个简单的范例家电遥控器 实现了这一模式。
我们首先了解了不使用此模式的HardCoding方式的实现方法,讨论了它的缺点;然后又换了另一种改进了的实现方法,再次讨论了它的不足。
然后,我们通过将对象的调用封装到一个Command对象中的方式,巧妙地完成了设计。最后,我们给出了Command模式的正式定义。
转载地址:http://www.tracefact.net/Design-Pattern/Command.aspx
分享到:
相关推荐
命令模式是一种行为设计模式,它将请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。在本专题中,我们将深入探讨命令模式,并通过Python和C++两种...
**命令模式(Command模式)详解** 命令模式是一种行为设计模式,它将请求封装为一个对象,使得我们可以使用不同的请求、队列或者记录请求日志,还可以支持可撤销的操作。在命令模式中,我们创建表示各种操作的类...
命令模式的核心是将请求者(Invoker)与执行者(Receiver)解耦,通过引入命令(Command)接口和具体命令(Concrete Command)类来实现。在该模式中,有四个主要角色: 1. **命令(Command)接口**:定义了一个接收...
c++设计模式-行为型模式-命令模式;qt工程 ;c++简单源码; 命令(Command)模式的定义如下:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将...
在命令模式中,主要涉及到四个角色:客户端(Client)、命令接口(Command Interface)、具体命令(Concrete Command)和接收者(Receiver)。客户端创建具体命令对象并设置其接收者,然后将命令对象传递给调用者...
在“设计模式-command”中,我们看到这个模式的应用,通过将一个操作封装为一个对象(即命令对象),使得发送者可以无需知道接收者的具体实现,只需要知道如何发送命令即可。这种松耦合使得系统更易于维护和扩展。 ...
命令模式是一种行为设计模式,它将请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。在iOS开发中,命令模式尤其适用于处理事件驱动的编程场景,因为...
命令模式的核心结构包括:**命令(Command)**、**接收者(Receiver)**、**调用者(Invoker)**和**客户端(Client)**。下面将详细介绍这些组件及其在C#中的实现。 1. **命令(Command)接口**:定义了命令的接口...
此外,"Command模式.ppt"和"Command模式.docx"可能包含了对命令模式更深入的理论解释和示例代码,帮助学习者理解其原理和应用。 总之,Command模式通过将操作封装在独立的类中,提高了代码的可读性和可维护性,同时...
### (行为型模式) Command 命令模式 #### 概述 在软件工程领域,设计模式被广泛地应用于解决常见的编程难题。其中,“Command”(命令)模式是一种行为型设计模式,它允许将请求封装成对象,从而使你能够用不同的...
命令模式是一种行为设计模式,它将请求封装成独立的对象,使得可以使用不同的请求、队列请求、记录请求历史以及支持可撤销的操作。这种模式在软件工程中被广泛应用,尤其是在需要解耦请求发起者和接收者时,提高了...
### Cisco Catalyst 4500 Series Command Reference-12.2.54 #### 概述 Cisco Catalyst 4500系列交换机是企业级网络解决方案中的关键组件,为高性能、可扩展性和服务质量(QoS)提供了坚实的基础。Cisco IOS...
**命令模式(Command Pattern)详解** 命令模式是一种行为设计模式,它将请求封装为一个对象,使得你可以使用不同的请求、队列或者日志请求,也可以支持可撤销的操作。在C++中实现命令模式,可以有效地解耦调用者和...
Go语言中的`command`通常指的是命令模式,这是一种行为设计模式,它封装了对象的状态和行为,并将它们作为命令参数传递,使得请求可以被异步执行。在Go这种并发编程非常强大的语言中,命令模式有着广泛的应用,特别...
命令模式是一种行为设计模式,它将请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。在软件工程中,这种模式常用于解耦发出命令的对象和执行命令的...
命令模式(Command Pattern)是一种行为设计模式,它允许将请求封装为一个对象,从而使不同的请求可以使用不同的参数进行参数化,队列或日志请求,以及支持可撤销的操作。在 Java 中,命令模式的实现通常涉及一个...
命令模式是一种行为设计模式,它将请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。在本例中,我们将通过模仿遥控器来理解命令模式的应用。 遥控器...
**命令模式** 命令模式是一种行为设计模式,它将请求封装为一个对象,使得你可以使用不同的请求、队列请求、或者记录请求日志。在软件工程中,命令模式的应用广泛,尤其是在处理事件驱动系统或者需要解耦调用者与被...
命令模式的核心概念包括四个角色:客户端(Client)、命令接口(Command Interface)、具体命令(Concrete Command)和接收者(Receiver)。让我们逐一深入理解这些角色: 1. 客户端(Client):客户端是整个系统中...