使用C# (.NET Core) 实现命令设计模式 (Command Pattern)

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

项目需求

640?wx_fmt=png&wxfrom=5&wx_lazy=1

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

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

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

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

640?wx_fmt=png

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

设计思路

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

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

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

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

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

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

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

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

一个命令模式的实际例子

一个快餐厅:

640?wx_fmt=png

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

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

640?wx_fmt=png

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

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

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

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

 

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

  • 订单里封装了做饭菜的请求. 可以把订单想象成一个对象, 这个对象就像是对做饭这个动作的请求. 并且它可以来回传递. 订单实现了一个只有orderUp()方法的接口, 这个方法里面封装了做饭的操作流程. 订单同时对动作实施者的引用(厨师). 因为都封装了, 所以服务员不知道订单里面有啥也不知道厨师是谁. 服务员只传递订单, 并调用orderUp().

  • 所以, 服务员的工作就是传递订单并且调用orderUp(). 服务员的取订单takeOrder()方法会传进来不同的参数(不同客户的不同订单), 但是这不是问题, 因为她知道所有的订单都支持orderUp()方法.

  • 厨师知道如何把饭做好. 一旦服务员调用了orderUp(), 厨师就接管了整个工作把饭菜做好. 但是服务员和厨师是解耦的: 服务员只有订单, 订单里封装着饭菜, 服务员只是调用订单上的一个方法而已. 同样的, 厨师只是从订单上收到指令, 他从来不和服务员直接接触.

项目设计图

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

640?wx_fmt=png

客户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();

        }

    }

}

640?wx_fmt=png

命令模式定义

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

类图:

640?wx_fmt=png

效果图:

640?wx_fmt=png

全功能代码的实施

遥控器:

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);

        }

    }

}

640?wx_fmt=png

该需求的设计图:

640?wx_fmt=png

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

实现取消按钮

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);

        }

    }

}

640?wx_fmt=png

基本是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();

        }

    }

}

运行:

640?wx_fmt=png

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

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);

        }

    }

}


640?wx_fmt=png

 

命令模式实际应用举例

请求队列

640?wx_fmt=png

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

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

记录请求

640?wx_fmt=png

640?wx_fmt=png

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

 

其它

这个系列的代码我放在这里了: https://github.com/solenovex/Head-First-Design-Patterns-in-CSharp

相关文章:

原文地址 http://www.cnblogs.com/cgzl/p/8830596.html


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

640?wx_fmt=jpeg

阅读更多
上一篇2018年4月更新70多个公司dnc招聘职位
下一篇把旧系统迁移到.Net Core 2.0 日记(2) - 依赖注入/日志NLog
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