引言
提起Command模式,我想沒有什么比遙控器的例子更能說明問題了,本文將通過它來一步步實現GOF的Command模式。
我們先看下這個遙控器程序的需求:假如我們需要為家里的電器設計一個遠程遙控器,通過這個控制器,我們可以控制電器(諸如燈、風扇、空調等)的開關。我們的控制器上有一系列的按鈕,分別對應家中的某個電器,當我們在遙控器上按下“On”時,電器打開;當我們按下“Off”時,電器關閉。
好了,讓我們開始Command 模式之旅吧。
HardCoding的實現方式
控制器的實現
一般來說,考慮問題通常有兩種方式:從最復雜的情況考慮,也就是盡可能的深謀遠慮,設計之初就考慮到程序的可維護性、擴展性;還有就是從最簡單的情況考慮,不考慮擴展,客戶要求什么,我們就做個什么,至于以后有什么新的需求,等以后再說。當然這兩種方式各有優劣,本文我們從最簡單的情況開始考慮。
我們假設控制器只能控制 三個電器,分別是:燈、電扇、門(你就當是電子門好了^^)。那么我們的控制器應該有三組,共六個按鈕,每一組按鈕分別有“On”,“Off”按鈕。同時,我們規定,第一組按鈕對應燈,第二組按鈕對應電扇,第三組則對應門,那么控制器應該就像這樣:

類的設計
好了,控制器大致是這么個樣子了,那么 燈、電扇、門又是什么樣子呢?如果你看過前面幾節的模式,你可能會以為此時又要為它們創建一個基類或者接口,然后供它們繼承或實現。現在讓我們先看看我們想要控制的電器是什么樣子的:

很抱歉,你遺憾地發現,它們的接口完全不同,我們沒有辦法對它們進行抽象,但是因為我們此刻僅考慮客戶最原始的需求(最簡單的情況),那么我們大可以直接將它們復合到 遙控器(ControlPanel) 中
NOTE:關于接口,有狹義的含義:就是一個聲明為interface的類型。還有一個廣義的含義:就是對象暴露給外界的方法、屬性,所以一個抽象類也可以稱作一個接口。這里,說它們的接口不同,意思是說:這三個電器暴露給外界的方法完全不同。

注意到,PressOn方法,它代表著某一個按鍵被按下,并接受一個int類型的參數:SlotNo,它代表是第幾個鍵被按下。顯然,SlotNo的取值為0到2。對于PressOff則是完全相同的設計。
代碼實現
1 namespace Command {
2
3 // 定義燈
4 public class Light{
5 public void TurnOn(){
6 Console.WriteLine("The light is turned on.");
7 }
8 public void TurnOff() {
9 Console.WriteLine("The light is turned off.");
10 }
11 }
12
13 // 定義風扇
14 public class Fan {
15 public void Start() {
16 Console.WriteLine("The fan is starting.");
17 }
18 public void Stop() {
19 Console.WriteLine("The fan is stopping.");
20 }
21 }
22
23 // 定義門
24 public class Door {
25 public void Open() {
26 Console.WriteLine("The door is open for you.");
27 }
28 public void Shut() {
29 Console.WriteLine("The door is closed for safety");
30 }
31 }
32
33 // 定義遙控器
34 public class ControlPanel {
35 private Light light;
36 private Fan fan;
37 private Door door;
38
39 public ControlPanel(Light light, Fan fan, Door door) {
40 this.light = light;
41 this.fan = fan;
42 this.door = door;
43 }
44
45 // 點擊On按鈕時的操作。slotNo,第幾個按鈕被按
46 public void PressOn(int slotNo){
47 switch (slotNo) {
48 case 0:
49 light.TurnOn();
50 break;
51 case 1:
52 fan.Start();
53 break;
54 case 2:
55 door.Open();
56 break;
57 }
58 }
59
60 // 點擊Off按鈕時的操作。
61 public void PressOff(int slotNo) {
62 switch (slotNo) {
63 case 0:
64 light.TurnOff();
65 break;
66 case 1:
67 fan.Stop();
68 break;
69 case 2:
70 door.Shut();
71 break;
72 }
73 }
74 }
75
76 class Program {
77 static void Main(string[] args) {
78 Light light = new Light();
79 Fan fan = new Fan();
80 Door door = new Door();
81
82 ControlPanel panel = new ControlPanel(light, fan, door);
83
84 panel.PressOn(0); // 按第一個On按鈕,燈被打開了
85 panel.PressOn(2); // 按第二個On按鈕,門被打開了
86 panel.PressOff(2); // 按第二個Off按鈕,門被關閉了
87 }
88 }
89 }
90
91 輸出為:
92
93 The light is turned on.
94 The door is open for you.
95 The door is closed for safety
存在問題
這個解決方案雖然能解決當前的問題,但是幾乎沒有任何擴展性可言。或者說,被調用者(Receiver:燈、電扇、門)與它們的調用者(Invoker:遙控器)是緊耦合的。遙控器不僅需要確切地知道它能控制哪些電器,并且需要知道這些電器由哪些方法可供調用。
- 如果我們需要調換一下按鈕所控制的電器的次序,比如說我們需要讓按鈕1不再控制燈,而是控制門,那么我們需要修改 PressOn 和 PressOff 方法中的Switch語句。
- 如果我們需要給遙控器多添一個按鈕,以使它多控制一個電器,那么遙控器的字段、構造函數、PressOn、PressOff方法都要修改。
- 如果我們不給遙控器多添按鈕,但是要求它可以控制10個或者電器,換言之,就是我們可以動態分配某個按鈕控制哪個電器,這樣的設計看上去簡直無法完成。
HardCoding 的另一實現
新設計方案
在考慮新的方案以前,我們先回顧前面的設計,第三個問題似乎暗示著我們的遙控器不夠好,思考一下,我們發現可以這樣設計遙控器:

