天天看點

設計模式學習筆記(六)-指令模式

1.1  如何開機

估計有些朋友看到這個标題會非常奇怪,電腦裝配好了,如何開機?不就是按下啟動按鈕就可以了嗎?難道還有什麼玄機不成。

對于使用電腦的客戶——就是我們來說,開機确實很簡單,按下啟動按鈕,然後耐心等待就可以了。但是當我們按下啟動按鈕過後呢?誰來處理?如何處理?都經曆了怎樣的過程,才讓電腦真正的啟動起來,供我們使用。

先一起來簡單的認識一下電腦的啟動過程,了解一下即可。

·當我們按下啟動按鈕,電源開始向主機闆和其它裝置供電

·主機闆的系統BIOS(基本輸入輸出系統)開始加電後自檢

·主機闆的BIOS會依次去尋找顯示卡等其它裝置的BIOS,并讓它們自檢或者初始化

·開始檢測CPU、記憶體、硬碟、光驅、序列槽、并口、軟驅、即插即用裝置等等

·BIOS更新ESCD(擴充系統配置資料),ESCD是BIOS和作業系統交換硬體配置資料的一種手段

·等前面的事情都完成後,BIOS才按照使用者的配置進行系統引導,進入作業系統裡面,等到作業系統裝載并初始化完畢,就出現我們熟悉的系統登入界面了。 

1.2  與我何幹

講了一通電腦啟動的過程,有些朋友會想,這與我何幹呢?

沒錯,看起來這些硬體知識跟你沒有什麼大的關系,但是,如果現在提出一個要求:請你用軟體把上面的過程表現出來,你該如何實作?

首先把上面的過程總結一下,主要就這麼幾個步驟:首先加載電源,然後是裝置檢查,再然後是裝載系統,最後電腦就正常啟動了。可是誰來完成這些過程?如何完成?

不能讓使用電腦的客戶——就是我們來做這些工作吧,真正完成這些工作的是主機闆,那麼客戶和主機闆如何發生聯系呢?現實中,是用連接配接線把按鈕連接配接到主機闆上的,這樣當客戶按下按鈕的時候,就相當于發指令給主機闆,讓主機闆去完成後續的工作。

另外,從客戶的角度來看,開機就是按下按鈕,不管什麼樣的主機闆都是一樣的,也就是說,客戶隻管發出指令,誰接收指令,誰實作指令,如何實作,客戶是不關心的。

1.3  有何問題

把上面的問題抽象描述一下:用戶端隻是想要發出指令或者請求,不關心請求的真正接收者是誰,也不關心具體如何實作,而且同一個請求的動作可以有不同的請求内容,當然具體的處理功能也不一樣,請問該怎麼實作?

2  解決方案

2.1  指令模式來解決

用來解決上述問題的一個合理的解決方案就是指令模式。那麼什麼是指令模式呢?

(1)指令模式定義

        将一個請求封裝為一個對象,進而使你可用不同的請求對客戶進行參數化;對請求排隊或記錄請求日志,以及支援可撤銷的操作。

(2)應用指令模式來解決的思路

首先來看看實際電腦的解決方案

先畫個圖來描述一下,看看實際的電腦是如何處理上面描述的這個問題的,如圖1所示:

圖1  電腦操作示意圖

當客戶按下按鈕的時候,按鈕本身并不知道如何處理,于是通過連接配接線來請求主機闆,讓主機闆去完成真正啟動機器的功能。

這裡為了描述它們之間的關系,把主機闆畫到了機箱的外面。如果連接配接線連接配接到不同的主機闆,那麼真正執行按鈕請求的主機闆也就不同了,而客戶是不知道這些變化的。

通過引入按鈕和連接配接線,來讓發出指令的客戶和指令的真正實作者——主機闆完全解耦,客戶操作的始終是按鈕,按鈕後面的事情客戶就統統不管了。

要用程式來解決上面提出的問題,一種自然的方案就是來模拟上述解決思路。

在指令模式中,會定義一個指令的接口,用來限制所有的指令對象,然後提供具體的指令實作,每個指令實作對象是對用戶端某個請求的封裝,對應于機箱上的按鈕,一個機箱上可以有很多按鈕,也就相當于會有多個具體的指令實作對象。

在指令模式中,指令對象并不知道如何處理指令,會有相應的接收者對象來真正執行指令。就像電腦的例子,機箱上的按鈕并不知道如何處理功能,而是把這個請求轉發給主機闆,由主機闆來執行真正的功能,這個主機闆就相當于指令模式的接收者。

在指令模式中,指令對象和接收者對象的關系,并不是與生俱來的,需要有一個裝配的過程,指令模式中的Client對象就來實作這樣的功能。這就相當于在電腦的例子中,有了機箱上的按鈕,也有了主機闆,還需要有一個連接配接線把這個按鈕連接配接到主機闆上才行。

