学习设计模式第十七 - 命令模式
本文摘取自TerryLee(李会军)老师的设计模式系列文章,版权归TerryLee,仅供个人学习参考。转载请标明原作者TerryLee。部分示例代码来自DoFactory。
概述
在软件系统中,"行为请求者"与"行为实现者"通常呈现一种"紧耦合"。但在某些场合,比如要对行为进行"记录、撤销/重做、事务"等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将"行为请求者"与"行为实现者"解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。这就是Command模式。
意图
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。
UML
Command模式结构图如下:
图1 Command模式UML图
参与者
这个模式涉及的类或对象:
-
Command
-
声明执行的操作实现的接口
-
ConcreteCommand
-
在接收对象与操作间定义一个绑定
-
实现Execute,在其中调用接收者上的相应的操作
-
Client
-
创建一个ConcreteCommand对象并设置其接收者
-
Invoker
-
让command对象执行请求
-
Receiver
-
知道怎样完成请求要执行的操作。
适用性
命令模式封装一个动作或请求为一个对象。这个模式的经典使用是在菜单系统中,其中每一个命令对象表示一个动作及相关的撤销动作。菜单动作包括如File|Open,File|Save,Edit|Copy等菜单项,并且每一个菜单项都映射到其自己的命令对象。
所有的命令对象实现了相同的接口,这样它们可以被以多态方式处理。典型的接口中包含如Do和Undo(或Execute和Undo)这样的方法。在下面这些地方你会找到命令模式:菜单命令系统及需要撤销功能的应用(如文字处理程序,另外有时候在需要撤销数据库操作功能的商业应用中)。
在下面的情况下应当考虑使用命令模式:
-
使用命令模式作为"CallBack"在面向对象系统中的替代。"CallBack"讲的便是先将一个函数登记上,然后在以后调用此函数。
-
需要在不同的时间指定请求、将请求排队。一个命令对象和原先的请求发出者可以有不同的生命期。换言之,原先的请求发出者可能已经不在了,而命令对象本身仍然是活动的。这时命令的接收者可以是在本地,也可以在网络的另外一个地址。命令对象可以在串形化之后传送到另外一台机器上去。
-
系统需要支持命令的撤消(undo)。命令对象可以把状态存储起来,等到客户端需要撤销命令所产生的效果时,可以调用undo()方法,把命令所产生的效果撤销掉。命令对象还可以提供redo()方法,以供客户端在需要时,再重新实施命令效果。
-
如果一个系统要将系统中所有的数据更新到日志里,以便在系统崩溃时,可以根据日志里读回所有的数据更新命令,重新调用Execute()方法一条一条执行这些命令,从而恢复系统在崩溃前所做的数据更新。
-
一个系统需要支持交易(Transaction)。一个交易结构封装了一组数据更新命令。使用命令模式来实现交易结构可以使系统增加新的交易类型。
DoFactory GoF代码
这个命令模式的例子中,请求被保存在对象中,以允许客户端执行或重做这些请求。
// Command pattern // Structural example using System; namespace DoFactory.GangOfFour.Command.Structural { // MainApp test application class MainApp { static void Main() { // Create receiver, command, and invoker Receiver receiver = new Receiver(); Command command = new ConcreteCommand(receiver); Invoker invoker = new Invoker(); // Set and execute command invoker.SetCommand(command); invoker.ExecuteCommand(); // Wait for user Console.ReadKey(); } } // "Command" abstract class Command { protected Receiver receiver; // Constructor public Command(Receiver receiver) { this.receiver = receiver; } public abstract void Execute(); } // "ConcreteCommand" class ConcreteCommand : Command { // Constructor public ConcreteCommand(Receiver receiver) : base(receiver) { } public override void Execute() { receiver.Action(); } } // "Receiver" class Receiver { public void Action() { Console.WriteLine("Called Receiver.Action()"); } } // "Invoker" class Invoker { private Command _command; public void SetCommand(Command command) { this._command = command; } public void ExecuteCommand() { _command.Execute(); } } }
在下面命令模式实际应用的例子中展示了一个可以进行无数次撤销与重做的简单的计算器。注意在C#中"operator"是一个关键字。在前面添加一个前缀 –'@'使其可以作为一个标示符使用。
例子中涉及到的类与适配器模式中标准的类对应关系如下:
-
Command – Command
-
ConcreteCommand – CalculatorCommand
-
Client – CommandApp
-
Invoker – User
-
Receiver – Calculator
// Command pattern // Real World example using System; using System.Collections.Generic; namespace DoFactory.GangOfFour.Command.RealWorld { // MainApp test application class MainApp { static void Main() { // Create user and let her compute User user = new User(); // User presses calculator buttons user.Compute('+', 100); user.Compute('-', 50); user.Compute('*', 10); user.Compute('/', 2); // Undo 4 commands user.Undo(4); // Redo 3 commands user.Redo(3); // Wait for user Console.ReadKey(); } } // "Command" abstract class Command { public abstract void Execute(); public abstract void UnExecute(); } // "ConcreteCommand" class CalculatorCommand : Command { private char _operator; private int _operand; private Calculator _calculator; // Constructor public CalculatorCommand(Calculator calculator, char @operator, int operand) { this._calculator = calculator; this._operator = @operator; this._operand = operand; } // Gets operator public char Operator { set { _operator = value; } } // Get operand public int Operand { set { _operand = value; } } // Execute new command public override void Execute() { _calculator.Operation(_operator, _operand); } // Unexecute last command public override void UnExecute() { _calculator.Operation(Undo(_operator), _operand); } // Returns opposite operator for given operator private char Undo(char @operator) { switch (@operator) { case '+': return '-'; case '-': return '+'; case '*': return '/'; case '/': return '*'; default: throw new ArgumentException("@operator"); } } } // "Receiver" class Calculator { private int _curr = 0; public void Operation(char @operator, int operand) { switch (@operator) { case '+': _curr += operand; break; case '-': _curr -= operand; break; case '*': _curr *= operand; break; case '/': _curr /= operand; break; } Console.WriteLine( "Current value = {0,3} (following {1} {2})", _curr, @operator, operand); } } // "Invoker" class User { // Initializers private Calculator _calculator = new Calculator(); private List<Command> _commands = new List<Command>(); private int _current = 0; public void Redo(int levels) { Console.WriteLine("\n---- Redo {0} levels ", levels); // Perform redo operations for (int i = 0; i < levels; i++) { if (_current < _commands.Count - 1) { Command command = _commands[_current++]; command.Execute(); } } } public void Undo(int levels) { Console.WriteLine("\n---- Undo {0} levels ", levels); // Perform undo operations for (int i = 0; i < levels; i++) { if (_current > 0) { Command command = _commands[--_current] as Command; command.UnExecute(); } } } public void Compute(char @operator, int operand) { // Create command operation and execute it Command command = new CalculatorCommand( _calculator, @operator, operand); command.Execute(); // Add command to undo list _commands.Add(command); _current++; } } }
在.NET优化版的例子中演示了与上述例子相同的功能,但使用了更多.NET内置的特性。在这个例子中Command抽象类中由于没有任何实现而被使用ICommand接口代替。另外,为增强User类中命令集合的类型安全,集合被实现为一个类型参数为ICommand接口类型的泛型列表(List<>)。
// Command pattern // .NET Optimized example using System; using System.Collections.Generic; namespace DoFactory.GangOfFour.Command.NETOptimized { class MainApp { static void Main() { // Create user and let her compute var user = new User(); // Issue several compute commands user.Compute('+', 100); user.Compute('-', 50); user.Compute('*', 10); user.Compute('/', 2); // Undo 4 commands user.Undo(4); // Redo 3 commands user.Redo(3); // Wait for user Console.ReadKey(); } } // "Command" interface ICommand { void Execute(); void UnExecute(); } // "ConcreteCommand" class CalculatorCommand : ICommand { private char _operator; private int _operand; private Calculator _calculator; // Constructor public CalculatorCommand(Calculator calculator, char @operator, int operand) { this._calculator = calculator; this._operator = @operator; this._operand = operand; } // Sets operator public char Operator { set { _operator = value; } } // Sets operand public int Operand { set { _operand = value; } } // Execute command public void Execute() { _calculator.Operation(_operator, _operand); } // Unexecute command public void UnExecute() { _calculator.Operation(Undo(_operator), _operand); } // Return opposite operator for given operator private char Undo(char @operator) { switch (@operator) { case '+': return '-'; case '-': return '+'; case '*': return '/'; case '/': return '*'; default: throw new ArgumentException("@operator"); } } } // "Receiver" class Calculator { private int _current = 0; // Perform operation for given operator and operand public void Operation(char @operator, int operand) { switch (@operator) { case '+': _current += operand; break; case '-': _current -= operand; break; case '*': _current *= operand; break; case '/': _current /= operand; break; } Console.WriteLine( "Current value = {0,3} (following {1} {2})", _current, @operator, operand); } } // "Invoker" class User { private Calculator _calculator = new Calculator(); private List<ICommand> _commands = new List<ICommand>(); private int _current = 0; // Redo original commands public void Redo(int levels) { Console.WriteLine("\n---- Redo {0} levels ", levels); // Perform redo operations for (int i = 0; i < levels; i++) { if (_current < _commands.Count - 1) { _commands[_current++].Execute(); } } } // Undo prior commands public void Undo(int levels) { Console.WriteLine("\n---- Undo {0} levels ", levels); // Perform undo operations for (int i = 0; i < levels; i++) { if (_current > 0) { _commands[--_current].UnExecute(); } } } // Compute new value given operator and operand public void Compute(char @operator, int operand) { // Create command operation and execute it ICommand command = new CalculatorCommand(_calculator, @operator, operand); command.Execute(); // Add command to undo list _commands.Add(command); _current++; } } }
Command模式解说
在众多的设计模式中,Command模式是很简单也很优雅的一种设计模式。Command模式它封装的是命令,把命令发出者的责任和命令执行者的责任分开。我们知道,一个类是一组操作和相应的一些变量的集合,现在有这样一个类Document,如下:
图2.Command模式示例中的Document模型
示意性代码:
// 文档类 public class Document { //显示操作 public void Display() { Console.WriteLine("Display "); } //撤销操作 public void Undo() { Console.WriteLine("Undo "); } //恢复操作 public void Redo() { Console.WriteLine("Redo "); } }
一般情况下我们使用这个类的时候,都会这样去写:
class Program { static void Main(string[] args) { Document doc = new Document(); doc.Display(); doc.Undo(); doc.Redo(); } }
这样的使用本来是没有任何问题的,但是我们看到在这个特定的应用中,出现了Undo/Redo的操作,这时如果行为的请求者和行为的实现者之间还是呈现这样一种紧耦合,就不太合适了。可以看到,客户程序是依赖于具体Document的命令(方法)的,引入Command模式,需要对Document中的三个命令进行抽象,这是Command模式最有意思的地方,因为在我们看来Display(),Undo(),Redo()这三个方法都应该是Document所具有的,如果单独抽象出来成一个命令对象,那就是把函数层面的功能提到了类的层面,有点功能分解的味道,我觉得这正是Command模式解决这类问题的优雅之处,先对命令对象进行抽象:
图3.对Document中的操作进行抽象
示意性代码:
// 抽象命令 public abstract class DocumentCommand { protected Document _document; public DocumentCommand(Document doc) { this._document = doc; } //执行 public abstract void Execute(); }
其他的具体命令类都继承于该抽象类,如下:
图4.Document上的操作都继承自命令的抽象
示意性代码:
// 显示命令 public class DisplayCommand : DocumentCommand { public DisplayCommand(Document doc) : base(doc) { } public override void Execute() { _document.Display(); } } // 撤销命令 public class UndoCommand : DocumentCommand { public UndoCommand(Document doc) : base(doc) { } public override void Execute() { _document.Undo(); } } // 重做命令 public class RedoCommand : DocumentCommand { public RedoCommand(Document doc) : base(doc) { } public override void Execute() { _document.Redo(); } }
现在还需要一个Invoker角色的类,这其实相当于一个中间角色,前面我曾经说过,使用这样的一个中间层也是我们经常使用的手法,即把A对B的依赖转换为A对C的依赖。如下:
图5.将命令抽象应用于系统中
示意性代码:
// Invoker角色 public class DocumentInvoker { DocumentCommand _discmd; DocumentCommand _undcmd; DocumentCommand _redcmd; public DocumentInvoker(DocumentCommand discmd, DocumentCommand undcmd, DocumentCommand redcmd) { this._discmd = discmd; this._undcmd = undcmd; this._redcmd = redcmd; } public void Display() { _discmd.Execute(); } public void Undo() { _undcmd.Execute(); } public void Redo() { _redcmd.Execute(); } }
现在再来看客户程序的调用代码:
class Program { static void Main(string[] args) { Document doc = new Document(); DocumentCommand discmd = new DisplayCommand(doc); DocumentCommand undcmd = new UndoCommand(doc); DocumentCommand redcmd = new RedoCommand(doc); DocumentInvoker invoker = new DocumentInvoker(discmd, undcmd, redcmd); invoker.Display(); invoker.Undo(); invoker.Redo(); } }
可以看到:
-
在客户程序中,不再依赖于Document的Display(),Undo(),Redo()命令,通过Command对这些命令进行了封装,使用它的一个关键就是抽象的Command类,它定义了一个操作的接口。同时我们也可以看到,本来这三个命令仅仅是三个方法而已,但是通过Command模式却把它们提到了类的层面,这其实是违背了面向对象的原则,但它却优雅的解决了分离命令的请求者和命令的执行者的问题,在使用Command模式的时候,一定要判断好使用它的时机。
-
上面的Undo/Redo只是简单示意性的实现,如果要实现这样的效果,需要对命令对象设置一个状态,由命令对象可以把状态存储起来。
.NET中的Command模式
虽然我们不曾看过Microsoft应用程序的源代码,但我们相当确定大部分,包括Visual Studio.NET,使用命令模式来支持其菜单,工具条,快捷方式及相关的撤销功能。我们曾期待在.NET中命令模式被暴露为WinForms统一命令路由架构的一部分,但是这没有。所以,到WPF出现前,命令模式在.NET Framework的使用没有普及,但随着WPF的引入这种情况发生改变:WPF在其命令系统中内置支持命令。
在ASP.NET的MVC模式中,有一种叫Front Controller的模式,它分为Handler和Command树两个部分,Handler处理所有公共的逻辑,接收HTTP Post或Get请求以及相关的参数并根据输入的参数选择正确的命令对象,然后将控制权传递到Command对象,由其完成后面的操作,这里面其实就是用到了Command模式。
图6.Front Controller的处理程序部分结构图
图7.Front Controller的命令部分结构图
Handler 类负责处理各个Web 请求,并将确定正确的 Command对象这一职责委派给CommandFactory 类。当CommandFactory返回Command对象后,Handler将调用Command上的Execute方法来执行请求。具体的实现如下
Handler类:
// Handler类 public class Handler : IHttpHandler { public void ProcessRequest(HttpContext context) { Command command = CommandFactory.Make(context.Request.Params); command.Execute(context); } public bool IsReusable { get { return true; } } }
Command接口:
// Command public interface Command { void Execute(HttpContext context); }
CommandFactory类:
// CommandFactory public class CommandFactory { public static Command Make(NameValueCollection parms) { string requestParm = parms["requestParm"]; Command command = null; //根据输入参数得到不同的Command对象 switch (requestParm) { case "1": command = new FirstPortal(); break; case "2": command = new SecondPortal(); break; default: command = new FirstPortal(); break; } return command; } }
RedirectCommand类:
public abstract class RedirectCommand : Command { //获得Web.Config中定义的key和url键值对,UrlMap类详见下载包中的代码 private UrlMap map = UrlMap.SoleInstance; protected abstract void OnExecute(HttpContext context); public void Execute(HttpContext context) { OnExecute(context); //根据key和url键值对提交到具体处理的页面 string url = String.Format("{0}?{1}", map.Map[context.Request.Url.AbsolutePath], context.Request.Url.Query); context.Server.Transfer(url); } }
FirstPortal类:
public class FirstPortal : RedirectCommand { protected override void OnExecute(HttpContext context) { //在输入参数中加入项portalId以便页面处理 context.Items["portalId"] = "1"; } }
SecondPortal类:
public class SecondPortal : RedirectCommand { protected override void OnExecute(HttpContext context) { context.Items["portalId"] = "2"; } }
效果及实现要点
-
Command模式的根本目的在于将"行为请求者"与"行为实现者"解耦,在面向对象语言中,常见的实现手段是"将行为抽象为对象"。
-
实现Command接口的具体命令对象ConcreteCommand有时候根据需要可能会保存一些额外的状态信息。
-
通过使用Compmosite模式,可以将多个命令封装为一个"复合命令"MacroCommand。
-
Command模式与C#中的Delegate有些类似。但两者定义行为接口的规范有所区别:Command以面向对象中的"接口-实现"来定义行为接口规范,更严格,更符合抽象原则;Delegate以函数签名来定义行为接口规范,更灵活,但抽象能力比较弱。
-
使用命令模式会导致某些系统有过多的具体命令类。某些系统可能需要几十个,几百个甚至几千个具体命令类,这会使命令模式在这样的系统里变得不实际。
来自《深入浅出设计模式》的例子
这个例子使用命令模式实现了一个支持撤销操作的多功能遥控器。
using System; using System.Text; namespace DoFactory.HeadFirst.Command { class RemoteLoader { static void Main(string[] args) { var remoteControl = new RemoteControl(); var light = new Light("Living Room"); var tv = new TV("Living Room"); var stereo = new Stereo("Living Room"); var hottub = new Hottub(); var lightOn = new LightOnCommand(light); var stereoOn = new StereoOnCommand(stereo); var tvOn = new TVOnCommand(tv); var hottubOn = new HottubOnCommand(hottub); var lightOff = new LightOffCommand(light); var stereoOff = new StereoOffCommand(stereo); var tvOff = new TVOffCommand(tv); var hottubOff = new HottubOffCommand(hottub); ICommand[] partyOn = { lightOn, stereoOn, tvOn, hottubOn}; ICommand[] partyOff = { lightOff, stereoOff, tvOff, hottubOff}; var partyOnMacro = new MacroCommand(partyOn); var partyOffMacro = new MacroCommand(partyOff); remoteControl.SetCommand(0, partyOnMacro, partyOffMacro); Console.WriteLine(remoteControl); Console.WriteLine("--- Pushing Macro On---"); remoteControl.OnButtonWasPushed(0); Console.WriteLine("\n--- Pushing Macro Off---"); remoteControl.OffButtonWasPushed(0); // Wait for user Console.ReadKey(); } } #region Remote Control public class RemoteControl { private ICommand[] _onCommands; private ICommand[] _offCommands; private ICommand _undoCommand; // Constructor public RemoteControl() { _onCommands = new ICommand[7]; _offCommands = new ICommand[7]; ICommand 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 OnButtonWasPushed(int slot) { _onCommands[slot].Execute(); _undoCommand = _onCommands[slot]; } public void OffButtonWasPushed(int slot) { _offCommands[slot].Execute(); _undoCommand = _offCommands[slot]; } public void UndoButtonWasPushed() { _undoCommand.Undo(); } public override string ToString() { StringBuilder sb = new StringBuilder(); sb.Append("\n------ Remote Control -------\n"); for (int i = 0; i < _onCommands.Length; i++) { sb.Append("[slot " + i + "] " + _onCommands[i].GetType().Name + " " + _offCommands[i].GetType().Name + "\n"); } sb.Append("[undo] " + _undoCommand.GetType().Name + "\n"); return sb.ToString(); } } #endregion #region Commands public interface ICommand { void Execute(); void Undo(); } public class NoCommand : ICommand { public void Execute() { } public void Undo() { } } 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(); } } } public class TVOnCommand : ICommand { private TV _tv; public TVOnCommand(TV tv) { this._tv = tv; } public void Execute() { _tv.On(); _tv.SetInputChannel(); } public void Undo() { _tv.Off(); } } public class TVOffCommand : ICommand { private TV _tv; public TVOffCommand(TV tv) { this._tv= tv; } public void Execute() { _tv.Off(); } public void Undo() { _tv.On(); } } public class StereoOnCommand : ICommand { private Stereo _stereo; public StereoOnCommand(Stereo stereo) { this._stereo = stereo; } public void Execute() { _stereo.On(); } public void Undo() { _stereo.Off(); } } public class StereoOffCommand : ICommand { private Stereo _stereo; public StereoOffCommand(Stereo stereo) { this._stereo = stereo; } public void Execute() { _stereo.Off(); } public void Undo() { _stereo.On(); } } public class StereoOnWithCDCommand : ICommand { private Stereo _stereo; public StereoOnWithCDCommand(Stereo stereo) { this._stereo = stereo; } public void Execute() { _stereo.On(); _stereo.SetCD(); _stereo.SetVolume(11); } public void Undo() { _stereo.Off(); } } public class LivingroomLightOnCommand : ICommand { private Light _light; public LivingroomLightOnCommand(Light light) { this._light = light; } public void Execute() { _light.On(); } public void Undo() { _light.Off(); } } public class LivingroomLightOffCommand : ICommand { private Light _light; public LivingroomLightOffCommand(Light light) { this._light = light; } public void Execute() { _light.Off(); } public void Undo() { _light.On(); } } public class LightOnCommand : ICommand { private Light _light; public LightOnCommand(Light light) { this._light = light; } public void Execute() { _light.On(); } public void Undo() { _light.Off(); } } public class LightOffCommand : ICommand { private Light _light; public LightOffCommand(Light light) { this._light = light; } public void Execute() { _light.Off(); } public void Undo() { _light.On(); } } public class HottubOffCommand : ICommand { private Hottub _hottub; public HottubOffCommand(Hottub hottub) { this._hottub = hottub; } public void Execute() { _hottub.SetTemperature(98); _hottub.Off(); } public void Undo() { _hottub.On(); } } public class CeilingFanOffCommand : ICommand { private CeilingFan _ceilingFan; private CeilingFanSpeed _prevSpeed; public CeilingFanOffCommand(CeilingFan ceilingFan) { this._ceilingFan = ceilingFan; } public void Execute() { _prevSpeed = _ceilingFan.Speed; _ceilingFan.Off(); } public void Undo() { switch (_prevSpeed) { case CeilingFanSpeed.High: _ceilingFan.high(); break; case CeilingFanSpeed.Medium: _ceilingFan.medium(); break; case CeilingFanSpeed.Low: _ceilingFan.low(); break; case CeilingFanSpeed.Off: _ceilingFan.Off(); break; } } } public class CeilingFanMediumCommand : ICommand { private CeilingFan _ceilingFan; private CeilingFanSpeed _prevSpeed; public CeilingFanMediumCommand(CeilingFan ceilingFan) { this._ceilingFan = ceilingFan; } public void Execute() { _prevSpeed = _ceilingFan.Speed; _ceilingFan.medium(); } public void Undo() { switch (_prevSpeed) { case CeilingFanSpeed.High: _ceilingFan.high(); break; case CeilingFanSpeed.Medium: _ceilingFan.medium(); break; case CeilingFanSpeed.Low: _ceilingFan.low(); break; case CeilingFanSpeed.Off: _ceilingFan.Off(); break; } } } public class CeilingFanHighCommand : ICommand { private CeilingFan _ceilingFan; private CeilingFanSpeed _prevSpeed; public CeilingFanHighCommand(CeilingFan ceilingFan) { this._ceilingFan = ceilingFan; } public void Execute() { _prevSpeed = _ceilingFan.Speed; _ceilingFan.high(); } public void Undo() { switch (_prevSpeed) { case CeilingFanSpeed.High: _ceilingFan.high(); break; case CeilingFanSpeed.Medium: _ceilingFan.medium(); break; case CeilingFanSpeed.Low: _ceilingFan.low(); break; case CeilingFanSpeed.Off: _ceilingFan.Off(); break; } } } public class HottubOnCommand : ICommand { private Hottub _hottub; public HottubOnCommand(Hottub hottub) { this._hottub = hottub; } public void Execute() { _hottub.On(); _hottub.SetTemperature(104); _hottub.Circulate(); } public void Undo() { _hottub.Off(); } } #endregion #region TV, Tub, CeilingFan, etc public class Hottub { private bool _on; private int _temperature; public void On() { _on = true; } public void Off() { _on = false; } public void Circulate() { if (_on) { Console.WriteLine("Hottub is bubbling!"); } } public void JetsOn() { if (_on) { Console.WriteLine("Hottub jets are on"); } } public void JetsOff() { if (_on) { Console.WriteLine("Hottub jets are off"); } } public void SetTemperature(int temperature) { if (temperature > this._temperature) { Console.WriteLine("Hottub is heating to a steaming " + temperature + " degrees"); } else { Console.WriteLine("Hottub is cooling to " + temperature + " degrees"); } this._temperature = temperature; } } public class TV { private string _location; private int _channel; public TV(string location) { this._location = location; } public void On() { Console.WriteLine(_location + " TV is on"); } public void Off() { Console.WriteLine(_location + " TV is off"); } public void SetInputChannel() { this._channel = 3; Console.WriteLine(_location + " TV channel " + _channel + " is set for DVD"); } } public class Stereo { private string _location; public Stereo(string location) { this._location = location; } public void On() { Console.WriteLine(_location + " stereo is on"); } public void Off() { Console.WriteLine(_location + " stereo is off"); } public void SetCD() { Console.WriteLine(_location + " stereo is set for CD input"); } public void setDVD() { Console.WriteLine(_location + " stereo is set for DVD input"); } public void SetRadio() { Console.WriteLine(_location + " stereo is set for Radio"); } public void SetVolume(int volume) { // code to set the volume // valid range: 1-11 (after all 11 is better than 10, right?) Console.WriteLine(_location + " Stereo volume set to " + volume); } } public class Light { private string _location; public Light(string location) { this._location = location; } public void On() { Level = 100; Console.WriteLine("Light is on"); } public void Off() { Level = 0; Console.WriteLine("Light is off"); } public void Dim(int level) { this.Level = level; if (Level == 0) { Off(); } else { Console.WriteLine("Light is dimmed to " + level + "%"); } } public int Level { get; private set; } } public class CeilingFan { private string _location; public CeilingFan(string location) { this._location = location; } public void high() { // turns the ceiling fan on to high Speed = CeilingFanSpeed.High; Console.WriteLine(_location + " ceiling fan is on high"); } public void medium() { // turns the ceiling fan on to medium Speed = CeilingFanSpeed.Medium; Console.WriteLine(_location + " ceiling fan is on medium"); } public void low() { // turns the ceiling fan on to low Speed = CeilingFanSpeed.Low; Console.WriteLine(_location + " ceiling fan is on low"); } public void Off() { // turns the ceiling fan off Speed = CeilingFanSpeed.Off; Console.WriteLine(_location + " ceiling fan is off"); } public CeilingFanSpeed Speed{ get; private set; } } public enum CeilingFanSpeed { High, Medium, Low, Off } #endregion }
来自《大话设计模式》例子
这个例子通过一个在烤肉店点餐的过程来展示命令模式的奇妙,首先我们看一下这个例子的UML,然后是代码:
图8.烤肉店示例的UML
using System; using System.Collections.Generic; using System.Text; namespace 命令模式 { class Program { static void Main(string[] args) { //开店前的准备 Barbecuer boy = new Barbecuer(); Command bakeMuttonCommand1 = new BakeMuttonCommand(boy); Command bakeMuttonCommand2 = new BakeMuttonCommand(boy); Command bakeChickenWingCommand1 = new BakeChickenWingCommand(boy); Waiter girl = new Waiter(); //开门营业 顾客点菜 girl.SetOrder(bakeMuttonCommand1); girl.SetOrder(bakeMuttonCommand2); girl.SetOrder(bakeChickenWingCommand1); //点菜完闭,通知厨房 girl.Notify(); Console.Read(); } } //服务员 public class Waiter { private IList<Command> orders = new List<Command>(); //设置订单 public void SetOrder(Command command) { if (command.ToString() == "命令模式.BakeChickenWingCommand") { Console.WriteLine("服务员:鸡翅没有了,请点别的烧烤。"); } else { orders.Add(command); Console.WriteLine("增加订单:" + command.ToString() + " 时间:" + DateTime.Now.ToString()); } } //取消订单 public void CancelOrder(Command command) { orders.Remove(command); Console.WriteLine("取消订单:" + command.ToString() + " 时间:" + DateTime.Now.ToString()); } //通知全部执行 public void Notify() { foreach (Command cmd in orders) { cmd.ExcuteCommand(); } } } //抽象命令 public abstract class Command { protected Barbecuer receiver; public Command(Barbecuer receiver) { this.receiver = receiver; } //执行命令 abstract public void ExcuteCommand(); } //烤羊肉串命令 class BakeMuttonCommand : Command { public BakeMuttonCommand(Barbecuer receiver) : base(receiver) { } public override void ExcuteCommand() { receiver.BakeMutton(); } } //烤鸡翅命令 class BakeChickenWingCommand : Command { public BakeChickenWingCommand(Barbecuer receiver) : base(receiver) { } public override void ExcuteCommand() { receiver.BakeChickenWing(); } } //烤肉串者 public class Barbecuer { public void BakeMutton() { Console.WriteLine("烤羊肉串!"); } public void BakeChickenWing() { Console.WriteLine("烤鸡翅!"); } } }
我看可以看到通过命令模式,客户与烤肉者实现了解耦合,提高了系统的健壮性和灵活性。
命令模式将发出请求的对象和执行请求的对象解耦。
在被解耦的两者之间是通过命令对象进行沟通的。命令对象封装了接收者和一个或一组动作。
调用者通过调用命令对象的execute()发出请求,这会使得接收者的动作被调用。
调用者可以接受命令当作参数,甚至在运行时动态地进行。
命令可以支持撤销,做法是实现一个undo()方法来回到execute()被执行前的状态。
宏命令是命令的一种简单的延伸,允许调用多个命令。宏方法也可以支持撤销。
实际操作时,很常见使用"聪明"命令对象,也就是直接实现了请求,而不是将工作委托给接收者。
命令也可以用来实现日志和事物系统。
接收方可以决定是是否否决命令。
通过将命令封装可以很容易的将命令写入日志等,也可以很容易地将命令形成一个命令队列,而且如果需要添加新的命令,对现有系统也几乎没有影响。
总结
Command模式是非常简单而又优雅的一种设计模式,它的根本目的在于将"行为请求者"与"行为实现者"解耦。