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

项目需求

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

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

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

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

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

设计思路

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

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

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

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

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

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

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

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

一个命令模式的实际例子

一个快餐厅:

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

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

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

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

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

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

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

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

项目设计图

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

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

命令模式定义

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

类图:

效果图:

全功能代码的实施

遥控器:

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

该需求的设计图:

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

实现取消按钮

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

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

运行:

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

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

命令模式实际应用举例

请求队列

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

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

记录请求

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

其它

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

使用C# (.NET Core) 实现命令设计模式 (Command Pattern)的更多相关文章

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

    本文的概念内容来自深入浅出设计模式一书. 项目需求 有这样一个可编程的新型遥控器, 它有7个可编程插槽, 每个插槽可连接不同的家用电器设备. 每个插槽对应两个按钮: 开, 关(ON, OFF). 此外 ...

  2. 乐在其中设计模式(C#) - 命令模式(Command Pattern)

    原文:乐在其中设计模式(C#) - 命令模式(Command Pattern) [索引页][源码下载] 乐在其中设计模式(C#) - 命令模式(Command Pattern) 作者:webabcd ...

  3. 设计模式 - 命令模式(command pattern) 多命令 具体解释

    命令模式(command pattern) 多命令 具体解释 本文地址: http://blog.csdn.net/caroline_wendy 參考命令模式: http://blog.csdn.ne ...

  4. 设计模式 - 命令模式(command pattern) 具体解释

    命令模式(command pattern) 详细解释 本文地址: http://blog.csdn.net/caroline_wendy 命令模式(command pattern) : 将请求封装成对 ...

  5. 设计模式 - 命令模式(command pattern) 宏命令(macro command) 具体解释

    命令模式(command pattern) 宏命令(macro command) 具体解释 本文地址: http://blog.csdn.net/caroline_wendy 參考: 命名模式(撤销) ...

  6. 设计模式 - 命令模式(command pattern) 撤销(undo) 具体解释

    命令模式(command pattern) 撤销(undo) 详细解释 本文地址: http://blog.csdn.net/caroline_wendy 參考命令模式: http://blog.cs ...

  7. 二十四种设计模式:命令模式(Command Pattern)

    命令模式(Command Pattern) 介绍将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化:对请求排队或记录请求日志,以及支持可取消的操作. 示例有一个Message实体类,某个 ...

  8. 设计模式-15命令模式(Command Pattern)

    1.模式动机 在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计,使 ...

  9. 设计模式のCommand Pattern(命令模式)----行为模式

    一.产生背景 熟悉计算机的同学应该清楚,用户发出各种命令,CPU执行命令,OS负责调度.命令模式(Command Pattern)是一种数据驱动的设计模式,它属于行为型模式.请求以命令的形式包裹在对象 ...

随机推荐

  1. Virtualbox 安装centos7虚拟机

    Virtualbox 安装centos7虚拟机 一,下载centos7 下载地址:https://mirrors.tuna.tsinghua.edu.cn/centos/7.9.2009/isos/x ...

  2. Linux 如何查看一个文件夹下面有多少个文件

    Linux 如何查看一个文件夹下面有多少个文件 $ tree $ find ./ -type f | wc -l $ ls -l | grep "^-" | wc -l refs ...

  3. 惠普机械键盘 K10GL 使用评测

    惠普机械键盘 GK100 使用评测 手感太差,不是 RGB 背光 惠普(HP) K10GL 机械键盘 有线 LED背光机械键盘 87键 混光青轴 refs https://item.jd.com/10 ...

  4. Promise nested then execute order All In One

    Promise nested then execute order All In One Promise nested then nested Promise not return new Promi ...

  5. js currying All In One

    js currying All In One 柯里化 refs https://juejin.im/post/6844903603266650125 xgqfrms 2012-2020 www.cnb ...

  6. chroot vs docker

    chroot vs docker chroot Linux A chroot on Unix operating systems is an operation that changes the ap ...

  7. github & gist & Weekly development breakdown

    github & gist & Weekly development breakdown https://gist.github.com/xgqfrms WakaTime waka-b ...

  8. JavaScript Best Practice

    JavaScript Best Practice Clean, maintainable, execute code

  9. c++ 动态解析PE导出表

    测试环境是x86 main #include <iostream> #include <Windows.h> #include <TlHelp32.h> #incl ...

  10. NGK数字钱包的特点是什么?NGK钱包的优点和缺点是什么?

    说起区块链数字资产,那就离不开谈到数字钱包.数字钱包不仅有资产管理的功能,还可以进行资产理财.资产交易,甚至能为公链DAPP导流. 对于NGK公链而言,其数字钱包已然成为了解NGK公链的基础条件.NG ...