命令模式筆記-後篇
以下定義採自深入淺出設計模式一書:
記得前篇介紹了簡單命令模式,他的圖長這樣:
記得前篇我們提出的創新需求是什麼:
「將來這個廠牌假如出了一系列的產品,可能包含了5種,那在遙控器上設定五個插槽,每購買一台就附上一個插卡晶片
而遙控器能讓我能自由插上所有我可以購買機器的話。那我不就有一個萬用遙控器了,這種自由度不是更棒嗎?」
我們透過完成了命令模式的遙控器API後,我想就可以操控房間裡面的所有電器了吧…
記得插糟嗎?上一篇我們為了操控CDPlayer,所以宣告了CDPlayerOnCommand
那現在假如我有五項電器產品,包含了電腦、遊樂器、電扇、音響以及電燈的話,那該怎麼辦!
計劃是這樣的:我們可以為遙控器的每一個插糟,都對應一個命令,讓遙控器變成Invoker(調用者),當按下遙控器的按鈕以後
就呼叫對應命令物件的execute()方法,而接收者(那些器材)的動作就會被調用
這個遙控器可能就是長成這個樣子,前一篇我們只有設計一個slot,讓CDPlayer插入他的晶片。
直覺上,我們可以透過陣列來存放這一些不同的器材與設備。然後以類似的方式,將命令指定給遙控器,像下面一樣:
1: onCommands[0] = onCommand;
2: offCommands[0] = offCommand;
陣列這麼好用的話,我們來實踐遙控器吧!
1:
2:
3: public interface Command{
4: public void execute();
5: public string getClass();
6: }
7:
8:
9: public class MyRoomController
10: {
11: Command[] onCommands;
12: Command[] offCommands;
13: public MyRoomController()
14: {
15: //共有五個插糟
16: onCommands = new Command[5];
17: offCommands = new Command[5];
18:
19: Command noCommand = new NoComand();
20:
21: for (int i = 0; i < 5; i++)
22: {
23: onCommands[i] = noCommand;
24: offCommands[i] = noCommand;
25: }
26: }
27:
28: public void setCommand(int slot, Command onCommand, Command offCommand)
29: {
30: onCommands[slot] = onCommand;
31: offCommands[slot] = offCommand;
32: }
33:
34: public void onButtonWasPushed(int slot)
35: {
36: onCommands[slot].execute();
37: }
38:
39: public void offButtonWasPushed(int slot)
40: {
41: offCommands[slot].execute();
42: }
43:
44: public string toString()
45: {
46: //改寫ToString()印出插糟名稱與對應的命令!
47: StringBuilder sb = new StringBuilder();
48: sb.Append("\n---------遙控器--------\n");
49: for (int i = 0; i < 5; i++)
50: {
51: sb.Append("[Slot"+ i +"]"+onCommands[i].getClass()+" " + offCommands[i].getClass()+"\n");
52: }
53:
54: return sb.ToString();
55: }
56: }
別忘了,遙控器也只是一個調用者,我們先來實踐一些接收者吧!
我們先實踐介面可能最單純的燈物件
1: //例如這個燈物件
2: public class Light{
3: string Location;
4: public Light(string Location){
5: this.Location = Location;
6: }
7:
8: public void getName(){
9: return this.Location();
10: }
11:
12: public void off(){
13: Console.Write(this.Location + "的燈被關了\n");
14: }
15:
16: public void on(){
17: Console.Write(this.Location + "的燈被開了\n");
18: }
19: }
延續前篇,我們要實踐一個命令,讓人調用,所以也要宣告一個實踐Command介面的一個命令出來喲
1: //燈就簡單多了,只有開跟關,當然你也可以寫一個更複雜,更多元的燈
2: public class LightOnCommand : Command
3: {
4: Light light;
5:
6: public LightOnCommand(Light light){
7: this.light = light;
8: }
9:
10: public void execute(){
11: this.light.on();
12: }
13:
14: public Light getClass()
15: {
16: return this.light;
17: }
18: }
那音響呢?這樣也要寫一個音響物件出來耶…沒錯,當你可以寫出來以後,相信你已經有能力可以完成你房間內所有電器的自動化的命令了!
1: //音響介面
2: public class Stereo{
3: string Location;
4:
5: public Stereo(string Location)
6: {
7: this.Location = Location;
8: }
9:
10: public void getName()
11: {
12: return this.Location() + "的音響";
13: }
14:
15: public void off(){
16: Console.Write("音響關了\n");
17: }
18:
19:
20: public void on(){
21: Console.Write("音響開了\n");
22: }
23:
24:
25: public void setVolume(int n){
26: Console.Write("音量調成了"+n.ToString()+"\n");
27: }
28:
29:
30: public void setCD(string cd){
31: Console.Write("放入了"+cd+"的CD\n");
32: }
33: }
好了,先這樣,我想,還不用測試,你就知道結果了吧!那我們測試先暫緩
剛剛onButtonWasPushed()的函式中,可能會需要這樣的程式碼:
1: public void onButtonWasPushed(int slot)
2: {
3: if (onCommands[slot]!=null)
4: onCommands[slot].execute();
5: }
6:
7: //空物件
8: public class NoCommand:Command
9: {
10: public void execute(){}
11: public void getClass(){
12: Console.write("沒有命令");
13: }
14: }
15:
16: //建構式中
17: Command noCommand = new NoComand();
18: for (int i = 0; i < 5; i++)
19: {
20: onCommands[i] = noCommand;
21: offCommands[i] = noCommand;
22: }
註:上面實踐了一種模式,NoCommand是一個空物件(null object),當需要一個沒有任何意義值時,空物件就很有用
Client也可將處理Null的責任轉移給空物件。
在這邊為例,遙控器不可能一出廠就知道你家有哪些電器需要命令來設定,所以提供了NoCommand物件作為代用品
他的責任就是,被執行到execute()的時候,什麼也不做,很多時候,空物件本身也是一種設計模式!
另外命令模式中還有一個特色要好好給他介紹一下:
命令是可以復原的!例如,電燈開了以後要復原他的命令,自然是把燈關閉了!
那關閉了音響以後,復原命令自然就是開啟音響囉!
既然命令都可以復原,那麼我們應該怎麼做呢??
在命令模式中實在是再簡單不多了!去針對介面寫程式吧!
1: public interface Command{
2: public void execute();
3: public void undo();
4:
5: public string getClass();
6: }
相對的,命令物件就要這樣做:
1: //燈就簡單多了,只有開跟關,當然你也可以寫一個更複雜,更多元的燈
2: public class LightOnCommand : Command
3: {
4: Light light;
5:
6: public LightOnCommand(Light light){
7: this.light = light;
8: }
9:
10: public void execute(){
11: this.light.on();
12: }
13:
14: public void undo()
15: {
16: this.light.off();
17: }
18:
19: public string getClass()
20: {
21: return this.light.getName();
22: }
23: }
相信你也知道LightOffCommand要怎麼實作了。
為了因應這樣復原按鈕的支援,所以,必須對遙控器類別作一些修改(看綠色的註解處)
1: public class MyRoomController
2: {
3: Command[] onCommands;
4: Command[] offCommands;
5: //復原功能的小修改
6: Command undoCommand;
7:
8: public MyRoomController()
9: {
10: onCommands = new Command[5];
11: offCommands = new Command[5];
12:
13:
14: Command noCommand = new NoComand();
15:
16: for (int i = 0; i < 5; i++)
17: {
18: onCommands[i] = noCommand;
19: offCommands[i] = noCommand;
20: }
21: //復原功能的小修改
22: undoCommand = noCommand();
23: }
24:
25: public void setCommand(int slot, Command onCommand, Command offCommand)
26: {
27: onCommands[slot] = onCommand;
28: offCommands[slot] = offCommand;
29: }
30:
31: public void onButtonWasPushed(int slot)
32: {
33: onCommands[slot].execute();
34: //復原功能的小修改
35: undoCommand = onCommands[slot];
36: }
37:
38: public void offButtonWasPushed(int slot)
39: {
40: offCommands[slot].execute();
41: //復原功能的小修改
42: undoCommand = offCommands[slot];
43: }
當你所控制的物件狀態只有兩種的時候,自然不會有問題,就是用上上面的方式來處理命令的實踐(Light.on or off)
但是假如你有先前執行的狀態的時候,通常想要實踐這樣的功能,需要去紀錄一些狀態,例如,Office的復原或是下一步的按鈕
想要回到上一步或下一步,就務必記錄下來狀態,因此,我們這時候,可以將物件的狀態變數,記錄在命令類別中。
例如:
1: public class ElectronicFanSpeedHighCommand():Command
2: {
3: ElectronicFan electronicfan;
4: //記錄狀態的變數。
5: string prevSpeed;
6:
7: public ElectronicFanSpeedHighCommand(ElectronicFan ElectronicFan){
8: this.electronicfan = ElectronicFan;
9: }
10:
11: public void execute(){
12: prevSpeed = electronicfan.getSpeed();//寫在風扇的物件方法中
13: electronicfan.speedHigh(); //改變速度之前,要像上一行一樣記錄狀態。
14: }
15:
16: public void undo(){
17: if (prevSpeed == electronicfan.HIGH) //常數
18: electronicfan.speedHigh();
19: if (prevSpeed == electronicfan.LOW) //常數
20: electronicfan.speedLow();
21: //以此類推
22: }
23: }
命令模式很容易的可以加入復原功能吧…而且將來需求又來的時候,只需要改到命令的實作類別而己。
然而…遙控器會不會夢想太小呢?
既然都已經是萬用遙控器了,那家庭自動化如何呢??
試想按下一個按鈕,就同時關燈,打開音響和電視,設定好DVD,並讓冷氣打開
或者是現在許多會議室都配置了自動化環境設定,可以快速進入會議模式、簡報模式或是上課模式的話
那這樣啟不是完美嗎?至少省去了很多試開關的動作呢…。
再回到Office的應用…「巨集」就是這樣的一個概念。將一連串的動作都”錄”下來,然後依序去執行,甚至…可以復原?
哇嗚,來實踐看看
1: //巨集,一次不只執行一個命令,不錯吧
2: public class MacroCommand : Command
3: {
4: Command[] commands;
5: public MacroCommand(Command[] commands)
6: {
7: this.commands = commands;
8: }
9:
10: public void execute()
11: {
12: //依序執行命令
13: for (int i = 0; i < commands.Length; i++)
14: {
15: commands[i].execute();
16: }
17: }
18: }
測試的時候,就如以下方式來建立”命令”巨集,下面例子是建立音樂廳模式,然後將這個模式設定到遙控器上!
1: //音樂廳模式:關燈後,又開音響...以此類推
2:
3: //建立所有的裝置
4: Light light = new Light("我房間");
5: Stereo stereo = new Stereo("我房間");
6: //...以此類推
7:
8:
9: //建立所有命令
10: LightOffCommand lightOff = new LightOffCommand(light);
11: StereoOnWithCDCommand stereoOn = new StereoOnWithCDCommand(stereo);
12: //...以此類推
13:
14:
15: //建立巨集陣列
16: Command[] concertModeOn = { lightOff, stereoOn };
17: Command[] concertModeOff = { lightOn, stereoOff };
18: MacroCommand concertModeOnMacro = new MacroCommand(concertModeOn);
19: MacroCommand concertModeOffMacro = new MacroCommand(concertModeOff);
20:
21: //設定給遙控器按鈕
22: MyRoomController Controller = new MyRoomController();
23: Controller.setCommand(0, concertModeOnMacro, concertModeOffMacro);
最後,巨集命令可以復原嗎?當然可以
既然建立了Interface,而且規範了undo方法。
你只需要在MacroCommand中,建立一個Undo()函式,然後跟執行一樣,去逐一復原每一道命令。
後記:
命令模式見到了他的本尊,可想而知他的靈活之處,這個模式將發出請求的物件與執行請求的物件鬆綁,而且命令物件可以一個或是一組動作。
只是很常見的,我們通常習慣在一個函式中,將所有的請求執行完,而不是將工作交由接收者(例如燈)去進行,當然這是個聰明(複雜笨重)的物件實踐行為
但這個物件就被緊緊的綁住了,只是如此一來,也不行能把接收者當作參數傳給命令物件。
最後的最後,再來複習一下命令模式吧:
命令模式:將請求封裝成物件,這可以讓你使用不同的請求、佇列、或者是日誌,來參數化其他物件。命令模式也可以支援復原操作。
註:
命令模式還有更多的用途:例如佇列的請求(網頁伺服器、排程器、執行緒池等…),工作佇列的應用,可以將實踐命令介面的物件放到佇列後
有效地去將執行緒的運算限制在固定數目中進行,如此一來,這樣可以有效的管理運算資源的使用。
還有日誌請求(特別是例如Office軟體這類的大型應用程式),這些都擴充了命令模式。
例如日誌的實作,通常調用了大型資料結構的動作,而藉由日誌記錄所有操作,如果系統出狀況,從一個CheckPoint
再重新採用這些操作。而不是每一個變動後,就記錄整個資訊(例如試算表、簡報等),這些技術後續被擴充採用交易機制(Transaction)