【澳门新葡新京】实现命令设计模式,封装调用

本文的概念内容来自深入浅出设计模式一书.

07 《Head First设计模式》 读书笔记07 封装调用:命令模式

项目需求

澳门新葡新京 1

有这样一个可编程的新型遥控器, 它有7个可编程插槽,
每个插槽可连接不同的家用电器设备. 每个插槽对应两个按钮: 开, 关(ON, OFF).
此外还有一个全局的取消按钮(UNDO).

现在客户想使用这个遥控器来控制不同厂家的家用电器, 例如电灯, 热水器,
风扇, 音响等等.

客户提出让我编写一个接口,
可以让这个遥控器控制插在插槽上的一个或一组设备.

看一下目前各家厂商都有哪些家用电器😭:

澳门新葡新京 2

问题来了,
这些家用电器并没有共同的标准….几乎各自都有自己的一套控制方法..
而且以后还要添加很多种家用电器.

 

设计思路

那就需要考虑一下设计方案了:

首先要考虑分离关注点(Separation of
concerns), 
遥控器应该可以解释按钮动作并可以发送请求,
但是它不应该了解家用电器和如何开关家用电器等.

但是目前遥控器只能做开关功能, 那么怎么让它去控制电灯或者音响呢?
我们不想让遥控器知道这些具体的家用电器, 更不想写出下面的代码:

if slot1 == Light then Light.On()
else if slot1 == Hub....

说到这就不得不提到命令模式(Command
Pattern)
了.

命令模式允许你把动作的请求者和动作的实际执行者解耦.
这里, 动作的请求者就是遥控器, 而执行动作的对象就是某个家用电器.

这是怎么解耦的呢? 怎么可能实现呢?

这就需要引进”命令对象(command object)”了.
命令对象会封装在某个对象上(例如卧室的灯)执行某个动作的请求(例如开灯).
所以, 如果我们为每一个按钮都准备一个命令对象, 那么当按钮被按下的时候,
我们就会调用这个命令对象去执行某些动作.
遥控器本身并不知道具体执行的动作是什么, 它只是有一个命令对象,
这个命令对象知道去对哪些电器去做什么样的操作. 就这样,
遥控器和电灯解耦了.

问题引入

  有一个附着多组开关按钮的遥控器,带有可编程插槽,每个都可以指定到一个不同的家电装置;有很多厂商开发的各种家电装置控制类;希望创建一组API,让每个插槽控制一个或一组装置。

  考虑:不应该让遥控器知道太多厂商类的细节,否则更多的家电加进来,就必须修改代码。

一个命令模式的实际例子

一个快餐厅:

澳门新葡新京 3

客户给服务员订单, 服务员把订单放到柜台并说: “有新订单了”,
然后厨师按照订单准备饭菜.

让我们仔细分析一下它们是怎么交互的:

澳门新葡新京 4

客户来了, 说我想要汉堡, 奶酪….就是创建了一个订单 (createOrder()).

订单上面写着客户想要的饭菜. 

服务员取得订单 takeOrder(), 把订单拿到柜台喊道:
“有新订单了” (调用orderUp())

厨师按照订单的指示把饭菜做好 (orderUp()里面的动作). 

 

分析一下这个例子的角色和职责:

  • 订单里封装了做饭菜的请求. 可以把订单想象成一个对象,
    这个对象就像是对做饭这个动作的请求. 并且它可以来回传递.
    订单实现了一个只有orderUp()方法的接口,
    这个方法里面封装了做饭的操作流程. 订单同时对动作实施者的引用(厨师).
    因为都封装了, 所以服务员不知道订单里面有啥也不知道厨师是谁.
    服务员只传递订单, 并调用orderUp().
  • 所以, 服务员的工作就是传递订单并且调用orderUp().
    服务员的取订单takeOrder()方法会传进来不同的参数(不同客户的不同订单),
    但是这不是问题, 因为她知道所有的订单都支持orderUp()方法.
  • 厨师知道如何把饭做好. 一旦服务员调用了orderUp(),
    厨师就接管了整个工作把饭菜做好. 但是服务员和厨师是解耦的:
    服务员只有订单, 订单里封装着饭菜,
    服务员只是调用订单上的一个方法而已. 同样的,
    厨师只是从订单上收到指令, 他从来不和服务员直接接触.

 

项目设计图

回到我们的需求, 参考快餐店的例子, 使用命令模式做一下设计:

澳门新葡新京 5

