设计模式的征途—19.命令(Command)模式
在生活中,我们装修新房的最后几道工序之一是安装插座和开关,通过开关可以控制一些电器的打开和关闭,例如电灯或换气扇。在购买开关时,用户并不知道它将来到底用于控制什么电器,也就是说,开关与电灯、换气扇并无直接关系,一个开关在安装之后可能用来控制电灯,也可能用来控制换气扇或者其他电器设备。相同的开关可以通过不同的电线来控制不同的电器,如下图所示。
在软件开发中也存在很多与开关和电器类似的请求发送者和接受者对象,例如一个按钮,它可能是一个“关闭窗口”请求的发送者,而按钮点击事件处理类则是该请求的接受者。为了降低系统的耦合度,将请求的发送者和接收者解耦,可以使用一种被称为命令模式的设计模式来设计系统。
命令模式(Command) | 学习难度:★★★☆☆ | 使用频率:★★★★☆ |
一、自定义功能按键的设计
1.1 需求背景
M公司开发人员为公司内部OA系统开发了一个桌面版应用程序,该应用程序为用户提供了一系列自定义功能键,用户可以通过这些功能键来实现一些快捷操作。M公司开发人员通过分析,发现不同的用户可能会有不同的使用习惯,在设置功能键的时候每个人都有自己的喜好,例如有的人喜欢将第一个功能键设置为“打开帮助文档”,有的人则喜欢将该功能键设置为“最小化至托盘”。为了让用户能够灵活地进行功能键的设置,开发人员提供了一个“功能键设置”窗口,如下图所示。
通过上图的界面,用户就可以将功能键和相应功能绑定在一起,还可以根据需求来修改功能键的设置,而且系统在未来可能还会增加一些新的功能或功能键。
1.2 初始设计
M公司开发人员打算使用如下code来实现功能键与功能处理类之间的调用关系:
public class FunctionButton { private HelpHandler handler; public void OnClick() { handler = new HelpHandler(); handler.Display(); } }
在上述代码中,功能按键类FunctionButton充当请求的发送者,帮助文档处理类HelpHandler则充当请求的接收者,在发送者FunctionButton的OnClick()方法中将调用接收者HelpHandler的Display()方法。显然,如果直接使用上述代码,将会有以下几个问题:
(1)请求发送者和请求接收者存在直接调用 => 耦合度太高!更换请求接收者必须修改发送者的源代码!
(2)FunctionButton类在设计和实现时功能已被固定 => 增加新的请求接收者要么修改FunctionButton类要么新增一个新的请求接收者类
(3)用户无法按照自己的需要来设置某个功能键的功能 => 无法在修改源代码情况下更换功能,缺乏灵活性!
二、命令模式概述
2.1 命令模式简介
命令(Command)模式:将一个请求封装为一个对象,从而可以用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事物(Transaction)模式。
2.2 命令模式结构
命令模式的核心在于引入了命令类,通过命令类来降低请求发送者和接收者的耦合度,请求发送者只需要指定一个命令对象,再通过命令对象来调用请求接收者的处理方法,其结构如下图所示。
其中,包含以下几个角色:
(1)Command(抽象命令类):一个抽象类或接口,声明了执行请求的Execute()方法,通过这些方法可以调用请求接收者的相关操作。
(2)ConcreteCommand(具体命令类):具体命令类是抽象命令类的子类,实现了抽象命令类中声明的方法。在实现Execute()方法时,将调用接收者对象的相关操作(Action)。
(3)Invoker(调用者):请求发送者,通过命令对象来执行请求。
(4)Receiver(接收者):接收者执行与请求相关的操作,它具体实现对请求的业务处理。
命令模式的本质在于:对请求进行封装,一个请求对应一个命令,将发出命令的责任和执行命令的责任分割开,使得请求的一方不必了解接收请求的一方的接口,更不必知道请求如何被接收、操作是否被执行、何时被执行,以及是怎么被执行的。
三、重构自定义功能键的设计
3.1 重构后的设计
其中,FBSettingWindow是“功能键设置”界面类,FunctionButton充当调用者,Command充当抽象命令类,MinimizeCommand、HelpCommand充当具体命令类,WindowHandler和HelpHandler充当请求接收者。
3.2 具体代码实现
(1)功能键设置窗口
/// <summary> /// 功能键设置窗口类 /// </summary> public class FBSettingWindow { // 窗口标题 public string Title { get; set; } // 所有功能键集合 private IList<FunctionButton> functionButtonList = new List<FunctionButton>(); public FBSettingWindow(string title) { this.Title = title; } public void AddFunctionButton(FunctionButton fb) { functionButtonList.Add(fb); } public void RemoveFunctionButton(FunctionButton fb) { functionButtonList.Remove(fb); } // 显示窗口及功能键 public void Display() { Console.WriteLine("显示窗口:{0}", this.Title); Console.WriteLine("显示功能键:"); foreach (var fb in functionButtonList) { Console.WriteLine(fb.Name); } Console.WriteLine("------------------------------------------"); } }
(2)请求发送者:FunctionButton
/// <summary> /// 请求发送者:功能键 /// </summary> public class FunctionButton { // 功能键名称 public string Name { get; set; } // 维持一个抽象命令对象的引用 private Command command; public FunctionButton(string name) { this.Name = name; } // 为功能键注入命令 public void SetCommand(Command command) { this.command = command; } // 发送请求的方法 public void OnClick() { Console.WriteLine("点击功能键:"); if (command != null) { command.Execute(); } } }
(3)抽象命令类:Command
/// <summary> /// 抽象命令类 /// </summary> public abstract class Command { public abstract void Execute(); }
(4)具体命令类:HelpCommand与MinimizeCommand
/// <summary> /// 具体命令类:帮助命令 /// </summary> public class HelpCommand : Command { private HelpHandler hander; public HelpCommand() { hander = new HelpHandler(); } // 命令执行方法,将调用请求接受者的业务方法 public override void Execute() { if (hander != null) { hander.Display(); } } } /// <summary> /// 具体命令类:最小化命令 /// </summary> public class MinimizeCommand : Command { private WindowHandler handler; public MinimizeCommand() { handler = new WindowHandler(); } // 命令执行方法,将调用请求接受者的业务方法 public override void Execute() { if (handler != null) { handler.Minimize(); } } }
(5)请求接收者:WindowHandler和HelpHandler
/// <summary> /// 请求接受者:帮助文档处理类 /// </summary> public class WindowHandler { public void Minimize() { Console.WriteLine("正在最小化窗口至托盘..."); } } /// <summary> /// 请求接受者:帮助文档处理类 /// </summary> public class HelpHandler { public void Display() { Console.WriteLine("正在显示帮助文档..."); } }
(6)客户端测试
public class Program { public static void Main(string[] args) { // Step1.模拟显示功能键设置窗口 FBSettingWindow window = new FBSettingWindow("功能键设置窗口"); // Step2.假如目前要设置两个功能键 FunctionButton buttonA = new FunctionButton("功能键A"); FunctionButton buttonB = new FunctionButton("功能键B"); // Step3.读取配置文件和反射生成具体命令对象 Command commandA = (Command)AppConfigHelper.GetCommandAInstance(); Command commandB = (Command)AppConfigHelper.GetCommandBInstance(); // Step4.将命令注入功能键 buttonA.SetCommand(commandA); buttonB.SetCommand(commandB); window.AddFunctionButton(buttonA); window.AddFunctionButton(buttonB); window.Display(); // Step5.调用功能键的业务方法 buttonA.OnClick(); buttonB.OnClick(); Console.ReadKey(); } }
这里为了提高系统的灵活性,将具体命令类配置在了配置文件中,并通过帮助类AppConfigHelper来读取配置并反射生成对象。其中配置文件设置如下:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="HelpCommand" value="Manulife.ChengDu.DesignPattern.Command.HelpCommand, Manulife.ChengDu.DesignPattern.Command" /> <add key="MinimizeCommand" value="Manulife.ChengDu.DesignPattern.Command.MinimizeCommand, Manulife.ChengDu.DesignPattern.Command" /> </appSettings> </configuration>
AppConfigHelper类的实现如下,这里不再详述。
public class AppConfigHelper { public static string GetCommandAName() { string factoryName = null; try { factoryName = System.Configuration.ConfigurationManager.AppSettings["HelpCommand"]; } catch (Exception ex) { Console.WriteLine(ex.Message); } return factoryName; } public static object GetCommandAInstance() { string assemblyName = AppConfigHelper.GetCommandAName(); Type type = Type.GetType(assemblyName); var instance = Activator.CreateInstance(type); return instance; } public static string GetCommandBName() { string factoryName = null; try { factoryName = System.Configuration.ConfigurationManager.AppSettings["MinimizeCommand"]; } catch (Exception ex) { Console.WriteLine(ex.Message); } return factoryName; } public static object GetCommandBInstance() { string assemblyName = AppConfigHelper.GetCommandBName(); Type type = Type.GetType(assemblyName); var instance = Activator.CreateInstance(type); return instance; } }
编译后运行,输出结果如下图所示:
此时,如果需要修改功能键,例如某个功能键可以实现“自动截屏”,只需要增加一个新的具体命令类,在该命令类与屏幕处理者(ScreenHandler)之间创建一个关联关系,然后将该具体命令类的对象通过配置文件注入到某个功能键即可,原有代码无需修改,符合开闭原则。
四、命令模式总结
4.1 主要优点
(1)降低了系统的耦合度 => 请求发送者与接受者不存在直接引用
(2)方便地增加新的命令到系统中 => 无须修改源代码,从而符合开闭原则
4.2 主要缺点
使用命令模式可能会导致某些系统有过多的具体命令类。 => 因为针对每一个对请求接收者的调用操作都需要设计一个具体命令,因此在某些系统中可能需要提供大量的具体命令类。
4.3 应用场景
系统需要将请求调用者和请求接收者解耦 => 那就快用命令模式吧骚年!
参考资料
刘伟,《设计模式的艺术—软件开发人员内功修炼之道》