在生活中,我們裝修新房的最後幾道工序之一是安裝插座和開關,通過開關可以控制一些電器的開啟和關閉,例如電燈或換氣扇。在購買開關時,使用者並不知道它將來到底用於控制什麼電器,也就是說,開關與電燈、換氣扇並無直接關係,一個開關在安裝之後可能用來控制電燈,也可能用來控制換氣扇或者其他電器裝置。相同的開關可以通過不同的電線來控制不同的電器,如下圖所示。
在軟體開發中也存在很多與開關和電器類似的請求傳送者和接受者物件,例如一個按鈕,它可能是一個“關閉視窗”請求的傳送者,而按鈕點選事件處理類則是該請求的接受者。為了降低系統的耦合度,將請求的傳送者和接收者解耦,可以使用一種被稱為命令模式的設計模式來設計系統。
命令模式(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 應用場景
系統需要將請求呼叫者和請求接收者解耦 => 那就快用命令模式吧騷年!
參考資料
劉偉,《設計模式的藝術—軟體開發人員內功修煉之道》