客户Client创建了一个命令(Command)对象.
相当于客人拿起了一个订单(点菜)准备开始点菜,
我在琢磨遥控器的槽需要插哪些家用电器. 命令对象和接收者是绑定在一起的.
相当于菜单和厨师, 遥控器的插槽和目标家用电器.

命令对象只有一个方法execute(), 里面封装了调用接收者实际控制操作的动作.
相当于饭店订单的orderUp().

客户调用setCommand()方法. 相当于客户想好点什么菜了, 就写在订单上面了.
我也想好遥控器要控制哪些家电了, 列好清单了. 

调用者拿着已经setCommand的命令对象,
在未来某个时间点调用命令对象上面的execute()方法.
相当于服务员拿起订单走到柜台前, 大喊一声: “有订单来了, 开始做菜吧”.
相当于我把遥控器和设备的接口连接上了, 准备开始控制.

最后接收者执行动作. 相当于厨师做饭.
家用电器使用自己独有的控制方法进行动作.

这里面:

客户 — 饭店客人, 我

命令 — 订单, 插槽

调用者 — 服务员, 遥控器

setCommand()设置命令 — takeOrder() 取订单, 插上需要控制的电器

execute() 执行 —  orderUp() 告诉柜台做饭, 按按钮

接收者 — 厨师, 家电

解决方案

  使用命令模式,将动作的请求者从动作的执行者对象中解耦。

  在该问题中,请求者可以是遥控器,而执行者对象就是厂商类其中之一的实例。

  利用“命令对象”,把请求封装成一个特定对象,每个按钮都存储一个命令对象,当按钮被按下的时候,就可以请命令对象做相关的工作。

  遥控器并不需要知道工作内容是什么,只要有个命令对象能和正确的对象沟通,把事情做好就可以了。

代码实施

所有命令对象需要实现的接口:

namespace CommandPattern.Abstractions
{
    public interface ICommand
    {
        void Execute();
    }
}

一盏灯:

using System;

namespace CommandPattern.Devices
{
    public class Light
    {
        public void On()
        {
            Console.WriteLine("Light is on");
        }

        public void Off()
        {
            Console.WriteLine("Light is off");
        }
    }
}

控制灯打开的命令:

using CommandPattern.Abstractions;
using CommandPattern.Devices;

namespace CommandPattern.Commands
{
    public class LightOnCommand : ICommand
    {
        private readonly Light light;

        public LightOnCommand(Light light)
        {
            this.light = light;
        }

        public void Execute()
        {
            this.light.On();
        }
    }
}

车库门: 

using System;

namespace CommandPattern.Devices
{
    public class GarageDoor
    {
        public void Up()
        {
            Console.WriteLine("GarageDoor is opened.");
        }

        public void Down()
        {
            Console.WriteLine("GarageDoor is closed.");
        }
    }
}

收起车库门命令:

using CommandPattern.Abstractions;
using CommandPattern.Devices;

namespace CommandPattern.Commands
{
    public class GarageDoorOpen : ICommand
    {
        private readonly GarageDoor garageDoor;

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

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

简易的遥控器:

using CommandPattern.Abstractions;

namespace CommandPattern.RemoteControls
{
    public class SimpleRemoteControl
    {
        public ICommand Slot { get; set; }
public void ButtonWasPressed()
        {
            Slot.Execute();
        }
    }
}

运行测试:

using System;
using CommandPattern.Commands;
using CommandPattern.Devices;
using CommandPattern.RemoteControls;

namespace CommandPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            var remote = new SimpleRemoteControl();
            var light = new Light();
            var lightOn = new LightOnCommand(light);

            remote.Slot = lightOn;
            remote.ButtonWasPressed();

            var garageDoor = new GarageDoor();
            var garageDoorOpen = new GarageDoorOpenCommand(garageDoor);

            remote.Slot = garageDoorOpen;
            remote.ButtonWasPressed();
        }
    }
}

澳门新葡新京 6

澳门新葡新京, 

命令模式定义

命令模式把请求封装成一个对象,
从而可以使用不同的请求对其它对象进行参数化, 对请求排队, 记录请求的历史,
并支持取消操作
.

类图:

澳门新葡新京 7

效果图:

澳门新葡新京 8

用实例说明

  在餐厅,顾客将自己的需求写在订单上,由服务员交给厨师,然后厨师按照订单指令准备餐点。

全功能代码的实施

遥控器:

using System.Text;
using CommandPattern.Abstractions;
using CommandPattern.Commands;

