Command 模式 Step by step

提起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();
}

哇,是不是有点简单的过分了!?但就是这么简单,可我们还是发现了两个问题:

  1. Command应该能知道它调用的是哪个电器类的哪个方法,这暗示我们Command类应该保存对于具体电器类的一个引用。
  2. 我们的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图,再进行代码实现吧(简单起见,只加入了灯和电扇)。

我们先看下这张图说明了什么,以及发生的顺序:

  1. ConsoleApplication,也就是我们的应用程序,它创建电器Fan、Light对象,以及LightOnCommand和FanStartCommand。
  2. LightOnCommand、FanStartCommand实现了ICommand接口,它保存着对于Fan和Light的引用,并通过Execute()调用Fan和Light的方法。
  3. 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模式的正式定义。

本文仅仅简要介绍了Command模式,它的高级应用:取消操作(UnDo)、事务支持(Transaction)、队列请求(Queuing Request) 以后有时间了会再写文章。

希望这篇文章能对你有所帮助!

时间: 2024-10-29 00:40:26

Command 模式 Step by step的相关文章

Git Step by Step – (8) Git的merge和rebase

前面一篇文章中提到了"git pull"等价于"git fetch"加上"git merge",然后还提到了pull命令支持rebase模式,这篇文章就介绍一下merge和rebase之间有什么差别. 由于我们主要是想看看merge跟rebase之间的区别,这里就是用本地仓库的分支进行演示了. merge 其实在介绍分支的那篇文章中已经介绍过了一些分支merge的内容,这里就进行一些补充和总结. 下面我们基于本地一个仓库开始介绍,当前仓库的分支情

Linux Booting Process: A step by step tutorial for understanding Linux boot sequence

One of the most remarkable achievement in the history of mankind is computers. Another amazing fact about this remarkable achievement called computers is that its a collection of different electronic components, and they work together in coordination

study Mvc step by step (一) 什么是Mvc啊?

当我们开始逐步把Net平台上面的Web开发从webform过度到MVC 开发的时候.我们总想弄清楚Mvc到底是什么??其实Mvc并不是Net特有的一种开发技术.而是一种软件开发的模式.早在上个世界80年代.Xerox PARC为编程语言Smalltalk-80发明的一种软件设计模式,已被广泛使用.那么什么是Mvc呢? MVC 是一种使用 MVC(Model View Controller 模型-视图-控制器)设计创建 Web 应用程序的模式: Model(模型)表示应用程序核心(比如数据库记录列

Step By Step(Lua目录)

Step By Step(Lua开篇)http://www.cnblogs.com/stephen-liu74/archive/2012/03/17/2403210.html一.简介二.主要优势三.应用场景 Step By Step(Lua基础知识)http://www.cnblogs.com/stephen-liu74/archive/2012/03/20/2407507.html一.基础知识二.类型与值 Step By Step(Lua表达式和语句)http://www.cnblogs.co

WPF MVVM 架构 Step By Step(6)(把actions从view model解耦)

到现在为止,我们创建了一个简单的MVVM的例子,包含了实现了的属性和命令.我们现在有这样一个包含了例如textbox类似的输入元素的视图,textbox用绑定来和view model联系,像点击button这样的行为用命令来联系.view model和model在内部通信. 但是在上面的架构中有一个问题,command类和view model有很严重的耦合.如果你记得command类的代码(在下面也有展示),在构造函数中传递view model对象,意味着这个command 类不能再其他的vie

WPF Step By Step 控件介绍

WPF Step By Step 控件介绍 回顾 上一篇,我们主要讨论了WPF的几个重点的基本知识的介绍,本篇,我们将会简单的介绍几个基本控件的简单用法,本文会举几个项目中的具体的例子,结合这些 例子,希望我们可以对WPF的掌握会更深刻.本文涉及的内容可能较多.请大家慢慢看看.错误之处,还请指出. 本文大纲 1.基本控件介绍与用法. 基本控件介绍与用法 文本控件 Label控件 label控件:一般用户描述性文字显示. 在Label控件使用时,一般给予用户提示.用法上没有什么很特殊的,label

WPF Step By Step 完整布局介绍

WPF Step By Step 完整布局介绍 回顾 上一篇,我们介绍了基本控件及控件的重要属性和用法,我们本篇详细介绍WPF中的几种布局容器及每种布局容器的使用场景,当 然这些都是本人在实际项目中的使用经验,可能还存在错误之处,还请大家指出. 本文大纲 1.Grid 2.StackPanel 3.DockPanel 4.WrapPanel Grid 1.Row和Column 我们下面来介绍Grid的行的用法,及我们在UI设计过程中需要注意的细节. 由于前面我们在第一章中已经介绍了基本的关于Gr

WPF Step By Step 系列-Prism框架在项目中使用

WPF Step By Step 系列-Prism框架在项目中使用 回顾 上一篇,我们介绍了关于控件模板的用法,本节我们将继续说明WPF更加实用的内容,在大型的项目中如何使用Prism框架,并给予Prism框架来构建基础的应用框架,并且如何来设计项目的架构和模块,下面我们就来一步步开始吧. 本文大纲 1.Prism框架下载和说明 2.Prism项目预览及简单介绍. 3.Prism框架如何在项目中使用. Prism框架下载和说明 Prism框架是针对WPF和Silverlight的MVVM框架,这

FreeSWITCH 1.2.5.3 Step by Step Install

Ubuntu: apt-get -y install build-essential automake autoconf git-core wget libtool apt-get -y install libncurses5-dev libtiff-dev libjpeg-dev zlib1g-dev 从 Git 仓库安装: 从代码库安装能让你永远使用最新的版本: git clone git://git.freeswitch.org/freeswitch.git cd freeswitch .