设计模式之命令模式(十三)
一、引出模式
在前面的模式中,我们已经组装好了一台电脑,现在要做的是开机。是的,开机!对用户们来说开机只不过是按个电源按钮,跟喝水一样简单!但对于我们搞技术的,就不一样了,可这其中又发生了什么不为人知的事呢?自己百度去!
我们先简单将以下流程,不做深入讲解。
首先加载电源,然后设备自检,接下来装在操作系统,最后电脑就启动了。可是谁来完成这些过程?如何完成的呢?
总不能让用户做这些吧,其实真正完成这些功能的是主板。那客户和主板又是怎么联系的呢?现实中,使用连接线将按钮连接到主板上,这样当用户按下按钮时,就相当与发命令给主板,让主板去完成后续工作。
想想,在这里有没有什么问题?
我们把这种情形放到软件开发中看看。客户端只是想要发出命令,不关心命令的执行者是谁,也不关心执行者是怎么完成的,有时同一个请求可能需要执行不同的操作,那怎么办?
二、认识模式
1.模式定义
将请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求进行排队或记录请求日志,以及支持可撤销的操作。
2.解决思路
我们来试着用命令模式解决上述开机的过程。
当客户按下按钮时,按钮本身并不知道如何处理,于是我们通过连接线,将按钮和主板连接起来,让主板去完成真正启动机器的功能。
在这里,我们通过引入按钮和连接线,来让发出命令的客户和命令的真正实现者——主板完全解耦,客户操作的始终是按钮,按钮后面的事客户就不管了,因为客户只知道只要按下按钮就能开机了,中间做了什么客户是不关心的,客户只求结果,不问过程。
在命令模式中,会定义一个命令的接口,用来约束所有的命令对象,然后提供具体的命令实现,每个命令实现对象是对客户端某个请求的封装,对应于机箱上的按钮,一个机箱可以有很多按钮,也就相当于有很多个具体的命令实现对象。
在命令模式中,命令对象是不知道如何处理命令的,他会转调命令接受者对象来真正执行命令。就像刚才例子中,按钮是不知道如何处理的,按钮是吧这个请求转发给主板,主板来执行,这个主板就相当于命令模式汇总的接受者。
在命令模式中,命令对象和接收者的关系并不是与生俱来的,需要有一个装配者对两者进行关联,命令模式中的Client对象可以实现这样的功能,在电脑中,有了按钮,有了主板,那还需要有根连接线将按钮和主板连接起来才行,这根连接线就充当着Client对象的角色。
命令模式中,还会提供一个Invoker对象来持有命令对象。比如,机箱上会有多个按钮,这个机箱就相当于Invoker对象,这样我们客户就可以通过Invoker也就是机箱来按下按钮来执行相应的命令。
3.模式结构原型
Command:定义命令的接口,声明执行的方法。
ConreteCommand:命令接口实现对象,是“虚”的实现;通常会持有接受者,并调用接受者的功能来完成命令要执行的操作。
Receiver:接受者,真正执行命令的对象。任何类都能成为接受者,只要它能够实现命令要求实现的相应的功能。
Invoker:要求命令对象执行请求,通常会持有命苦对象,可以持有很多命令对象。这个是客户端真正触发命令并要求执行相应操作的定法,也就是说这才是使用命令对象的入口。
Client:创建具体的命令对象,并且设置命令对象的接受者。注意这个不是我们常规意义上的客户端,而是在组装命令对象和接受者,可以将它称为装配者,真正使用命令的客户端是从Invoker来触发执行的。
4.模式原型示例代码
class Program { static void Main(string[] args) { Client client = new Client(); client.Run(); Console.ReadKey(); } } /// <summary> /// 示意,负责创建命令对象,并设定它的接受者 /// </summary> public class Client { public void Run() { Receiver receiver = new Receiver(); //创建命令对象,设定它的接收者 Command command = new ConcreteCommand(receiver); //创建Invoker,把命令对象设置进去 Invoker invoker = new Invoker(command); invoker.RunCommand(); } } /// <summary> /// 命令接口,声明执行的操作 /// </summary> public interface Command { /// <summary> /// 执行命令对应的操作 /// </summary> void Execute(); } /// <summary> /// 具体的命令实现对象 /// </summary> public class ConcreteCommand : Command { /// <summary> /// 持有相应的接受者对象 /// </summary> private Receiver receiver = null; /// <summary> /// 示意,命令对象可以有自己的状态 /// </summary> private string state = null; /// <summary> /// 构造方法,传入相应的接受者对象 /// </summary> /// <param name="receiver">相应的接受者对象</param> public ConcreteCommand(Receiver receiver) { this.receiver = receiver; } public void Execute() { //通常会转调接受者对象的相应方法,让接受者来真正执行功能 receiver.Action(); } } /// <summary> /// 接收者对象 /// </summary> public class Receiver { /// <summary> /// 示意方法,真正执行命令相应的操作 /// </summary> public void Action() { Console.WriteLine("命令执行了"); } } /// <summary> /// 调用者(机箱) /// </summary> public class Invoker { /// <summary> /// 持有命令对象 /// </summary> private Command command; /// <summary> /// 设置调用者持有的命令对象 /// </summary> /// <param name="command">命令对象</param> public Invoker(Command command) { this.command = command; } /// <summary> /// 示意方法,要求命令执行请求 /// </summary> public void RunCommand() { command.Execute(); } }
5.电脑开机示例示例代码
class Program { static void Main(string[] args) { //把命令和真正的实现组合起来,相当于在组装机器, MainBoardApi mainBoardApi=new GigaMainBoard(); //把机箱上按钮的连接线插接到主板上。 Command command=new OpenCommand(mainBoardApi); //真正的客户端测试 //为机箱上的按钮设置对应的命令,让按钮知道该干什么 Box box=new Box(); box.SetOpenCommand(command); //然后模拟按下机箱上的按钮 box.OpenButtonPressed(); Console.Read(); } } /// <summary> /// 命令接口,声明执行的操作 /// </summary> public interface Command { /// <summary> /// 执行命令对应的操作 /// </summary> void Execute(); } /// <summary> /// 持有开机命令的真正实现,通过调用接收者的方法来实现命令 /// </summary> public class OpenCommand : Command { /// <summary> /// 持有真正实现命令的接收者——主板对象 /// </summary> private MainBoardApi mainBoard = null; /// <summary> /// 构造方法,传入主板对象 /// </summary> /// <param name="mainBoard">主板对象</param> public OpenCommand(MainBoardApi mainBoard) { this.mainBoard = mainBoard; } public void Execute() { //对于命令对象,根本不知道如何开机,会转调主板对象 //让主板去完成开机的功能 this.mainBoard.Open(); } } /// <summary> /// 主板的接口 /// </summary> public interface MainBoardApi { /// <summary> /// 主板具有能开机的功能 /// </summary> void Open(); } /// <summary> /// 技嘉主板类,开机命令的真正实现者,在Command模式中充当Receiver /// </summary> public class GigaMainBoard : MainBoardApi { /// <summary> /// 真正的开机命令的实现 /// </summary> public void Open() { Console.WriteLine("技嘉主板现在正在开机,请等候"); Console.WriteLine("接通电源......"); Console.WriteLine("设备检查......"); Console.WriteLine("装载系统......"); Console.WriteLine("机器正常运转起来......"); Console.WriteLine("机器已经正常打开,请等候"); } } /// <summary> /// 微星主板类,开机命令的真正实现者,在Command模式中充当Receiver /// </summary> public class MsiMainBoard : MainBoardApi { /// <summary> /// 真正的开机命令的实现 /// </summary> public void Open() { Console.WriteLine("微星主板现在正在开机,请等候"); Console.WriteLine("接通电源......"); Console.WriteLine("设备检查......"); Console.WriteLine("装载系统......"); Console.WriteLine("机器正常运转起来......"); Console.WriteLine("机器已经正常打开,请等候"); } } /// <summary> /// 机箱对象,本身有按钮,持有按钮对应的命令对象 /// </summary> public class Box { /// <summary> /// 开机命令对象 /// </summary> private Command openCommand; /// <summary> /// 设置开机命令对象 /// </summary> /// <param name="command">开机命令对象</param> public void SetOpenCommand(Command command) { this.openCommand = command; } /// <summary> /// 提供给客户使用,接受并相应用户请求,相当于按钮被按下触发的方法 /// </summary> public void OpenButtonPressed() { //按下按钮,执行命令 openCommand.Execute(); } }
三、理解模式
1.命令模式的关键
命令模式的关键之处就是把请求封装称为对象,也就是命令对象,并定义统一的执行操作的接口,这个命令对象被存储、转发、记录、处理、撤销等,整个命令模式都是围绕这个对象进行的。
2.命令模式的组装和调用
命令模式中经常会有一个命令的装配者,用它来维护命令的“虚”实现和真实实现之间的关系。如果是超级智能的命令,也就是说命令对自己完全实现好了,不需要接受者,那就是命令模式的退化,不需要接受者,自然也不需要装配者。
实际开发中,Client和Invoker是可以融合在一起的,有客户在使用命令模式的时候,先进行命令对象和接受者的组装,组装完成后,就可以调用命令执行请求。
3.命令的接受者
接受者是可以是任意的类,只要这个对象知道如何真正的执行命令,执行时是从Command的实现类里面转调过来的。
一个接受者对象可以处理多个命令对象,接受者和命令之间没有约定的对象关系。
4.智能命令
在标准的命令模式中,命令的实现类是没有真正实现命令要求的功能的,真正执行命令的是接受者。
如果命令的实现对象比较智能,自己就能实现命令要求的功能,就不需要调用接受者,这种情况称为智能命令。
5.发起请求的对象和真正处理的对象是解耦的
请求有谁来处理?如何处理?发起请求的对象是不知道的,也就是发情请求的对象和真正实现的对象是解耦的。
6.参数化配置
所谓的命令模式的参数化配置,指的是:可以用不同的命令对象,去参数化配置客户端的请求。
如前面的表述,按下按钮你是不知道是开机,关机还是重启的,那就要看参数化配置是哪一个具体的按钮对象。
示例代码:
class Program { static void Main(string[] args) { //把命令和真正的实现组合起来,相当于在组装机器, MainBoardApi mainBoardApi = new GigaMainBoard(); //创建开机命令 Command command = new OpenCommand(mainBoardApi); //创建重启机器的命令 ResetCommand resetCommand = new ResetCommand(mainBoardApi); //真正的客户端测试 //为机箱上的按钮设置对应的命令,让按钮知道该干什么 Box box = new Box(); //先正确配置,就是开机按钮对开机命令,重启按钮对重启命令 box.SetOpenCommand(command); box.SetResetCommand(resetCommand); //然后模拟按下机箱上的按钮 Console.WriteLine("正确配置下------------------------->"); Console.WriteLine(">>>按下开机按钮:>>>"); box.OpenButtonPressed(); Console.WriteLine(">>>按下重启按钮:>>>"); box.ResetButtonPressed(); Console.Read(); } } /// <summary> /// 命令接口,声明执行的操作 /// </summary> public interface Command { /// <summary> /// 执行命令对应的操作 /// </summary> void Execute(); } /// <summary> /// 持有开机命令的真正实现,通过调用接收者的方法来实现命令 /// </summary> public class OpenCommand : Command { /// <summary> /// 持有真正实现命令的接收者——主板对象 /// </summary> private MainBoardApi mainBoard = null; /// <summary> /// 构造方法,传入主板对象 /// </summary> /// <param name="mainBoard">主板对象</param> public OpenCommand(MainBoardApi mainBoard) { this.mainBoard = mainBoard; } public void Execute() { //对于命令对象,根本不知道如何开机,会转调主板对象 //让主板去完成开机的功能 this.mainBoard.Open(); } } /// <summary> /// 重启机器命令的实现,实现Command接口, /// 持有重启机器命令的真正实现,通过调用接收者的方法来实现命令 /// </summary> public class ResetCommand : Command { /// <summary> /// 持有真正实现命令的接收者——主板对象 /// </summary> private MainBoardApi mainBoard = null; /// <summary> /// 构造方法,传入主板对象 /// </summary> /// <param name="mainBoard">主板对象</param> public ResetCommand(MainBoardApi mainBoard) { this.mainBoard = mainBoard; } public void Execute() { //对于命令对象,根本不知道如何重启机器,会转调主板对象 //让主板去完成重启机器的功能 this.mainBoard.Reset(); } } /// <summary> /// 主板的接口 /// </summary> public interface MainBoardApi { /// <summary> /// 主板具有能开机的功能 /// </summary> void Open(); /// <summary> /// 主板具有实现重启的功能 /// </summary> void Reset(); } /// <summary> /// 技嘉主板类,开机命令的真正实现者,在Command模式中充当Receiver /// </summary> public class GigaMainBoard : MainBoardApi { /// <summary> /// 真正的开机命令的实现 /// </summary> public void Open() { Console.WriteLine("技嘉主板现在正在开机,请等候"); Console.WriteLine("接通电源......"); Console.WriteLine("设备检查......"); Console.WriteLine("装载系统......"); Console.WriteLine("机器正常运转起来......"); Console.WriteLine("机器已经正常打开,请等候"); } /// <summary> /// 真正的重新启动机器命令的实现 /// </summary> public void Reset() { Console.WriteLine("微星主板现在正在重新启动机器,请等候"); Console.WriteLine("机器已经正常打开,请等候"); } } /// <summary> /// 微星主板类,开机命令的真正实现者,在Command模式中充当Receiver /// </summary> public class MsiMainBoard : MainBoardApi { /// <summary> /// 真正的开机命令的实现 /// </summary> public void Open() { Console.WriteLine("微星主板现在正在开机,请等候"); Console.WriteLine("接通电源......"); Console.WriteLine("设备检查......"); Console.WriteLine("装载系统......"); Console.WriteLine("机器正常运转起来......"); Console.WriteLine("机器已经正常打开,请等候"); } /// <summary> /// 真正的重新启动机器命令的实现 /// </summary> public void Reset() { Console.WriteLine("微星主板现在正在重新启动机器,请等候"); Console.WriteLine("机器已经正常打开,请等候"); } } /// <summary> /// 机箱对象,本身有按钮,持有按钮对应的命令对象 /// </summary> public class Box { /// <summary> /// 开机命令对象 /// </summary> private Command openCommand; /// <summary> /// 设置开机命令对象 /// </summary> /// <param name="command">开机命令对象</param> public void SetOpenCommand(Command command) { this.openCommand = command; } /// <summary> /// 提供给客户使用,接受并相应用户请求,相当于按钮被按下触发的方法 /// </summary> public void OpenButtonPressed() { //按下按钮,执行命令 openCommand.Execute(); } /// <summary> /// 重启机器命令对象 /// </summary> private Command resetCommand; /// <summary> /// 设置重启机器命令对象 /// </summary> /// <param name="command"></param> public void SetResetCommand(Command command) { this.resetCommand = command; } /// <summary> /// 提供给客户使用,接受并相应用户请求,相当于重启按钮被按下触发的方法 /// </summary> public void ResetButtonPressed() { //按下按钮,执行命令 resetCommand.Execute(); } }
7.可撤销的操作
可撤销的操作意思是:放弃该操作,回到未执行操作前的状态。
有两种基本的思路来实现可撤销的操作,一种是补偿式又称反操作式,比如被撤销的操作是+,那撤销的操作就是-。
另一种是存储恢复式,就是把操作前的状态记录下来,然后要撤销操作时直接恢复回去。
在这里我们演示第一种可撤销操作,剩下一种等到备忘录模式时在讲。
做一个计算机功能,只需要实现加减运算,还要让这个计算器支持可撤销的
示例代码:
class Program { static void Main(string[] args) { //1:组装命令和接收者 //创建接收者 OperationApi operation = new Operation(); //创建命令对象,并组装命令和接收者 AddCommand addCmd = new AddCommand(operation, 5); SubCommand substractCmd = new SubCommand(operation, 3); //2:把命令设置到持有者,就是计算器里面 Calculator calculator = new Calculator(); calculator.SetAddCommand(addCmd); calculator.SetSubCommand(substractCmd); //3:模拟按下按钮,测试一下 calculator.AddPressed(); Console.WriteLine("一次加法运算后的结果为:" + operation.GetResult()); calculator.SubPressed(); Console.WriteLine("一次减法运算后的结果为:" + operation.GetResult()); //测试撤消 calculator.UndoPressed(); Console.WriteLine("撤销一次后的结果为:" + operation.GetResult()); calculator.UndoPressed(); Console.WriteLine("再撤销一次后的结果为:" + operation.GetResult()); //测试恢复 calculator.RedoPressed(); Console.WriteLine("恢复操作一次后的结果为:" + operation.GetResult()); calculator.RedoPressed(); Console.WriteLine("再恢复操作一次后的结果为:" + operation.GetResult()); Console.Read(); } } /// <summary> /// 命令接口,声明执行的操作,支持可撤销操作 /// </summary> public interface Command { /// <summary> /// 执行命令对应的操作 /// </summary> void Execute(); /// <summary> /// 执行撤销命令对应的操作 /// </summary> void Undo(); } /// <summary> /// 具体的加法命令实现对象 /// </summary> public class AddCommand : Command { /// <summary> /// 持有具体执行计算的对象 /// </summary> private OperationApi operationApi = null; /// <summary> /// 操作的数据,也就是要加上的数据 /// </summary> private int num; /// <summary> /// 构造方法,传入具体执行计算的对象 /// </summary> /// <param name="operationApi"></param> /// <param name="num"></param> public AddCommand(OperationApi operationApi, int num) { this.operationApi = operationApi; this.num = num; } public void Execute() { ////转调接收者去真正执行功能,这个命令是做加法 operationApi.Add(num); } public void Undo() { //转调接收者去真正执行功能 //命令本身是做加法,那么撤销的时候就是做减法了 operationApi.Sub(num); } } /// <summary> /// 具体的减法命令实现对象 /// </summary> public class SubCommand : Command { /// <summary> /// 持有具体执行计算的对象 /// </summary> private OperationApi operationApi = null; /// <summary> /// 操作的数据,也就是要加上的数据 /// </summary> private int num; /// <summary> /// 构造方法,传入具体执行计算的对象 /// </summary> /// <param name="operationApi"></param> /// <param name="num"></param> public SubCommand(OperationApi operationApi, int num) { this.operationApi = operationApi; this.num = num; } public void Execute() { //转调接收者去真正执行功能,这个命令是做减法 operationApi.Sub(num); } public void Undo() { //转调接收者去真正执行功能 //命令本身是做减法,那么撤销的时候就是做加法了 operationApi.Add(num); } } /// <summary> /// 操作运算的接口 /// </summary> public interface OperationApi { /// <summary> /// 获取计算完成后的结果 /// </summary> /// <returns></returns> int GetResult(); /// <summary> /// 设置计算开始的初始值 /// </summary> /// <param name="result"></param> void SetResult(int result); /// <summary> /// 执行加法 /// </summary> /// <param name="num"></param> void Add(int num); /// <summary> /// 执行减法 /// </summary> /// <param name="num"></param> void Sub(int num); } /// <summary> /// 运算类,真正实现加减法运算 /// </summary> public class Operation : OperationApi { /// <summary> /// 记录运算的结果 /// </summary> private int result; public int GetResult() { return result; } /// <summary> /// 设置值 /// </summary> /// <param name="result"></param> public void SetResult(int result) { this.result = result; } /// <summary> /// 实现加法功能 /// </summary> /// <param name="num"></param> public void Add(int num) { result += num; } /// <summary> /// 实现减法功能 /// </summary> /// <param name="num"></param> public void Sub(int num) { result -= num; } } /// <summary> /// 计算器类,计算器上有加法按钮、减法按钮,还有撤销和恢复的按钮 /// </summary> public class Calculator { /// <summary> /// 命令的操作的历史记录,在撤销时候用 /// </summary> private List<Command> undoCmds = new List<Command>(); /// <summary> /// 命令被撤销的历史记录,在恢复时候用 /// </summary> private List<Command> redoCmds = new List<Command>(); /// <summary> /// 持有执行加法的命令对象 /// </summary> private Command addCommand = null; /// <summary> /// 持有执行减法的命令对象 /// </summary> private Command subCommand = null; /// <summary> /// 设置执行加法的命令对象 /// </summary> /// <param name="addCommand"></param> public void SetAddCommand(Command addCommand) { this.addCommand = addCommand; } /// <summary> /// 设置执行减法的命令对象 /// </summary> /// <param name="subCommand"></param> public void SetSubCommand(Command subCommand) { this.subCommand = subCommand; } /// <summary> /// 加法按钮 /// </summary> public void AddPressed() { this.addCommand.Execute(); //把操作记录到历史记录里面 undoCmds.Add(this.addCommand); } /// <summary> /// 减法按钮 /// </summary> public void SubPressed() { this.subCommand.Execute(); //把操作记录到历史记录里面 undoCmds.Add(this.subCommand); } /// <summary> /// 撤销按钮 /// </summary> public void UndoPressed() { if (this.undoCmds.Count > 0) { //取出最后一个命令来撤销 Command cmd = this.undoCmds.Last(); cmd.Undo(); //如果还有恢复的功能,那就把这个命令记录到恢复的历史记录里面 this.redoCmds.Add(cmd); //然后把最后一个命令删除掉, this.undoCmds.Remove(cmd); } else { Console.WriteLine("很抱歉,没有可撤销的命令"); } } /// <summary> /// 恢复按钮 /// </summary> public void RedoPressed() { if (this.redoCmds.Count > 0) { //取出最后一个命令来重做 Command cmd = this.redoCmds.Last(); cmd.Execute(); //把这个命令记录到可撤销的历史记录里面 this.undoCmds.Add(cmd); //然后把最后一个命令删除掉 this.redoCmds.Remove(cmd); } else { Console.WriteLine("很抱歉,没有可恢复的命令"); } } }
8.宏命令
宏命令就是包含多个命令的命令,是一个命令的组合。命令命令模式也是能实现的。
9.队列请求
所谓队列请求,就是对命令对象进行排队,组成工作队列,然后一次取出命令对象来执行。
10.日志请求
日志请求,就是将请求的历史记录保存下来,一般是采用永久存储的方式。如果运行请求过程中,系统崩溃了,那么当系统再次运行时,就可以从保存的历史记录中获取日志请求,并重新执行命令。
11.命令模式的优点
更松散的耦合
命令模式使得发起命令的对象——客户端,和命令的执行者对象完全解耦。
更动态的控制
命令模式将请求封装起来,可以动态地对它进行参数化、队列化和日志化等操作,使得系统更加灵活。
更自然的复合命令
命令模式中的命令对象能够很容易的组合成符合命令,如前面的宏命令。
12.何时选用命令模式
如果需要抽象出需要执行的动作,并参数化这些对象,可以使用命令模式。将这些需要执行的动作抽象成为命令,然后实现命令的参数化配置。
如果需要在不同的时刻指定、排列和执行请求,可以选用命令模式。
如果需要支持取消操作,可以选用命令模式,通过管理命令对象,很容易实现命令的恢复和重做功能。
如果需要支持系统崩溃时,重启后能将系统的操作功能重新执行一遍,可以选用命令模式。
在需要事务的系统中,可以选用命令模式。
13.命令模式的本质
命令模式的本质就是“封装请求”。命令模式的关键就是把请求封装称为命令对象,然后就可以对这个对象进行一系列的处理。