namespace CommandPattern.RemoteControls
{
    public class RemoteControl
    {
        private ICommand[] onCommands;
        private ICommand[] offCommands;

        public RemoteControl()
        {
            onCommands = new ICommand[7];
            offCommands = new ICommand[7];

            var noCommand = new NoCommand();
            for (int i = 0; i < 7; i++)
            {
                onCommands[i] = noCommand;
                offCommands[i] = noCommand;
            }
        }

        public void SetCommand(int slot, ICommand onCommand, ICommand offCommand)
        {
            onCommands[slot] = onCommand;
            offCommands[slot] = offCommand;
        }

        public void OnButtonWasPressed(int slot)
        {
            onCommands[slot].Execute();
        }
        public void OffButtonWasPressed(int slot)
        {
            offCommands[slot].Execute();
        }

        public override string ToString()
        {
            var sb = new StringBuilder("\n------------Remote Control-----------\n");
            for(int i =0; i< onCommands.Length; i++){
                sb.Append($"[slot{i}] {onCommands[i].GetType()}\t{offCommands[i].GetType()} \n");
            }
            return sb.ToString();
        }
    }
}

这里面有一个NoCommand, 它是一个空的类, 只是为了初始化command
以便以后不用判断是否为null.

关灯:

using CommandPattern.Abstractions;
using CommandPattern.Devices;

namespace CommandPattern.Commands
{
    public class LightOffCommand: ICommand
    {
        private readonly Light light;

        public LightOffCommand(Light light)
        {
            this.light = light;
        }

        public void Execute()
        {
            light.Off();
        }
    }
}

下面试一个有点挑战性的, 音响:

namespace CommandPattern.Devices
{
    public class Stereo
    {
        public void On()
        {
            System.Console.WriteLine("Stereo is on.");
        }

        public void Off()
        {
            System.Console.WriteLine("Stereo is off.");
        }

        public void SetCD()
        {
            System.Console.WriteLine("Stereo is set for CD input.");
        }

        public void SetVolume(int volume)
        {
            System.Console.WriteLine($"Stereo's volume is set to {volume}");
        }
    }
}

音响打开命令:

using CommandPattern.Abstractions;

namespace CommandPattern.Devices
{
    public class StereoOnWithCDCommand : ICommand
    {
        private readonly Stereo stereo;

        public StereoOnWithCDCommand(Stereo stereo)
        {
            this.stereo = stereo;
        }

        public void Execute()
        {
            stereo.On();
            stereo.SetCD();
            stereo.SetVolume(10);
        }
    }
}

测试运行:

using System;
using CommandPattern.Commands;
using CommandPattern.Devices;
using CommandPattern.RemoteControls;

namespace CommandPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            var remote = new RemoteControl();
            var light = new Light();
            var lightOn = new LightOnCommand(light);
            var lightOff = new LightOffCommand(light);
            var garageDoor = new GarageDoor();
            var garageDoorOpen = new GarageDoorOpenCommand(garageDoor);
            var garageDoorClose = new GarageDoorCloseCommand(garageDoor);
            var stereo = new Stereo();
            var stereoOnWithCD = new StereoOnWithCDCommand(stereo);
            var stereoOff = new StereoOffCommand(stereo);

            remote.SetCommand(0, lightOn, lightOff);
            remote.SetCommand(1, garageDoorOpen, garageDoorClose);
            remote.SetCommand(2, stereoOnWithCD, stereoOff);

            System.Console.WriteLine(remote);

            remote.OnButtonWasPressed(0);
            remote.OffButtonWasPressed(0);
            remote.OnButtonWasPressed(1);
            remote.OffButtonWasPressed(1);
            remote.OnButtonWasPressed(2);
            remote.OffButtonWasPressed(2);
        }
    }
}

澳门新葡新京 9

该需求的设计图:

澳门新葡新京 10

还有一个问题…取消按钮呢?

 

实现取消按钮

  1. 可以在ICommand接口里面添加一个undo()方法,
    然后在里面执行上一次动作相反的动作即可:

    namespace CommandPattern.Abstractions
    {

     public interface ICommand
     {
         void Execute();
         void Undo();
     }
    

    }

例如开灯:

using CommandPattern.Abstractions;
using CommandPattern.Devices;

namespace CommandPattern.Commands
{
    public class LightOnCommand : ICommand
    {
        private readonly Light light;

        public LightOnCommand(Light light)
        {
            this.light = light;
        }

        public void Execute()
        {
            light.On();
        }