對比一下,我們看到可以通過左側可以上下活動的閥門來控制當前遙控器控制的是哪個電器(按照圖中當前顯示,控制的是燈),在選定了閥門后,我們可以再通過On,Off按鈕來對電器進行控制。此時,我們需要多添一個方法,通過它來控制閥門(進而選擇想要控制的電器)。我們管這個方法叫做SetDevice()。那么我們的設計變成下圖所示:
NOTE:在圖中,以及現實世界中,閥門所能控制的電器數總是有限的,但在程序中,可以是無限的,就看你有多少個諸如light的電器類了

注意到幾點變化:
- 因為我們假設遙控器可以控制的電器是無限多的,所以這里不能指定具體電器類型,因為在C#中所有類型均繼承自Object,我們將SetDevice()方法接受的參數設置成為Object。
- ControlPanel不知道它將控制哪個類,所以圖中ControlPanel和Light、Door、Fan沒有聯系。
- PressOn()和PressOff()方法不再需要參數,因為很明顯,只有一組On和Off按鈕。
代碼實現
1 namespace Command {
2
3 public class Light { // 略 }
4 public class Fan { // 略 }
5 public class Door { // 略 }
6
7 // 定義遙控器
8 public class ControlPanel {
9 private Object device;
10
11 // 點擊On按鈕時的操作。
12 public void PressOn() {
13 Light light = device as Light;
14 if (light != null) light.TurnOn();
15
16 Fan fan = device as Fan;
17 if (fan != null) fan.Start();
18
19 Door door = device as Door;
20 if (door != null) door.Open();
21 }
22
23 // 點擊Of按鈕時的操作。
24 public void PressOff() {
25 Light light = device as Light;
26 if (light != null) light.TurnOff();
27
28 Fan fan = device as Fan;
29 if (fan != null) fan.Stop();
30
31 Door door = device as Door;
32 if (door != null) door.Shut();
33 }
34
35 // 設置閥門控制哪個電器
36 public void SetDevice(Object device) {
37 this.device = device;
38 }
39 }
40
41 class Program {
42 static void Main(string[] args) {
43
44 Light light = new Light();
45 Fan fan = new Fan();
46
47 ControlPanel panel = new ControlPanel();
48
49 panel.SetDevice(light); // 設置閥門控制燈
50 panel.PressOn(); // 打開燈
51 panel.PressOff(); // 關閉燈
52
53 panel.SetDevice(fan); // 設置閥門控制電扇
54 panel.PressOn(); // 打開門
55 }
56 }
57 }
存在問題
我們首先可以看到,這個方案似乎解決了第一種設計的大多數問題,除了一點點瑕疵:
- 盡管我們可以控制任意多的設備,但是我們每添加一個可以控制的設備,仍需要修改PressOn()和PressOff()方法。
- 在PressOn()和PressOff()方法中,需要對所有可能控制的電器進行類型轉換,無疑效率低下。
封裝調用
問題分析
我們的處境似乎一籌莫展,想不到更好的辦法來解決。這時候,讓我們先回頭再觀察一下ControlPanel的PressOn()和PressOff()代碼。
1 // 點擊On按鈕時的操作。
2 public void PressOn() {
3 Light light = device as Light;
4 if (light != null) light.TurnOn();
5
6 Fan fan = device as Fan;
7 if (fan != null) fan.Start();
8
9 Door door = device as Door;
10 if (door != null) door.Open();
11 }
我們發現PressOn()和PressOff()方法在每次添加新設備時需要作修改,而實際上改變的是對對象方法的調用,因為不管有多少個if語句,只會調用其中某個不為null的對象的一個方法。然后我們再回顧一下OO的思想,Encapsulate what varies(封裝變化)。我們想是不是應該有辦法將這變化的這部分(方法的調用)封裝起來呢?
在考慮如何封裝之前,我們假設已經有一個類,把它封裝起來了,我們管這個類叫做Command,那么這個類該如何使用呢?
我們先考慮一下它的構成,因為它要封裝各個對象的方法,所以,它應該暴露出一個方法,這個方法既可以代表 light.TurnOn(),也可以代表fan.Start(),還可以代表door.Open(),讓我們給這個方法起個名字,叫做Execute()。
好了,現在我們有了Command類,還有了一個萬金油的Execute()方法,現在,我們修改PressOn()方法,讓它通過這個Command類來控制電器(調用各個類的方法)。
1 // 點擊On按鈕時的操作。
2 public void PressOn() {
3 command.Execute();
4 }
哇,是不是有點簡單的過分了!?但就是這么簡單,可我們還是發現了兩個問題:
- Command應該能知道它調用的是哪個電器類的哪個方法,這暗示我們Command類應該保存對于具體電器類的一個引用。
- 我們的ControlPanel應該有兩個Command,一個Command對應于所有開啟的操作(我們管它叫onCommand),一個Command對應所有關閉的操作(我們管它叫offCommand)。
同時,我們的SetDevice(object)方法,也應該改成SetCommand(onCommand,offCommand)。好了,現在讓我們看看新版ControlPanel 的全景圖吧。

Command類型的實現
顯然,我們應該能看出:onCommand實體變量(instance variable)和offCommand變量屬于Command類型,同時,上面我們已經討論過Command類應該具有一個Execute()方法,除此以外,它還需要可以保存對各個對象的引用,通過Execute()方法可以調用其引用的對象的方法。
那么我們按照這個思路,來看下開燈這一操作(調用light對象的TurnOn()方法)的Command對象應該是什么樣的:
1 public class LightOnCommand{
2 Light light;
3 public Command(Light light){
4 this.light = light;
5 }
6
7 public void Execute(){
8 light.TurnOn();
9 }
10 }
再看下開電扇(調用fan對象的Start()方法)的Command對象應該是什么樣的:
1 public class FanStartCommand{
2 Fan fan;
3 public Command(Fan fan){
4 this.fan = fan;
5 }
6
7 public void Execute(){
8 fan.Start();
9 }
10 }
這樣顯然是不行的,它沒有解決任何的問題,因為FanStartCommand和LightOnCommand是不同的類型,而我們的ControlPanel要求對于所有打開的操作應該只接受一個類型的Command的。但是經過我們上面的討論,我們已經知道所有的Command都有一個Execute()方法,我們何不定義一個接口來解決這個問題呢?

OK,現在我們已經完成了全部的設計,讓我們先看一下最終的UML圖,再進行代碼實現吧(簡單起見,只加入了燈和電扇)。

我們先看下這張圖說明了什么,以及發生的順序:
- ConsoleApplication,也就是我們的應用程序,它創建電器Fan、Light對象,以及LightOnCommand和FanStartCommand。
- LightOnCommand、FanStartCommand實現了ICommand接口,它保存著對于Fan和Light的引用,并通過Execute()調用Fan和Light的方法。
- ControlPanel復合了Command對象,通過調用Command的Execute()方法,間接調用了Light的TurnOn()方法或者是Fan的Stop()方法。
它們之間的時序圖是這樣的:

可以看出:通過引入Command對象,ControlPanel對于它實際調用的對象Fan或者Light是一無所知的,它只知道當On按下的時候就調用onCommand的Execute()方法;當Off按下的時候就調用offCommand的Execute()方法。Light和Fan當然更不知道誰在調用它。通過這種方式,我們實現了調用者(Invoker,遙控器ControlPanel) 和 被調用者(Receiver,電扇Fan等)的解耦。如果將來我們需要對這個ControlPanel進行擴展,只需要再添加一個實現了ICommand接口的對象就可以了,對于ControlPanel無需做任何修改。
代碼實現
1 namespace Command {
2
3 // 定義空調,用于測試給遙控器添新控制類型
4 public class AirCondition {
5 public void Start() {
6 Console.WriteLine("The AirCondition is turned on.");
7 }
8 public void SetTemperature(int i) {
9 Console.WriteLine("The temperature is set to " + i);
10 }
11 public void Stop() {
12 Console.WriteLine("The AirCondition is turned off.");
13 }
14 }
15
16 // 定義Command接口
17 public interface ICommand {
18 void Execute();
19 }
20
21 // 定義開空調命令
22 public class AirOnCommand : ICommand {
23 AirCondition airCondition;
24 public AirOnCommand(AirCondition airCondition) {
25 this.airCondition = airCondition;
26 }
27 public void Execute() { //注意,你可以在Execute()中添加多個方法
28 airCondition.Start();
29 airCondition.SetTemperature(16);
30 }
31 }
32
33 // 定義關空調命令
34 public class AirOffCommand : ICommand {
35 AirCondition airCondition;
36 public AirOffCommand(AirCondition airCondition) {
37 this.airCondition = airCondition;
38 }
39 public void Execute() {
40 airCondition.Stop();
41 }
42 }
43
44
45 // 定義遙控器
46 public class ControlPanel {
47 private ICommand onCommand;
48 private ICommand offCommand;
49
50 public void PressOn() {
51 onCommand.Execute();
52 }
53
54 public void PressOff() {
55 offCommand.Execute();
56 }
57
58 public void SetCommand(ICommand onCommand,ICommand offCommand) {
59 this.onCommand = onCommand;
60 this.offCommand = offCommand;
61 }
62 }
63
64 class Program {
65 static void Main(string[] args) {
66
67 // 創建遙控器對象
68 ControlPanel panel = new ControlPanel();
69
70 AirCondition airCondition = new AirCondition(); //創建空調對象
71
72 // 創建Command對象,傳遞空調對象
73 ICommand onCommand = new AirOnCommand(airCondition);
74 ICommand offCommand = new AirOffCommand(airCondition);
75
76 // 設置遙控器的Command
77 panel.SetCommand(onCommand, offCommand);
78
79 panel.PressOn(); //按下On按鈕,開空調,溫度調到16度
80 panel.PressOff(); //按下Off按鈕,關空調
81
82 }
83 }
84 }
Command 模式
實際上,我們上面做的這一切,實現了另一個設計模式:Command模式。現在又到了給出官方定義的時候了。每次到了這部分我就不知道該怎么寫了,寫的人太多了,資料也太多了,我相信你看到這里對Command模式已經比較清楚了,所以我還是一如既往地從簡吧。
Command模式的正式定義:將一個請求封裝為一個對象,從而使你可用不同的請求對客戶進行參數化;對請求排隊或記錄請求日志,以及支持可撤消的操作。
它的 靜態圖 是這樣的:

它的 時序圖 是這樣的:

可以和我們前面的圖對比一下,對于這兩個圖,除了改了個名字外基本沒變,我就不再說明了,也留給你一點思考的空間。
總結
本文簡單地介紹了GOF的Commmand模式,我們通過一個簡單的范例家電遙控器 實現了這一模式。
我們首先了解了不使用此模式的HardCoding方式的實現方法,討論了它的缺點;然后又換了另一種改進了的實現方法,再次討論了它的不足。 然后,我們通過將對象的調用封裝到一個Command對象中的方式,巧妙地完成了設計。最后,我們給出了Command模式的正式定義。
本文僅僅簡要介紹了Command模式,它的高級應用:取消操作(UnDo)、事務支持(Transaction)、隊列請求(Queuing Request) 以后有時間了會再寫文章。
希望這篇文章能對你有所幫助!
本文轉自:http://www.tracefact.net/Design-Pattern/Command.aspx
其他鏈接:http://blog.csdn.net/tianxiaoqi2008/article/details/7276846
posted on 2012-07-10 13:40
王海光 閱讀(523)
評論(0) 編輯 收藏 引用 所屬分類:
Design Pattern