指令模式還會提供一個Invoker對象來持有指令對象,就像電腦的例子,機箱上會有多個按鈕,這個機箱就相當于指令模式的Invoker對象。這樣一來,指令模式的用戶端就可以通過Invoker來觸發并要求執行相應的指令了,這也相當于真正的客戶是按下機箱上的按鈕來操作電腦一樣。

2.2  模式結構和說明

指令模式的結構如圖2所示:

圖2  指令模式結構圖

Command:

定義指令的接口,聲明執行的方法。

ConcreteCommand:

指令接口實作對象,是“虛”的實作;通常會持有接收者,并調用接收者的功能來完成指令要執行的操作。

Receiver:

接收者,真正執行指令的對象。任何類都可能成為一個接收者,隻要它能夠實作指令要求實作的相應功能。

Invoker:

要求指令對象執行請求,通常會持有指令對象,可以持有很多的指令對象。這個是用戶端真正觸發指令并要求指令執行相應操作的地方,也就是說相當于使用指令對象的入口。

Client:

建立具體的指令對象,并且設定指令對象的接收者。注意這個不是我們正常意義上的用戶端,而是在組裝指令對象和接收者,或許,把這個Client稱為裝配者會更好了解,因為真正使用指令的用戶端是從Invoker來觸發執行。

2.3  指令模式示例代碼

(1)先來看看指令接口的定義,示例代碼如下: 

public interface Command {

    public void execute();

}

 (2)再來看看具體的指令實作對象,示例代碼如下:

public class ConcreteCommand implements Command {

    private Receiver receiver = null;

    private String state;

    public ConcreteCommand(Receiver receiver){

       this.receiver = receiver;

    }  

    public void execute() {

       //通常會轉調接收者對象的相應方法,讓接收者來真正執行功能

       receiver.action();

    }

}

(3)再來看看接收者對象的實作示意,示例代碼如下: 

public class Receiver {

    public void action(){

       //真正執行指令操作的功能代碼

    }

}

(4)接下來看看Invoker對象,示例代碼如下: 

public class Invoker {

    private Command command = null;

    public void setCommand(Command command) {

       this.command = command;

    }

    public void runCommand() {

       //調用指令對象的執行方法

       command.execute();

    }

}

(5)再來看看Client的實作,注意這個不是我們通常意義上的測試用戶端,主要功能是要建立指令對象并設定它的接收者,是以這裡并沒有調用執行的代碼,示例代碼如下: 

public class Client {

    public void assemble(){

       //建立接收者

       Receiver receiver = new Receiver();

       //建立指令對象,設定它的接收者

       Command command = new ConcreteCommand(receiver);

       //建立Invoker,把指令對象設定進去

       Invoker invoker = new Invoker();

       invoker.setCommand(command);

    }

}

2.4  使用指令模式來實作示例

       要使用指令模式來實作示例,需要先把指令模式中所涉及的各個部分,在實際的示例中對應出來,然後才能按照指令模式的結構來設計和實作程式。根據前面描述的解決思路,大緻對應如下:

·機箱上的按鈕就相當于是指令對象

·機箱相當于是Invoker

·主機闆相當于接收者對象

·指令對象持有一個接收者對象,就相當于是給機箱的按鈕連上了一根連接配接線

·當機箱上的按鈕被按下的時候,機箱就把這個指令通過連接配接線發送出去。

主機闆類才是真正實作開機功能的地方,是真正執行指令的地方,也就是“接收者”。指令的實作對象,其實是個“虛”的實作,就如同那根連接配接線,它哪知道如何實作啊,還不就是把指令傳遞給連接配接線連到的主機闆。

使用指令模式來實作示例的結構如圖3所示:

圖3  使用指令模式來實作示例的結構示意圖

還是來看看示例代碼,會比較清楚。

(1)定義主機闆

        根據前面的描述,我們會發現,真正執行客戶指令或請求的是主機闆,也隻有主機闆才知道如何去實作客戶的指令,是以先來抽象主機闆,把它用對象描述出來。

先來定義主機闆的接口,最起碼主機闆會有一個能開機的方法,示例代碼如下: 

public interface MainBoardApi {

    public void open();

}

定義了接口,那就接着定義實作類吧,定義兩個主機闆的實作類,一個是技嘉主機闆,一個是微星主機闆,現在的實作是一樣的,但是不同的主機闆對同一個指令的操作可以是不同的,這點大家要注意。由于兩個實作基本一樣,就示例一個,示例代碼如下: 

public class GigaMainBoard implements MainBoardApi{

    public void open(){

       System.out.println("技嘉主機闆現在正在開機,請等候");

       System.out.println("接通電源......");

       System.out.println("裝置檢查......");

       System.out.println("裝載系統......");

       System.out.println("機器正常運轉起來......");

       System.out.println("機器已經正常打開,請操作");

    }

}