        public void Undo()
        {
            light.Off();
        }
    }
}

遥控器:

using System.Text;
using CommandPattern.Abstractions;
using CommandPattern.Commands;

namespace CommandPattern.RemoteControls
{
    public class RemoteControlWithUndo
    {
        private ICommand[] onCommands;
        private ICommand[] offCommands;
        private ICommand undoCommand;

        public RemoteControlWithUndo()
        {
            onCommands = new ICommand[7];
            offCommands = new ICommand[7];

            var noCommand = new NoCommand();
            for (int i = 0; i < 7; i++)
            {
                onCommands[i] = noCommand;
                offCommands[i] = noCommand;
            }
            undoCommand = noCommand;
        }

        public void SetCommand(int slot, ICommand onCommand, ICommand offCommand)
        {
            onCommands[slot] = onCommand;
            offCommands[slot] = offCommand;
        }

        public void OnButtonWasPressed(int slot)
        {            
            onCommands[slot].Execute();
            undoCommand = onCommands[slot];
        }

        public void OffButtonWasPressed(int slot)
        {
            offCommands[slot].Execute();
            undoCommand = offCommands[slot];
        }

        public void UndoButtonWasPressed()
        {
            undoCommand.Undo();
        }

        public override string ToString()
        {
            var sb = new StringBuilder("\n------------Remote Control-----------\n");
            for(int i =0; i< onCommands.Length; i++){
                sb.Append($"[slot{i}] {onCommands[i].GetType()}\t{offCommands[i].GetType()} \n");
            }
            return sb.ToString();
        }
    }
}

测试一下:

using System;
using CommandPattern.Commands;
using CommandPattern.Devices;
using CommandPattern.RemoteControls;

namespace CommandPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            var remote = new RemoteControl();
            var light = new Light();
            var lightOn = new LightOnCommand(light);
            var lightOff = new LightOffCommand(light);
            var stereo = new Stereo();
            var stereoOnWithCD = new StereoOnWithCDCommand(stereo);
            var stereoOff = new StereoOffCommand(stereo);

            remote.SetCommand(0, lightOn, lightOff);
            remote.SetCommand(1, stereoOnWithCD, stereoOff);

            System.Console.WriteLine(remote);

            remote.OnButtonWasPressed(0);
            remote.OffButtonWasPressed(0);
            remote.OnButtonWasPressed(1);
            remote.OffButtonWasPressed(1);
        }
    }
}

澳门新葡新京 11

基本是OK的, 但是有点小问题, 音响的开关状态倒是取消了,
但是它的音量(也包括播放介质, 不过这个我就不去实现了)并没有恢复.

下面就来处理一下这个问题.

修改Stereo:

namespace CommandPattern.Devices
{
    public class Stereo
    {

        public Stereo()
        {
            Volume = 5;
        }

        public void On()
        {
            System.Console.WriteLine("Stereo is on.");
        }

        public void Off()
        {
            System.Console.WriteLine("Stereo is off.");
        }

        public void SetCD()
        {
            System.Console.WriteLine("Stereo is set for CD input.");
        }

        private int volume;
        public int Volume
        {
            get { return volume; }
            set
            {
                volume = value;
                System.Console.WriteLine($"Stereo's volume is set to {volume}");
            }
        }

    }
}

命令:

using CommandPattern.Abstractions;

namespace CommandPattern.Devices
{
    public class StereoOnWithCDCommand : ICommand
    {
        private int previousVolume;

        private readonly Stereo stereo;
public StereoOnWithCDCommand(Stereo stereo)
        {
            this.stereo = stereo;
       previousVolume = stereo.Volume;
        }

        public void Execute()
        {
            stereo.On();
            stereo.SetCD();
            stereo.Volume = 10;
        }

        public void Undo()
        {
            stereo.Volume = previousVolume;
            stereo.SetCD();
            stereo.Off();
        }
    }
}

运行:

澳门新葡新京 12

定义命令模式

  命令模式将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。

  命令模式也支持可撤销的操作。

  命令对象将动作和接收者包进对象中。

需求变更—-一个按钮控制多个设备的多个动作

Party Mode (聚会模式):

思路是创建一种命令, 它可以执行多个其它命令

MacroCommand:

using CommandPattern.Abstractions;

namespace CommandPattern.Commands
{
    public class MacroCommand : ICommand
    {
        private ICommand[] commands;

        public MacroCommand(ICommand[] commands)
        {
            this.commands = commands;
        }

