活用命令模式
在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计,使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活。
本文的目的
本文的目的要旨在于
- 了解Command Design Pattern
- 在实用示例中尽力利用.net的语言特色去优化Command Design Pattern令到我们更容易去使用它
- 从应用中理解Command Design Pattern能解决什么问题,避免为走进“模式而模式“的误区
据此,我们不会过多地讨论 Command Design Pattern的理论而关于命令模式的理论都引自于GOF。我们希望能从应用中吸收甚至升华模式中的精粹。
概述
在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计,使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活。
问题
在软件系统中,“行为请求者”与“行为实现者”通常呈现一种“紧耦合”。但在某些场合,比如要对行为进行“记录、撤销/重做、事务”等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将“行为请求者”与“行为实现者”解耦?
解决方案
命令模式(Command Pattern):将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式又称为动作(Action)模式或事务(Transaction)模式。
Command Pattern: Encapsulate a request as an object, thereby letting you parameterize clients with different requests,queue or log requests,and support undoable operations.
适用性
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互 - 使用命令模式作为"CallBack"在面向对象系统中的替代。"CallBack"讲的便是先将一个函数登记上,然后在以后调用此函数。
- 系统需要在不同的时间指定请求、将请求排队和执行请求 - 一个命令对象和原先的请求发出者可以有不同的生命期。换言之,原先的请求发出者可能已经不在了,而命令对象本身仍然是活动的。这时命令的接收者可以是在本地,也可以在网络的另外一个地址。命令对象可以在串形化之后传送到另外一台机器上去。
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作 - 命令对象可以把状态存储起来,等到客户端需要撤销命令所产生的效果时,可以调用undo()方法,把命令所产生的效果撤销掉。命令对象还可以提供redo()方法,以供客户端在需要时,再重新实施命令效果
- 系统需要将一组操作组合在一起,即支持宏命令。
- 需要在不同的时间指定请求、将请求排队
- 如果一个系统要将系统中所有的数据更新到日志里,以便在系统崩溃时,可以根据日志里读回所有的数据更新命令,重新调用Execute()方法一条一条执行这些命令,从而恢复系统在崩溃前所做的数据更新。
- 一个系统需要支持交易(Transaction)。一个交易结构封装了一组数据更新命令。使用命令模式来实现交易结构可以使系统增加新的交易类型。
优点和缺点
使用命令模式的优点和缺点
命令允许请求的一方和接收请求的一方能够独立演化,从而且有以下的优点:
- 降低系统的耦合度:Command模式将调用操作的对象与知道如何实现该操作的对象解耦。
- Command是头等的对象。它们可像其他的对象一样被操纵和扩展。
- 组合命令:你可将多个命令装配成一个组合命令,即可以比较容易地设计一个命令队列和宏命令。一般说来,组合命令是Composite模式的一个实例。
- 增加新的Command很容易,因为这无需改变已有的类。
- 可以方便地实现对请求的Undo和Redo。
命令模式的缺点如下:
使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个命令都需要设计一个具体命令类,因此某些系统可能需要大量具体命令类,这将影响命令模式的使用。
命令模式概述
Command Pattern模式
模式的组成
- 抽象命令类(Command): 声明执行操作的接口。调用接收者相应的操作,以实现执行的方法Execute。
- 具体命令类(ConcreteCommand): 创建一个具体命令对象并设定它的接收者。通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
- 调用者(Invoker): 要求该命令执行这个请求。通常会持有命令对象,可以持有很多的命令对象。
- 接收者(Receiver): 知道如何实施与执行一个请求相关的操作。任何类都可能作为一个接收者,只要它能够实现命令要求实现的相应功能。
- 客户类(Client): 创建具体的命令对象,并且设置命令对象的接收者。真正使用命令的客户端是从Invoker来触发执行
命令
命令 - Command
声明执行操作的接口。调用接收者相应的操作,以实现执行的方法Execute
Command接口定义
public interface ICommand { void Execute(object parameters=null); }
parameters - 可选参数,可作为执行命令时的参数或作为在命令间传递的上下文对象
ConcreteCommand
public abstract class CommandBase<TReceiver>:ICommand where TReceiver:class { public TReceiver Receiver{ get; private set;} public CommandBase(){ Receiver=null; } public CommandBase(TReceiver receiver) { this.Receiver=receiver; } public abstract void Execute(object parameters=null); }
错误处理 IErrorHandler
我们可以为Command对象增加IErrorHandler错误处理的接口,这样可以在多命令执行时当命令的执行出现异常可以及时阻断或记录异常信息。一般由 Invoker 调用
public interface IErrorHandler { void OnError(Exception e); }
撤销/回放接口 ICanUndo
public interface ICanUndo { void Undo(); }
实现支持Undo和错误处理的Command抽象类
public class Command<TReceiver>:ICommand,ICanUndo,IErrorHandler where TReceiver:class { protected object Receiver {get;private set;} public Command(){ this.Receiver=null; } public Command(TReceiver receiver) { this.Receiver=receiver; } public Command(TReceiver receiver,Action<object> executionHandler):this(receiver) { this.ExecutionHandler=executionHandler; } public Command(TReceiver receiver,Action<object> executionHandler,Action<Exception> errorHandler):this(receiver,executionHandler) { this.ErrorHandler=errorHandler; } public void Execute(object parameters=null) { if (ExecutionHandler==null) this.OnExecute(parameters); else this.ExecutionHandler(parameters); } public virtual void Undo(){} public void HandleError(Exception e) { if (ErrorHandler==null) this.OnError(e); else this.ErrorHandler(e); } public Action<object> ExecutionHandler {get;set;} public Action<Exception> ErrorHandler {get;set;} protected virtual void OnError(Exception e) {} protected virtual void OnExecute(object parameters=null){} }
在CommandBase类内我们加入了两个Action属性 ExecutionHandler 和 ErrorHandler 这样做的目的在于使命令对象能在实例化时可支持动态方法重载。这样做的好处在于可以从某程度上减少Command类,由其对于某些只起到解耦作用的简单Command只需要向Command注入方法体而不是添加一个类。
代码示例:动态Command
public void Main() { var receiver=new Order(); var dynamicCmd=new Command(receiver,(o)=>{ Receiver.State=1; }); ... }
如以上代码所示,此类Command实质上可能只有几行代码,这样的话就可以使用动态命令的形式,实例化Receiver和Command的同时注入 Execute 的方法代码。
执行者
执行者 - Invoker
要求该命令执行这个请求。通常会持有命令对象,可以持有很多的命令对象
Invoker 的实现决定了整个Command Design Pattern的应用方向,Invoker 对命令的执行方式将决定模式的整体行为,向Invoker增加不同的行为和改变命令集的执行方式就会产生多种不同的Invoker实现。如可以加入执行命令的过滤器就实现了“条件命令集”,增加可持久化的命令执行状态上下文则Invoker就会具有事务的特性,增加异步执行命令的行为就能使Invoker能执行长时间或持久运行的事务或命令等等。
从行为方式上我们将Invoker暂时划分为:命令队列,并行命令集、事务
在讲述执行器的分类之前我们可以先为执行器增加一个可选的通用功能:执行条件。执行条件由条件接口ICommandFilter和CommandCollection集成组成,便于在Invoker添加命令执行条件。
public interface ICommandFilter { bool ShouldExecute(ICommand command); } public class CommandFilterCollection:ICollection<ICommandFilter>,List<ICommandFilter> { public bool ShouldExecute(ICommand command) { foreach (var filter in Filters) { if (!filter.ShouldExecute(command)) return false; } return true; } }
宏
执行者实现:宏(命令序列) - Marco
特性:
- 有序的命令实例序列,命令需要按序执行
- 执行失败处理策略
- 回退
- 直接终止
- 强制忽略
- 可具有执行过滤条件
适用性:向导、宏代码执行器、安装程序
Marco
public class Macro { public Macro() { Commands=new List<Command>(); Filters=new CommandCollection(); } public ICollect<ICommand> Commands{get;private set;} public CommandFilterCollection Filters {get;private set;} public Exception LastError { get;private set;} public virtual void Run() { foreach (var cmd in Commands) { if (Filters.ShouldExecute(cmd)) cmd.Execute(); } } public void OnError(Action<Exception> act) { if (act!=null) { act(this.LastError); } } public void Add(param ICommand[] cmd){ Commands.AddRange(cmd); } public Remove(ICommand cmd) { Commands.Remove(cmd); } }
Client
public void Main() { var cmd1=new Command1(); var cmd2=new Command2(); var cmd3=new Command3(); var macro=new Macro(); macro.Add(cmd1,cmd2,cmd3); macro.OnError(e=>{ console.Write(e.Message); }); macro.Run(); }
指令表
执行者实现:指令表 - CommandTable
特性:用命令字符串调用命令集
适用性:命令行,重组已有方法
public void Main() { var delExeCmd=new Command(null,(o)=>{ //Do delete exe file that file name starts with o.startsWith here }); var delBatCmd=new Command(null,(o)=>{ //Do delete bat file that file name starts with o.startsWith here }); var delJpgCmd=new Command(null,(o)=>{ // Do delete jpg file that file name starts with o.startsWith here. }); var cmdTable=new CommandTable(); cmdTable.Add("clean", delExeCmd,delBatCmd); cmdTable.Run("clean",new {startsWith=new string[]{"AB","EF"}}); }
由此范例我们可以推导出Add和Run这两个方法,且从行为上看,CommandTable实质就是一组用名称重组的宏命令集,换言之 CommandTable 是Macro的集合式应用
public class CommandTable { public Dictionary<string,Macro> CommandSet{get;set;} public void Add(string commandName,params ICommand[] commands) { var macro=new Macro(); macro.Add(commands); this.CommandSet.Add(commandName,macro); } public void Run(string commandName,object parameters=null) { var macro=this.CommandSet[commandName]; macro.Run(parameters); } }
命令队列
执行者实现:命令队列 CommandQueue
特性:
- 按先进先出的队列规则执行
- 可Undo到某一指定位置
public void Main() { // Initialize cmd1 - cmdn ... var cmdQueue=new CommandQueue(); cmd.Enqueue(cmd1); ... cmd.Enqueue(cmdn); //Dequeue command and execute; cmdQueue.Dequeue(); //We can add Run method to execute all queue commands. //cmdQueue.Run(); //From index 5 upto n will undo cmdQueue.Undo(5); }
事务
执行者实现:事务 - Transaction
特性:
- 支持异步命令的执行
- 具有事务执行上下文
- 通过上下文恢复命令实例和执行状态
适用性:工作流
由于事务对象实出比较复杂化,此处我们将会通一个完整的示例演示一个基于的从购物车付款的事务流程。
域对象:
public class Product { public int ID {get;set;} public string Name {get;set;} public decimal Price {get;set;} } public class OrderItem { public int ID { get;set; } public int Quantity { get;set; } public Product Product{ get;set; } public decimal Total { get;set; } } public class ShoppingCart { public ICollect<OrderItem> Items{ get;set;} public Order Checkout() { // Impletement checkout code ... } } public class Order { public int ID {get;set;} public string Number {get;set;} public DateTime OrderDate {get;set;} public ICollect<OrderItem> Items{ get;set;} }
命令实现
public class CheckOutCommand:Command<ShoppingCart> { private OrderTranscationContext Context{get;private set;} public CheckOutCommand(ShoppingCart cart,OrderTranscationContext ctx):base(cart) { this.Context=ctx; } public override void Execute(object parameters=null) { ctx.Order= Receiver.Checkout(); } } public class ProcessPaymentCommand:Command<OrderTranscationContext> { public override void Execute(object parameters=null) { //Implement:Process with payment gateway Receiver.State=TranscationStates.Paid; } } public class AddPaymentCommand:Command<OrderTranscationContext> { public override void Execute(object parameters=null) { // Implement:Add payment to order ... Receiver.State=TranscationStates.Paid; } }
事务对象
public class TranscationContext { public Guid TranID { get;set;} public dynamic Data { get;set;} } public class OrderTranscationContext:TranscationContext { public Order Order{get;set;} public TranscationStates State {get;set;} } public enum TranscationStates { Pendding=1, Paid=2, Completed=3, } public Transcation { public List<ICommand> Commands{get;set;} public void Add(params ICommand[] cmds) { Commands.AddRange(cmds);} public async Start(object parameters=null) { foreach (var cmd in Commands) { try { await cmd.Execute(parameters); } catch(Exception e) { var _errorHanlder=cmd as IErorrHandler; if (_errorHandler!=null) _errorHandler.OnError(e); } } } // Undo all commands public void Rollback() { foreach (var cmd in Commands) { var undoCmd=cmd as ICanUndo; if (undoCmd!=null) undoCmd.Undo(); } } }
Client
public void Main() { var cart=new ShoppingCart(); var rep=new TransRepository(); var tranCtx=new OrderTranscationContext(); var checkOutCmd=new CheckOutCommand(cart,tranCtx); var processPaymentCmd=new ProcessPaymentCommand(tranCtx); var addPaymentCmd=new new AddPaymentCommand(tranCtx); var orderTran=new Transcation(); tran.Add(checkOutCmd,processPaymentCmd,addPaymentCmd); //将上下文状态存数据库内 rep.Add(tranCtx); rep.Submit(); tran.Start(); tran.OnStateChanged(s=>{ rep.Update(tranCtx); rep.Submit(); }); }
收接者
收接者 - Receiver
知道如何实施与执行一个请求相关的操作。任何类都可能作为一个接收者,只要它能够实现命令要求实现的相应功能。
对 Receiver 的其它用法:
- 当Command 的Execute 方法内调用Receiver上的方法时,Receiver则被用作回调对象
- 在Command 内获取/设置 Receiver 的属性值则是将Receiver作为上下文使用。
心得:由于Receiver被Command解耦,Invoker完全不知道Receiver的类型细节,因此Receiver并无需一定要从某一接口继承,他可以是任意的类,也可作为Invoker的外部状态对象使用。
除了使用已定义的类作为Receiver,在.net中我们还可以利用.net的动态方法与动态类型编写动态Receiver,以下的范例就演示如何制作动态Receiver
public void Main() { dynamic dynamicReceiver=new { State=0, Callback=new Action({ Console.Write("Receiver call back"); }); }; var dynamicCmd=new Command(dynamicReceiver,(o)=>{ Receiver.State=1; Receiver.Callback(); }); }
这种运用可以适用在装配Invoker的命令(集)序列时,也就是 Client端,也可以将其详细封装成为特定的Invoker或Factory 方法,这样可以令到代码更为之简洁清晰。同时也可以减少类的数量。
相关信息
这篇文章中的代码有很多是为了说明问题而写的伪代码(运行可能有问题^_^),为了能让大家都得到方便,我将命令模式制作成了一个自由类库发布在NuGet上,有兴趣的朋友可以访问 https://www.nuget.org/packages/DNA.Patterns/ 下载或在VS.NET的NUGet 管理器内直接键入以下命令安装
PM> Install-Package DNA.Patterns