引言
提起Command模式,我想沒(méi)有什么比遙控器的例子更能說(shuō)明問(wèn)題了,本文將通過(guò)它來(lái)一步步實(shí)現(xiàn)GOF的Command模式。
我們先看下這個(gè)遙控器程序的需求:假如我們需要為家里的電器設(shè)計(jì)一個(gè)遠(yuǎn)程遙控器,通過(guò)這個(gè)控制器,我們可以控制電器(諸如燈、風(fēng)扇、空調(diào)等)的開關(guān)。我們的控制器上有一系列的按鈕,分別對(duì)應(yīng)家中的某個(gè)電器,當(dāng)我們?cè)谶b控器上按下“On”時(shí),電器打開;當(dāng)我們按下“Off”時(shí),電器關(guān)閉。
好了,讓我們開始Command 模式之旅吧。
HardCoding的實(shí)現(xiàn)方式
控制器的實(shí)現(xiàn)
一般來(lái)說(shuō),考慮問(wèn)題通常有兩種方式:從最復(fù)雜的情況考慮,也就是盡可能的深謀遠(yuǎn)慮,設(shè)計(jì)之初就考慮到程序的可維護(hù)性、擴(kuò)展性;還有就是從最簡(jiǎn)單的情況考慮,不考慮擴(kuò)展,客戶要求什么,我們就做個(gè)什么,至于以后有什么新的需求,等以后再說(shuō)。當(dāng)然這兩種方式各有優(yōu)劣,本文我們從最簡(jiǎn)單的情況開始考慮。
我們假設(shè)控制器只能控制 三個(gè)電器,分別是:燈、電扇、門(你就當(dāng)是電子門好了^^)。那么我們的控制器應(yīng)該有三組,共六個(gè)按鈕,每一組按鈕分別有“On”,“Off”按鈕。同時(shí),我們規(guī)定,第一組按鈕對(duì)應(yīng)燈,第二組按鈕對(duì)應(yīng)電扇,第三組則對(duì)應(yīng)門,那么控制器應(yīng)該就像這樣:
計(jì)模式/01.gif)
類的設(shè)計(jì)
好了,控制器大致是這么個(gè)樣子了,那么 燈、電扇、門又是什么樣子呢?如果你看過(guò)前面幾節(jié)的模式,你可能會(huì)以為此時(shí)又要為它們創(chuàng)建一個(gè)基類或者接口,然后供它們繼承或?qū)崿F(xiàn)?,F(xiàn)在讓我們先看看我們想要控制的電器是什么樣子的:
計(jì)模式/02.gif)
很抱歉,你遺憾地發(fā)現(xiàn),它們的接口完全不同,我們沒(méi)有辦法對(duì)它們進(jìn)行抽象,但是因?yàn)槲覀兇丝虄H考慮客戶最原始的需求(最簡(jiǎn)單的情況),那么我們大可以直接將它們復(fù)合到 遙控器(ControlPanel) 中
NOTE:關(guān)于接口,有狹義的含義:就是一個(gè)聲明為interface的類型。還有一個(gè)廣義的含義:就是對(duì)象暴露給外界的方法、屬性,所以一個(gè)抽象類也可以稱作一個(gè)接口。這里,說(shuō)它們的接口不同,意思是說(shuō):這三個(gè)電器暴露給外界的方法完全不同。
計(jì)模式/03.gif)
注意到,PressOn方法,它代表著某一個(gè)按鍵被按下,并接受一個(gè)int類型的參數(shù):SlotNo,它代表是第幾個(gè)鍵被按下。顯然,SlotNo的取值為0到2。對(duì)于PressOff則是完全相同的設(shè)計(jì)。
代碼實(shí)現(xiàn)
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 // 定義風(fēng)扇
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 // 點(diǎn)擊On按鈕時(shí)的操作。slotNo,第幾個(gè)按鈕被按
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 // 點(diǎn)擊Off按鈕時(shí)的操作。
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); // 按第一個(gè)On按鈕,燈被打開了
85 panel.PressOn(2); // 按第二個(gè)On按鈕,門被打開了
86 panel.PressOff(2); // 按第二個(gè)Off按鈕,門被關(guān)閉了
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
存在問(wèn)題
這個(gè)解決方案雖然能解決當(dāng)前的問(wèn)題,但是幾乎沒(méi)有任何擴(kuò)展性可言?;蛘哒f(shuō),被調(diào)用者(Receiver:燈、電扇、門)與它們的調(diào)用者(Invoker:遙控器)是緊耦合的。遙控器不僅需要確切地知道它能控制哪些電器,并且需要知道這些電器由哪些方法可供調(diào)用。
- 如果我們需要調(diào)換一下按鈕所控制的電器的次序,比如說(shuō)我們需要讓按鈕1不再控制燈,而是控制門,那么我們需要修改 PressOn 和 PressOff 方法中的Switch語(yǔ)句。
- 如果我們需要給遙控器多添一個(gè)按鈕,以使它多控制一個(gè)電器,那么遙控器的字段、構(gòu)造函數(shù)、PressOn、PressOff方法都要修改。
- 如果我們不給遙控器多添按鈕,但是要求它可以控制10個(gè)或者電器,換言之,就是我們可以動(dòng)態(tài)分配某個(gè)按鈕控制哪個(gè)電器,這樣的設(shè)計(jì)看上去簡(jiǎn)直無(wú)法完成。
HardCoding 的另一實(shí)現(xiàn)
新設(shè)計(jì)方案
在考慮新的方案以前,我們先回顧前面的設(shè)計(jì),第三個(gè)問(wèn)題似乎暗示著我們的遙控器不夠好,思考一下,我們發(fā)現(xiàn)可以這樣設(shè)計(jì)遙控器:
計(jì)模式/04.gif)
對(duì)比一下,我們看到可以通過(guò)左側(cè)可以上下活動(dòng)的閥門來(lái)控制當(dāng)前遙控器控制的是哪個(gè)電器(按照?qǐng)D中當(dāng)前顯示,控制的是燈),在選定了閥門后,我們可以再通過(guò)On,Off按鈕來(lái)對(duì)電器進(jìn)行控制。此時(shí),我們需要多添一個(gè)方法,通過(guò)它來(lái)控制閥門(進(jìn)而選擇想要控制的電器)。我們管這個(gè)方法叫做SetDevice()。那么我們的設(shè)計(jì)變成下圖所示:
NOTE:在圖中,以及現(xiàn)實(shí)世界中,閥門所能控制的電器數(shù)總是有限的,但在程序中,可以是無(wú)限的,就看你有多少個(gè)諸如light的電器類了
計(jì)模式/05.gif)
注意到幾點(diǎn)變化:
- 因?yàn)槲覀兗僭O(shè)遙控器可以控制的電器是無(wú)限多的,所以這里不能指定具體電器類型,因?yàn)樵贑#中所有類型均繼承自O(shè)bject,我們將SetDevice()方法接受的參數(shù)設(shè)置成為Object。
- ControlPanel不知道它將控制哪個(gè)類,所以圖中ControlPanel和Light、Door、Fan沒(méi)有聯(lián)系。
- PressOn()和PressOff()方法不再需要參數(shù),因?yàn)楹苊黠@,只有一組On和Off按鈕。
代碼實(shí)現(xiàn)
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 // 點(diǎn)擊On按鈕時(shí)的操作。
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 // 點(diǎn)擊Of按鈕時(shí)的操作。
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 // 設(shè)置閥門控制哪個(gè)電器
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); // 設(shè)置閥門控制燈
50 panel.PressOn(); // 打開燈
51 panel.PressOff(); // 關(guān)閉燈
52
53 panel.SetDevice(fan); // 設(shè)置閥門控制電扇
54 panel.PressOn(); // 打開門
55 }
56 }
57 }
存在問(wèn)題
我們首先可以看到,這個(gè)方案似乎解決了第一種設(shè)計(jì)的大多數(shù)問(wèn)題,除了一點(diǎn)點(diǎn)瑕疵:
- 盡管我們可以控制任意多的設(shè)備,但是我們每添加一個(gè)可以控制的設(shè)備,仍需要修改PressOn()和PressOff()方法。
- 在PressOn()和PressOff()方法中,需要對(duì)所有可能控制的電器進(jìn)行類型轉(zhuǎn)換,無(wú)疑效率低下。
封裝調(diào)用
問(wèn)題分析
我們的處境似乎一籌莫展,想不到更好的辦法來(lái)解決。這時(shí)候,讓我們先回頭再觀察一下ControlPanel的PressOn()和PressOff()代碼。
1 // 點(diǎn)擊On按鈕時(shí)的操作。
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 }
我們發(fā)現(xiàn)PressOn()和PressOff()方法在每次添加新設(shè)備時(shí)需要作修改,而實(shí)際上改變的是對(duì)對(duì)象方法的調(diào)用,因?yàn)椴还苡卸嗌賯€(gè)if語(yǔ)句,只會(huì)調(diào)用其中某個(gè)不為null的對(duì)象的一個(gè)方法。然后我們?cè)倩仡櫼幌翺O的思想,Encapsulate what varies(封裝變化)。我們想是不是應(yīng)該有辦法將這變化的這部分(方法的調(diào)用)封裝起來(lái)呢?
在考慮如何封裝之前,我們假設(shè)已經(jīng)有一個(gè)類,把它封裝起來(lái)了,我們管這個(gè)類叫做Command,那么這個(gè)類該如何使用呢?
我們先考慮一下它的構(gòu)成,因?yàn)樗庋b各個(gè)對(duì)象的方法,所以,它應(yīng)該暴露出一個(gè)方法,這個(gè)方法既可以代表 light.TurnOn(),也可以代表fan.Start(),還可以代表door.Open(),讓我們給這個(gè)方法起個(gè)名字,叫做Execute()。
好了,現(xiàn)在我們有了Command類,還有了一個(gè)萬(wàn)金油的Execute()方法,現(xiàn)在,我們修改PressOn()方法,讓它通過(guò)這個(gè)Command類來(lái)控制電器(調(diào)用各個(gè)類的方法)。
1 // 點(diǎn)擊On按鈕時(shí)的操作。
2 public void PressOn() {
3 command.Execute();
4 }
哇,是不是有點(diǎn)簡(jiǎn)單的過(guò)分了???但就是這么簡(jiǎn)單,可我們還是發(fā)現(xiàn)了兩個(gè)問(wèn)題:
- Command應(yīng)該能知道它調(diào)用的是哪個(gè)電器類的哪個(gè)方法,這暗示我們Command類應(yīng)該保存對(duì)于具體電器類的一個(gè)引用。
- 我們的ControlPanel應(yīng)該有兩個(gè)Command,一個(gè)Command對(duì)應(yīng)于所有開啟的操作(我們管它叫onCommand),一個(gè)Command對(duì)應(yīng)所有關(guān)閉的操作(我們管它叫offCommand)。
同時(shí),我們的SetDevice(object)方法,也應(yīng)該改成SetCommand(onCommand,offCommand)。好了,現(xiàn)在讓我們看看新版ControlPanel 的全景圖吧。
計(jì)模式/06.gif)
Command類型的實(shí)現(xiàn)
顯然,我們應(yīng)該能看出:onCommand實(shí)體變量(instance variable)和offCommand變量屬于Command類型,同時(shí),上面我們已經(jīng)討論過(guò)Command類應(yīng)該具有一個(gè)Execute()方法,除此以外,它還需要可以保存對(duì)各個(gè)對(duì)象的引用,通過(guò)Execute()方法可以調(diào)用其引用的對(duì)象的方法。
那么我們按照這個(gè)思路,來(lái)看下開燈這一操作(調(diào)用light對(duì)象的TurnOn()方法)的Command對(duì)象應(yīng)該是什么樣的:
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 }
再看下開電扇(調(diào)用fan對(duì)象的Start()方法)的Command對(duì)象應(yīng)該是什么樣的:
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 }
這樣顯然是不行的,它沒(méi)有解決任何的問(wèn)題,因?yàn)镕anStartCommand和LightOnCommand是不同的類型,而我們的ControlPanel要求對(duì)于所有打開的操作應(yīng)該只接受一個(gè)類型的Command的。但是經(jīng)過(guò)我們上面的討論,我們已經(jīng)知道所有的Command都有一個(gè)Execute()方法,我們何不定義一個(gè)接口來(lái)解決這個(gè)問(wèn)題呢?
計(jì)模式/07.gif)
OK,現(xiàn)在我們已經(jīng)完成了全部的設(shè)計(jì),讓我們先看一下最終的UML圖,再進(jìn)行代碼實(shí)現(xiàn)吧(簡(jiǎn)單起見(jiàn),只加入了燈和電扇)。
計(jì)模式/08.gif)
我們先看下這張圖說(shuō)明了什么,以及發(fā)生的順序:
- ConsoleApplication,也就是我們的應(yīng)用程序,它創(chuàng)建電器Fan、Light對(duì)象,以及LightOnCommand和FanStartCommand。
- LightOnCommand、FanStartCommand實(shí)現(xiàn)了ICommand接口,它保存著對(duì)于Fan和Light的引用,并通過(guò)Execute()調(diào)用Fan和Light的方法。
- ControlPanel復(fù)合了Command對(duì)象,通過(guò)調(diào)用Command的Execute()方法,間接調(diào)用了Light的TurnOn()方法或者是Fan的Stop()方法。
它們之間的時(shí)序圖是這樣的:
計(jì)模式/09.gif)
可以看出:通過(guò)引入Command對(duì)象,ControlPanel對(duì)于它實(shí)際調(diào)用的對(duì)象Fan或者Light是一無(wú)所知的,它只知道當(dāng)On按下的時(shí)候就調(diào)用onCommand的Execute()方法;當(dāng)Off按下的時(shí)候就調(diào)用offCommand的Execute()方法。Light和Fan當(dāng)然更不知道誰(shuí)在調(diào)用它。通過(guò)這種方式,我們實(shí)現(xiàn)了調(diào)用者(Invoker,遙控器ControlPanel) 和 被調(diào)用者(Receiver,電扇Fan等)的解耦。如果將來(lái)我們需要對(duì)這個(gè)ControlPanel進(jìn)行擴(kuò)展,只需要再添加一個(gè)實(shí)現(xiàn)了ICommand接口的對(duì)象就可以了,對(duì)于ControlPanel無(wú)需做任何修改。
代碼實(shí)現(xiàn)
1 namespace Command {
2
3 // 定義空調(diào),用于測(cè)試給遙控器添新控制類型
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 // 定義開空調(diào)命令
22 public class AirOnCommand : ICommand {
23 AirCondition airCondition;
24 public AirOnCommand(AirCondition airCondition) {
25 this.airCondition = airCondition;
26 }
27 public void Execute() { //注意,你可以在Execute()中添加多個(gè)方法
28 airCondition.Start();
29 airCondition.SetTemperature(16);
30 }
31 }
32
33 // 定義關(guān)空調(diào)命令
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 // 創(chuàng)建遙控器對(duì)象
68 ControlPanel panel = new ControlPanel();
69
70 AirCondition airCondition = new AirCondition(); //創(chuàng)建空調(diào)對(duì)象
71
72 // 創(chuàng)建Command對(duì)象,傳遞空調(diào)對(duì)象
73 ICommand onCommand = new AirOnCommand(airCondition);
74 ICommand offCommand = new AirOffCommand(airCondition);
75
76 // 設(shè)置遙控器的Command
77 panel.SetCommand(onCommand, offCommand);
78
79 panel.PressOn(); //按下On按鈕,開空調(diào),溫度調(diào)到16度
80 panel.PressOff(); //按下Off按鈕,關(guān)空調(diào)
81
82 }
83 }
84 }
Command 模式
實(shí)際上,我們上面做的這一切,實(shí)現(xiàn)了另一個(gè)設(shè)計(jì)模式:Command模式?,F(xiàn)在又到了給出官方定義的時(shí)候了。每次到了這部分我就不知道該怎么寫了,寫的人太多了,資料也太多了,我相信你看到這里對(duì)Command模式已經(jīng)比較清楚了,所以我還是一如既往地從簡(jiǎn)吧。
Command模式的正式定義:將一個(gè)請(qǐng)求封裝為一個(gè)對(duì)象,從而使你可用不同的請(qǐng)求對(duì)客戶進(jìn)行參數(shù)化;對(duì)請(qǐng)求排隊(duì)或記錄請(qǐng)求日志,以及支持可撤消的操作。
它的 靜態(tài)圖 是這樣的:
計(jì)模式/11.gif)
它的 時(shí)序圖 是這樣的:
計(jì)模式/11.gif)
可以和我們前面的圖對(duì)比一下,對(duì)于這兩個(gè)圖,除了改了個(gè)名字外基本沒(méi)變,我就不再說(shuō)明了,也留給你一點(diǎn)思考的空間。
總結(jié)
本文簡(jiǎn)單地介紹了GOF的Commmand模式,我們通過(guò)一個(gè)簡(jiǎn)單的范例家電遙控器 實(shí)現(xiàn)了這一模式。
我們首先了解了不使用此模式的HardCoding方式的實(shí)現(xiàn)方法,討論了它的缺點(diǎn);然后又換了另一種改進(jìn)了的實(shí)現(xiàn)方法,再次討論了它的不足。 然后,我們通過(guò)將對(duì)象的調(diào)用封裝到一個(gè)Command對(duì)象中的方式,巧妙地完成了設(shè)計(jì)。最后,我們給出了Command模式的正式定義。
本文僅僅簡(jiǎn)要介紹了Command模式,它的高級(jí)應(yīng)用:取消操作(UnDo)、事務(wù)支持(Transaction)、隊(duì)列請(qǐng)求(Queuing Request) 以后有時(shí)間了會(huì)再寫文章。
希望這篇文章能對(duì)你有所幫助!
本文轉(zhuǎn)自:http://www.tracefact.net/Design-Pattern/Command.aspx
其他鏈接:http://blog.csdn.net/tianxiaoqi2008/article/details/7276846
posted on 2012-07-10 13:40
王海光 閱讀(523)
評(píng)論(0) 編輯 收藏 引用 所屬分類:
Design Pattern