        public void Execute()
        {
            for (int i = 0; i < commands.Length; i++)
            {
                commands[i].Execute();
            }
        }

        public void Undo()
        {
            for (int i = 0; i < commands.Length; i++)
            {
                commands[i].Undo();
            }
        }
    }
}

使用这个MacroCommand:

using System;
using CommandPattern.Abstractions;
using CommandPattern.Commands;
using CommandPattern.Devices;
using CommandPattern.RemoteControls;

namespace CommandPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            var light = new Light();
            var lightOn = new LightOnCommand(light);
            var lightOff = new LightOffCommand(light);
            var garageDoor = new GarageDoor();
            var garageDoorOpen = new GarageDoorOpenCommand(garageDoor);
            var garageDoorClose = new GarageDoorCloseCommand(garageDoor);
            var stereo = new Stereo();
            var stereoOnWithCD = new StereoOnWithCDCommand(stereo);
            var stereoOff = new StereoOffCommand(stereo);

            var macroOnCommand = new MacroCommand(new ICommand[] { lightOn, garageDoorOpen, stereoOnWithCD });
            var macroOffCommand = new MacroCommand(new ICommand[] { lightOff, garageDoorClose, stereoOff });

            var remote = new RemoteControl();
            remote.SetCommand(0, macroOnCommand, macroOffCommand);
            System.Console.WriteLine(remote);

            System.Console.WriteLine("--- Pushing Macro on ---");
            remote.OnButtonWasPressed(0);
            System.Console.WriteLine("--- Pushing Macro off ---");
            remote.OffButtonWasPressed(0);
        }
    }
}

澳门新葡新京 13

 

 

命令模式实际应用举例

命令模式的类图

 澳门新葡新京 14

 

 

用命令模式实现遥控器问题的类图:

澳门新葡新京 15 

 

请求队列

澳门新葡新京 16

这个工作队列是这样工作的: 你添加命令到队列的结尾,
在队列的另一端有几个线程. 线程这样工作: 它们从队列移除一个命令,
调用它的execute()方法, 然后等待调用结束,
然后丢弃这个命令再获取一个新的命令.

这样我们就可以把计算量限制到固定的线程数上面了.
工作队列和做工作的对象也是解耦的.

撤销操作的实现

  在Command接口中加入undo()方法,然后每个具体的命令类都实现这个方法:undo方法中调用的操作和execute()方法中的操作相反,比如on的反面是off。

  遥控器类也要做一些修改:加入新的实例变量记录上一个操作,当按下撤销按钮,就调用该实例变量的undo()方法。

  如果撤销到某一个状态需要记录一些变量值,则在execute()方法中加入保存操作前状态数值的语句,然后在undo()的时候恢复。

  如果想要多次撤销,即撤销到很早以前的状态,可以使用一个堆栈记录操作过程的每一个命令。

记录请求

澳门新葡新京 17

澳门新葡新京 18

这个例子就是使用命令模式记录请求动作的历史, 如果出问题了,
可以按照这个历史进行恢复.

 

 

其它

这个系列的代码我放在这里了: 

 

宏命令

  通过一个键,控制多种操作。

  在宏命令中,用命令数组存储一大堆命令,当这个宏命令被遥控器执行时,就一次性执行数组里的每个命令。

 

命令模式的更多用途

 

队列请求

  命令可以将运算块打包(一个接收者和一组动作),然后将它传来传去,就像是对一般的对象一样。

  我们可以利用这样的特性衍生一些应用,例如:日程安排,线程池,工作队列等。

  想象有一个工作队列:你在一端添加命令,然后另一端则是线程。

  线程从队列中取出命令,调用它的execute()方法,等待这个调用完成,然后将此命令对象丢弃,在取出下一个命令……

 

日志请求

  某些应用需要我们将所有的动作都记录在日志中,并能在系统死机之后,重新调用这些动作恢复到之前的状态。

  通过新增两个方法store()、load(),命令模式就能够支持这一点。

  在Java中,我们可以利用对象的序列化(Serialization)实现这些方法,但是一般认为序列化最好还是只用在对象的持久化上(persistence)。

  怎么做:当我们执行命令的时候,将历史记录储存在磁盘中。一旦系统死机,我们就可以将命令对象重新加载,并成批地依次调用这些对象的execute()方法。

 

题外话:《Head
First设计模式》读书笔记系列的更新暂停,我不会告诉你是因为图书馆的书超期并且被人预约了所以我借不到了。年后开学再说吧,希望我能坚持到底。

 

相关文章