提起command模式,我想沒有什麼比遙控器的例子更能說明問題了,本文将通過它來一步步實作gof的command模式。
我們先看下這個遙控器程式的需求:假如我們需要為家裡的電器設計一個遠端遙控器,通過這個控制器,我們可以控制電器(諸如燈、風扇、空調等)的開關。我們的控制器上有一系列的按鈕,分别對應家中的某個電器,當我們在遙控器上按下“on”時,電器打開;當我們按下“off”時,電器關閉。
好了,讓我們開始command 模式之旅吧。
一般來說,考慮問題通常有兩種方式:從最複雜的情況考慮,也就是盡可能的深謀遠慮,設計之初就考慮到程式的可維護性、擴充性;還有就是從最簡單的情況考慮,不考慮擴充,客戶要求什麼,我們就做個什麼,至于以後有什麼新的需求,等以後再說。當然這兩種方式各有優劣,本文我們從最簡單的情況開始考慮。
我們假設控制器隻能控制 三個電器,分别是:燈、電扇、門(你就當是電子門好了^^)。那麼我們的控制器應該有三組,共六個按鈕,每一組按鈕分别有“on”,“off”按鈕。同時,我們規定,第一組按鈕對應燈,第二組按鈕對應電扇,第三組則對應門,那麼控制器應該就像這樣:
好了,控制器大緻是這麼個樣子了,那麼 燈、電扇、門又是什麼樣子呢?如果你看過前面幾節的模式,你可能會以為此時又要為它們建立一個基類或者接口,然後供它們繼承或實作。現在讓我們先看看我們想要控制的電器是什麼樣子的:
很抱歉,你遺憾地發現,它們的接口完全不同,我們沒有辦法對它們進行抽象,但是因為我們此刻僅考慮客戶最原始的需求(最簡單的情況),那麼我們大可以直接将它們複合到 遙控器(controlpanel) 中
note:關于接口,有狹義的含義:就是一個聲明為interface的類型。還有一個廣義的含義:就是對象暴露給外界的方法、屬性,是以一個抽象類也可以稱作一個接口。這裡,說它們的接口不同,意思是說:這三個電器暴露給外界的方法完全不同。
注意到,presson方法,它代表着某一個按鍵被按下,并接受一個int類型的參數:slotno,它代表是第幾個鍵被按下。顯然,slotno的取值為0到2。對于pressoff則是完全相同的設計。
namespace command {
// 定義燈
public class light{
public void turnon(){
console.writeline("the light is turned on.");
}
public void turnoff() {
console.writeline("the light is turned off.");
}
// 定義風扇
public class fan {
public void start() {
console.writeline("the fan is starting.");
public void stop() {
console.writeline("the fan is stopping.");
// 定義門
public class door {
public void open() {
console.writeline("the door is open for you.");
public void shut() {
console.writeline("the door is closed for safety");
// 定義遙控器
public class controlpanel {
private light light;
private fan fan;
private door door;
public controlpanel(light light, fan fan, door door) {
this.light = light;
this.fan = fan;
this.door = door;
// 點選on按鈕時的操作。slotno,第幾個按鈕被按
public void presson(int slotno){
switch (slotno) {
case 0:
light.turnon();
break;
case 1:
fan.start();
case 2:
door.open();
}
// 點選off按鈕時的操作。
public void pressoff(int slotno) {
light.turnoff();
fan.stop();
door.shut();
class program {
static void main(string[] args) {
light light = new light();
fan fan = new fan();
door door = new door();
controlpanel panel = new controlpanel(light, fan, door);
panel.presson(0); // 按第一個on按鈕,燈被打開了
panel.presson(2); // 按第二個on按鈕,門被打開了
panel.pressoff(2); // 按第二個off按鈕,門被關閉了
}
輸出為:
the light is turned on.
the door is open for you.
the door is closed for safety
這個解決方案雖然能解決目前的問題,但是幾乎沒有任何擴充性可言。或者說,被調用者(receiver:燈、電扇、門)與它們的調用者(invoker:遙控器)是緊耦合的。遙控器不僅需要确切地知道它能控制哪些電器,并且需要知道這些電器由哪些方法可供調用。
如果我們需要調換一下按鈕所控制的電器的次序,比如說我們需要讓按鈕1不再控制燈,而是控制門,那麼我們需要修改 presson 和 pressoff 方法中的switch語句。
如果我們需要給遙控器多添一個按鈕,以使它多控制一個電器,那麼遙控器的字段、構造函數、presson、pressoff方法都要修改。
如果我們不給遙控器多添按鈕,但是要求它可以控制10個或者電器,換言之,就是我們可以動态配置設定某個按鈕控制哪個電器,這樣的設計看上去簡直無法完成。
在考慮新的方案以前,我們先回顧前面的設計,第三個問題似乎暗示着我們的遙控器不夠好,思考一下,我們發現可以這樣設計遙控器:
對比一下,我們看到可以通過左側可以上下活動的閥門來控制目前遙控器控制的是哪個電器(按照圖中目前顯示,控制的是燈),在標明了閥門後,我們可以再通過on,off按鈕來對電器進行控制。此時,我們需要多添一個方法,通過它來控制閥門(進而選擇想要控制的電器)。我們管這個方法叫做setdevice()。那麼我們的設計變成下圖所示:
note:在圖中,以及現實世界中,閥門所能控制的電器數總是有限的,但在程式中,可以是無限的,就看你有多少個諸如light的電器類了
注意到幾點變化:
因為我們假設遙控器可以控制的電器是無限多的,是以這裡不能指定具體電器類型,因為在c#中所有類型均繼承自object,我們将setdevice()方法接受的參數設定成為object。
controlpanel不知道它将控制哪個類,是以圖中controlpanel和light、door、fan沒有聯系。
presson()和pressoff()方法不再需要參數,因為很明顯,隻有一組on和off按鈕。
public class light { // 略 }
public class fan { // 略 }
public class door { // 略 }
private object device;
// 點選on按鈕時的操作。
public void presson() {
light light = device as light;
if (light != null) light.turnon();
fan fan = device as fan;
if (fan != null) fan.start();
door door = device as door;
if (door != null) door.open();
// 點選of按鈕時的操作。
public void pressoff() {
if (light != null) light.turnoff();
if (fan != null) fan.stop();
if (door != null) door.shut();
// 設定閥門控制哪個電器
public void setdevice(object device) {
this.device = device;
fan fan = new fan();
controlpanel panel = new controlpanel();
panel.setdevice(light); // 設定閥門控制燈
panel.presson(); // 打開燈
panel.pressoff(); // 關閉燈
panel.setdevice(fan); // 設定閥門控制電扇
panel.presson(); // 打開門
我們首先可以看到,這個方案似乎解決了第一種設計的大多數問題,除了一點點瑕疵:
盡管我們可以控制任意多的裝置,但是我們每添加一個可以控制的裝置,仍需要修改presson()和pressoff()方法。
在presson()和pressoff()方法中,需要對所有可能控制的電器進行類型轉換,無疑效率低下。
我們的處境似乎一籌莫展,想不到更好的辦法來解決。這時候,讓我們先回頭再觀察一下controlpanel的presson()和pressoff()代碼。
// 點選on按鈕時的操作。
public void presson() {
light light = device as light;
if (light != null) light.turnon();
fan fan = device as fan;
if (fan != null) fan.start();
door door = device as door;
if (door != null) door.open();
我們發現presson()和pressoff()方法在每次添加新裝置時需要作修改,而實際上改變的是對對象方法的調用,因為不管有多少個if語句,隻會調用其中某個不為null的對象的一個方法。然後我們再回顧一下oo的思想,encapsulate what varies(封裝變化)。我們想是不是應該有辦法将這變化的這部分(方法的調用)封裝起來呢?
在考慮如何封裝之前,我們假設已經有一個類,把它封裝起來了,我們管這個類叫做command,那麼這個類該如何使用呢?
我們先考慮一下它的構成,因為它要封裝各個對象的方法,是以,它應該暴露出一個方法,這個方法既可以代表 light.turnon(),也可以代表fan.start(),還可以代表door.open(),讓我們給這個方法起個名字,叫做execute()。
好了,現在我們有了command類,還有了一個萬金油的execute()方法,現在,我們修改presson()方法,讓它通過這個command類來控制電器(調用各個類的方法)。
command.execute();
哇,是不是有點簡單的過分了!?但就是這麼簡單,可我們還是發現了兩個問題:
command應該能知道它調用的是哪個電器類的哪個方法,這暗示我們command類應該儲存對于具體電器類的一個引用。
我們的controlpanel應該有兩個command,一個command對應于所有開啟的操作(我們管它叫oncommand),一個command對應所有關閉的操作(我們管它叫offcommand)。
同時,我們的setdevice(object)方法,也應該改成setcommand(oncommand,offcommand)。好了,現在讓我們看看新版controlpanel 的全景圖吧。
顯然,我們應該能看出:oncommand實體變量(instance variable)和offcommand變量屬于command類型,同時,上面我們已經讨論過command類應該具有一個execute()方法,除此以外,它還需要可以儲存對各個對象的引用,通過execute()方法可以調用其引用的對象的方法。
那麼我們按照這個思路,來看下開燈這一操作(調用light對象的turnon()方法)的command對象應該是什麼樣的:
public class lightoncommand{
light light;
public command(light light){
this.light = light;
public void execute(){
light.turnon();
再看下開電扇(調用fan對象的start()方法)的command對象應該是什麼樣的:
public class fanstartcommand{
fan fan;
public command(fan fan){
this.fan = fan;
fan.start();
這樣顯然是不行的,它沒有解決任何的問題,因為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無需做任何修改。
// 定義空調,用于測試給遙控器添新控制類型
public class aircondition {
console.writeline("the aircondition is turned on.");
public void settemperature(int i) {
console.writeline("the temperature is set to " + i);
console.writeline("the aircondition is turned off.");
// 定義command接口
public interface icommand {
void execute();
// 定義開空調指令
public class aironcommand : icommand {
aircondition aircondition;
public aironcommand(aircondition aircondition) {
this.aircondition = aircondition;
public void execute() { //注意,你可以在execute()中添加多個方法
aircondition.start();
aircondition.settemperature(16);
// 定義關空調指令
public class airoffcommand : icommand {
public airoffcommand(aircondition aircondition) {
public void execute() {
aircondition.stop();
private icommand oncommand;
private icommand offcommand;
oncommand.execute();
offcommand.execute();
public void setcommand(icommand oncommand,icommand offcommand) {
this.oncommand = oncommand;
this.offcommand = offcommand;
// 建立遙控器對象
aircondition aircondition = new aircondition(); //建立空調對象
// 建立command對象,傳遞空調對象
icommand oncommand = new aironcommand(aircondition);
icommand offcommand = new airoffcommand(aircondition);
// 設定遙控器的command
panel.setcommand(oncommand, offcommand);
panel.presson(); //按下on按鈕,開空調,溫度調到16度
panel.pressoff(); //按下off按鈕,關空調
實際上,我們上面做的這一切,實作了另一個設計模式:command模式。現在又到了給出官方定義的時候了。每次到了這部分我就不知道該怎麼寫了,寫的人太多了,資料也太多了,我相信你看到這裡對command模式已經比較清楚了,是以我還是一如既往地從簡吧。
command模式的正式定義:将一個請求封裝為一個對象,進而使你可用不同的請求對客戶進行參數化;對請求排隊或記錄請求日志,以及支援可撤消的操作。
它的 靜态圖 是這樣的:
它的 時序圖 是這樣的:
可以和我們前面的圖對比一下,對于這兩個圖,除了改了個名字外基本沒變,我就不再說明了,也留給你一點思考的空間。
本文簡單地介紹了gof的commmand模式,我們通過一個簡單的範例家電遙控器 實作了這一模式。
我們首先了解了不使用此模式的hardcoding方式的實作方法,讨論了它的缺點;然後又換了另一種改進了的實作方法,再次讨論了它的不足。 然後,我們通過将對象的調用封裝到一個command對象中的方式,巧妙地完成了設計。最後,我們給出了command模式的正式定義。
本文僅僅簡要介紹了command模式,它的進階應用:取消操作(undo)、事務支援(transaction)、隊列請求(queuing request) 以後有時間了會再寫文章。
希望這篇文章能對你有所幫助!