微星主機闆的實作和這個完全一樣,隻是把技嘉改名成微星了。

(2)定義指令接口和指令的實作

         對于客戶來說,開機就是按下按鈕,别的什麼都不想做。把使用者的這個動作抽象一下,就相當于客戶發出了一個指令或者請求,其它的客戶就不關心了。為描述客戶的指令,現定義出一個指令的接口,裡面隻有一個方法,那就是執行,示例代碼如下: 

public interface Command {

    public void execute();

}

有了指令的接口,再來定義一個具體的實作,其實就是模拟現實中機箱上按鈕的功能,因為我們按下的是按鈕,但是按鈕本身是不知道如何啟動電腦的,它需要把這個指令轉給主機闆,讓主機闆去真正執行開機功能。示例代碼如下: 

public class OpenCommand implements Command{

    private MainBoardApi mainBoard = null;

    public OpenCommand(MainBoardApi mainBoard) {

       this.mainBoard = mainBoard;

    }

    public void execute() {

       //對于指令對象,根本不知道如何開機,會轉調主機闆對象

       //讓主機闆去完成開機的功能

       this.mainBoard.open();

    }

}

       由于客戶不想直接和主機闆打交道,而且客戶根本不知道具體的主機闆是什麼,客戶隻是希望按下啟動按鈕,電腦就正常啟動了,就這麼簡單。就算換了主機闆,客戶還是一樣的按下啟動按鈕就可以了。

換句話說就是:客戶想要和主機闆完全解耦,怎麼辦呢?

這就需要在客戶和主機闆之間建立一個中間對象了,客戶發出的指令傳遞給這個中間對象,然後由這個中間對象去找真正的執行者——主機闆,來完成工作。

很顯然,這個中間對象就是上面的指令實作對象,請注意:這個實作其實是個虛的實作,真正的實作是主機闆完成的,在這個虛的實作裡面,是通過轉調主機闆的功能來實作的,主機闆對象執行個體,是從外面傳進來的。

(3)提供機箱

客戶需要操作按鈕,按鈕是放置在機箱之上的,是以需要把機箱也定義出來,示例代碼如下: 

public class Box {

    private Command openCommand;

    public void setOpenCommand(Command command){

       this.openCommand = command;

    }

    public void openButtonPressed(){

       //按下按鈕,執行指令

       openCommand.execute();

    }

}

(4)客戶使用按鈕

抽象好了機箱和主機闆,指令對象也準備好了,客戶想要使用按鈕來完成開機的功能,在使用之前,客戶的第一件事情就應該是把按鈕和主機闆組裝起來,形成一個完整的機器。

在實際生活中,是由裝機工程師來完成這部分工作,這裡為了測試簡單,直接寫在用戶端開頭了。機器組裝好過後,客戶應該把與主機闆連接配接好的按鈕對象放置到機箱上,等待客戶随時操作。把這個過程也用代碼描述出來,示例代碼如下: 

public class Client {

    public static void main(String[] args) {

       //1:把指令和真正的實作組合起來,相當于在組裝機器,

       //把機箱上按鈕的連接配接線插接到主機闆上。

       MainBoardApi mainBoard = new GigaMainBoard();

       OpenCommand openCommand = new OpenCommand(mainBoard);

       //2:為機箱上的按鈕設定對應的指令,讓按鈕知道該幹什麼

       Box box = new Box();

       box.setOpenCommand(openCommand);

       //3:然後模拟按下機箱上的按鈕

       box.openButtonPressed();

    }

}

運作一下,看看效果,輸出如下: 

技嘉主機闆現在正在開機,請等候

接通電源......

裝置檢查......

裝載系統......

機器正常運轉起來......

機器已經正常打開,請操作

你可以給指令對象組裝不同的主機闆實作類,然後再次測試,看看效果。

事實上,你會發現,如果對象結構已經組裝好了過後,對于真正的用戶端,也就是真實的使用者而言,任務就是面對機箱,按下機箱上的按鈕,就可以執行開機的指令了,實際生活中也是這樣的。

(5)小結

如同前面的示例,把客戶的開機請求封裝成為一個 OpenCommand 對象,客戶的開機操作就變成了執行 OpenCommand 對象的方法了?如果還有其它的指令對象,比如讓機器重新開機的 ResetCommand 對象;那麼客戶按下按鈕的動作,就可以用這不同的指令對象去比對,也就是對客戶進行參數化。

用大白話描述就是:客戶按下一個按鈕,到底是開機還是重新開機,那要看參數化配置的是哪一個具體的按鈕對象,如果參數化的是開機的指令對象,那就執行開機的功能,如果參數化的是重新開機的指令對象,那就執行重新開機