1 場景問題#
1.1 如何開機##
估計有些朋友看到這個標題會非常奇怪,電腦裝配好了,如何開機?不就是按下啟動按鈕就可以了嗎?難道還有什么玄機不成。
對于使用電腦的客戶——就是我們來說,開機確實很簡單,按下啟動按鈕,然后耐心等待就可以了。但是當我們按下啟動按鈕過后呢?誰來處理?如何處理?都經歷了怎樣的過程,才讓電腦真正的啟動起來,供我們使用。
先一起來簡單的認識一下電腦的啟動過程,了解一下即可。
當我們按下啟動按鈕,電源開始向主板和其它設備供電
主板的系統BIOS(基本輸入輸出系統)開始加電后自檢
主板的BIOS會依次去尋找顯卡等其它設備的BIOS,并讓它們自檢或者初始化
開始檢測CPU、內存、硬盤、光驅、串口、并口、軟驅、即插即用設備等等
BIOS更新ESCD(擴展系統配置數據),ESCD是BIOS和操作系統交換硬件配置數據的一種手段
等前面的事情都完成后,BIOS才按照用戶的配置進行系統引導,進入操作系統里面,等到操作系統裝載并初始化完畢,就出現我們熟悉的系統登錄界面了。
1.2 與我何干##
講了一通電腦啟動的過程,有些朋友會想,這與我何干呢?
沒錯,看起來這些硬件知識跟你沒有什么大的關系,但是,如果現在提出一個要求:請你用軟件把上面的過程表現出來,你該如何實現?
首先把上面的過程總結一下,主要就這么幾個步驟:首先加載電源,然后是設備檢查,再然后是裝載系統,最后電腦就正常啟動了。可是誰來完成這些過程?如何完成?
不能讓使用電腦的客戶——就是我們來做這些工作吧,真正完成這些工作的是主板,那么客戶和主板如何發生聯系呢?現實中,是用連接線把按鈕連接到主板上的,這樣當客戶按下按鈕的時候,就相當于發命令給主板,讓主板去完成后續的工作。
另外,從客戶的角度來看,開機就是按下按鈕,不管什么樣的主板都是一樣的
,也就是說,客戶只管發出命令,誰接收命令,誰實現命令,如何實現,客戶是不關心的
。
1.3 有何問題##
把上面的問題抽象描述一下:客戶端只是想要發出命令或者請求,不關心請求的真正接收者是誰,也不關心具體如何實現,而且同一個請求的動作可以有不同的請求內容,當然具體的處理功能也不一樣
,請問該怎么實現?
2 解決方案#
2.1 命令模式來解決##
用來解決上述問題的一個合理的解決方案就是命令模式。那么什么是命令模式呢?
- 命令模式定義
將一個請求封裝為一個對象,從而使你可用不同的請求對客戶進行參數化;對請求排隊或記錄請求日志,以及支持可撤銷的操作。
- 應用命令模式來解決的思路
首先來看看實際電腦的解決方案,先畫個圖來描述一下,看看實際的電腦是如何處理上面描述的這個問題的,如圖所示:
當客戶按下按鈕的時候,按鈕本身并不知道如何處理,于是通過連接線來請求主板,讓主板去完成真正啟動機器的功能。
這里為了描述它們之間的關系,把主板畫到了機箱的外面。如果連接線連接到不同的主板,那么真正執行按鈕請求的主板也就不同了,而客戶是不知道這些變化的。
通過引入按鈕和連接線,來讓發出命令的客戶和命令的真正實現者——主板完全解耦,客戶操作的始終是按鈕,按鈕后面的事情客戶就統統不管了。
要用程序來解決上面提出的問題,一種自然的方案就是來模擬上述解決思路。
在命令模式中,會定義一個命令的接口,用來約束所有的命令對象,然后提供具體的命令實現,每個命令實現對象是對客戶端某個請求的封裝
,對應于機箱上的按鈕,一個機箱上可以有很多按鈕,也就相當于會有多個具體的命令實現對象。在命令模式中,命令對象并不知道如何處理命令,會有相應的接收者對象來真正執行命令
。就像電腦的例子,機箱上的按鈕并不知道如何處理功能,而是把這個請求轉發給主板,由主板來執行真正的功能,這個主板就相當于命令模式的接收者。在命令模式中,命令對象和接收者對象的關系,并不是與生俱來的,需要有一個裝配的過程,命令模式中的Client對象就來實現這樣的功能
。這就相當于在電腦的例子中,有了機箱上的按鈕,也有了主板,還需要有一個連接線把這個按鈕連接到主板上才行。命令模式還會提供一個Invoker對象來持有命令對象
,就像電腦的例子,機箱上會有多個按鈕,這個機箱就相當于命令模式的Invoker對象。這樣一來,命令模式的客戶端就可以通過Invoker來觸發并要求執行相應的命令了,這也相當于真正的客戶是按下機箱上的按鈕來操作電腦一樣。
2.2 模式結構和說明##
命令模式的結構如圖所示:
Command:
定義命令的接口,聲明執行的方法
。ConcreteCommand:
命令接口實現對象,是“虛”的實現
;通常會持有接收者,并調用接收者的功能來完成命令要執行的操作。Receiver:
接收者,真正執行命令的對象
。任何類都可能成為一個接收者,只要它能夠實現命令要求實現的相應功能。Invoker:
要求命令對象執行請求,通常會持有命令對象,可以持有很多的命令對象
。這個是客戶端真正觸發命令并要求命令執行相應操作的地方,也就是說相當于使用命令對象的入口。Client:創建具體的命令對象,并且設置命令對象的接收者。注意這個不是我們常規意義上的客戶端,
而是在組裝命令對象和接收者,或許,把這個Client稱為裝配者會更好理解
,因為真正使用命令的客戶端是從Invoker來觸發執行。
2.3 命令模式示例代碼##
- 先來看看命令接口的定義,示例代碼如下:
/**
* 命令接口,聲明執行的操作
*/
public interface Command {
/**
* 執行命令對應的操作
*/
public void execute();
}
- 再來看看具體的命令實現對象,示例代碼如下:
/**
* 具體的命令實現對象
*/
public class ConcreteCommand implements Command {
/**
* 持有相應的接收者對象
*/
private Receiver receiver = null;
/**
* 示意,命令對象可以有自己的狀態
*/
private String state;
/**
* 構造方法,傳入相應的接收者對象
* @param receiver 相應的接收者對象
*/
public ConcreteCommand(Receiver receiver){
this.receiver = receiver;
}
public void execute() {
//通常會轉調接收者對象的相應方法,讓接收者來真正執行功能
receiver.action();
}
}
- 再來看看接收者對象的實現示意,示例代碼如下:
/**
* 接收者對象
*/
public class Receiver {
/**
* 示意方法,真正執行命令相應的操作
*/
public void action(){
//真正執行命令操作的功能代碼
}
}
- 接下來看看Invoker對象,示例代碼如下:
/**
* 調用者
*/
public class Invoker {
/**
* 持有命令對象
*/
private Command command = null;
/**
* 設置調用者持有的命令對象
* @param command 命令對象
*/
public void setCommand(Command command) {
this.command = command;
}
/**
* 示意方法,要求命令執行請求
*/
public void runCommand() {
//調用命令對象的執行方法
command.execute();
}
}
- 再來看看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
主板相當于接收者對象
命令對象持有一個接收者對象,就相當于是給機箱的按鈕連上了一根連接線,當機箱上的按鈕被按下的時候,機箱就把這個命令通過連接線發送出去。
主板類才是真正實現開機功能的地方,是真正執行命令的地方,也就是“接收者”。命令的實現對象,其實是個“虛”的實現,就如同那根連接線,它哪知道如何實現啊,還不就是把命令傳遞給連接線連到的主板。
使用命令模式來實現示例的結構如圖所示:
- 定義主板
根據前面的描述,我們會發現,真正執行客戶命令或請求的是主板,也只有主板才知道如何去實現客戶的命令,因此先來抽象主板,把它用對象描述出來。
先來定義主板的接口,最起碼主板會有一個能開機的方法,示例代碼如下:
/**
* 主板的接口
*/
public interface MainBoardApi {
/**
* 主板具有能開機的功能
*/
public void open();
}
定義了接口,那就接著定義實現類吧,定義兩個主板的實現類,一個是技嘉主板,一個是微星主板,現在的實現是一樣的,但是不同的主板對同一個命令的操作可以是不同的,這點大家要注意。由于兩個實現基本一樣,就示例一個,示例代碼如下:
/**
* 技嘉主板類,開機命令的真正實現者,在Command模式中充當Receiver
*/
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("機器已經正常打開,請操作");
}
}
- 定義命令接口和命令的實現
對于客戶來說,開機就是按下按鈕,別的什么都不想做。把用戶的這個動作抽象一下,就相當于客戶發出了一個命令或者請求,其它的客戶就不關心了
。為描述客戶的命令,現定義出一個命令的接口,里面只有一個方法,那就是執行,示例代碼如下:
/**
* 命令接口,聲明執行的操作
*/
public interface Command {
/**
* 執行命令對應的操作
*/
public void execute();
}
有了命令的接口,再來定義一個具體的實現,其實就是模擬現實中機箱上按鈕的功能,因為我們按下的是按鈕,但是按鈕本身是不知道如何啟動電腦的,它需要把這個命令轉給主板,讓主板去真正執行開機功能
。示例代碼如下:
/**
* 開機命令的實現,實現Command接口,
* 持有開機命令的真正實現,通過調用接收者的方法來實現命令
*/
public class OpenCommand implements Command{
/**
* 持有真正實現命令的接收者——主板對象
*/
private MainBoardApi mainBoard = null;
/**
* 構造方法,傳入主板對象
* @param mainBoard 主板對象
*/
public OpenCommand(MainBoardApi mainBoard) {
this.mainBoard = mainBoard;
}
public void execute() {
//對于命令對象,根本不知道如何開機,會轉調主板對象
//讓主板去完成開機的功能
this.mainBoard.open();
}
}
由于客戶不想直接和主板打交道,而且客戶根本不知道具體的主板是什么,客戶只是希望按下啟動按鈕,電腦就正常啟動了,就這么簡單。就算換了主板,客戶還是一樣的按下啟動按鈕就可以了。
換句話說就是:客戶想要和主板完全解耦,怎么辦呢?
這就需要在客戶和主板之間建立一個中間對象了,客戶發出的命令傳遞給這個中間對象,然后由這個中間對象去找真正的執行者——主板,來完成工作。
很顯然,這個中間對象就是上面的命令實現對象,請注意:這個實現其實是個虛的實現,真正的實現是主板完成的,在這個虛的實現里面,是通過轉調主板的功能來實現的,主板對象實例,是從外面傳進來的
。
- 提供機箱
/**
* 機箱對象,本身有按鈕,持有按鈕對應的命令對象
*/
public class Box {
/**
* 開機命令對象
*/
private Command openCommand;
/**
* 設置開機命令對象
* @param command 開機命令對象
*/
public void setOpenCommand(Command command){
this.openCommand = command;
}
/**
* 提供給客戶使用,接收并響應用戶請求,相當于按鈕被按下觸發的方法
*/
public void openButtonPressed(){
//按下按鈕,執行命令
openCommand.execute();
}
}
- 客戶使用按鈕
抽象好了機箱和主板,命令對象也準備好了,客戶想要使用按鈕來完成開機的功能,在使用之前,客戶的第一件事情就應該是把按鈕和主板組裝起來,形成一個完整的機器
。
在實際生活中,是由裝機工程師來完成這部分工作,這里為了測試簡單,直接寫在客戶端開頭了。機器組裝好過后,客戶應該把與主板連接好的按鈕對象放置到機箱上,等待客戶隨時操作。把這個過程也用代碼描述出來,示例代碼如下:
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();
}
}
- 小結
如同前面的示例,把客戶的開機請求封裝成為一個OpenCommand對象,客戶的開機操作就變成了執行OpenCommand對象的方法了?如果還有其它的命令對象,比如讓機器重啟的ResetCommand對象;那么客戶按下按鈕的動作,就可以用這不同的命令對象去匹配,也就是對客戶進行參數化
。
用大白話描述就是:客戶按下一個按鈕,到底是開機還是重啟,那要看參數化配置的是哪一個具體的按鈕對象,如果參數化的是開機的命令對象,那就執行開機的功能,如果參數化的是重啟的命令對象,那就執行重啟的功能。雖然按下的是同一個按鈕,但是請求是不同的,對應執行的功能也就不同了
。
在模式講解的時候會給大家一個參數化配置的示例,這里就不多講了。至于對請求排隊或記錄請求日志,以及支持可撤銷的操作等功能,也放到模式講解里面。
3 模式講解#
3.1 認識命令模式##
- 命令模式的關鍵
命令模式的關鍵之處就是把請求封裝成為對象,也就是命令對象,并定義了統一的執行操作的接口
,這個命令對象可以被存儲、轉發、記錄、處理、撤銷等,整個命令模式都是圍繞這個對象在進行
。
- 命令模式的組裝和調用
在命令模式中經常會有一個命令的組裝者,用它來維護命令的“虛”實現和真實實現之間的關系
。如果是超級智能的命令,也就是說命令對象自己完全實現好了,不需要接收者,那就是命令模式的退化,不需要接收者,自然也不需要組裝者了。
而真正的用戶就是具體化請求的內容,然后提交請求進行觸發就好了。真正的用戶會通過invoker來觸發命令。
在實際開發過程中,Client和Invoker可以融合在一起,由客戶在使用命令模式的時候,先進行命令對象和接收者的組裝,組裝完成后,就可以調用命令執行請求
。
- 命令模式的接收者
接收者可以是任意的類,對它沒有什么特殊要求,這個對象知道如何真正執行命令的操作,執行時是從command的實現類里面轉調過來。
一個接收者對象可以處理多個命令,接收者和命令之間沒有約定的對應關系
。接收者提供的方法個數、名稱、功能和命令中的可以不一樣,只要能夠通過調用接收者的方法來實現命令對應的功能就可以了。
- 智能命令
在標準的命令模式里面,命令的實現類是沒有真正實現命令要求的功能的,真正執行命令的功能的是接收者
。
如果命令的實現對象比較智能,它自己就能真實地實現命令要求的功能,而不再需要調用接收者,那么這種情況就稱為智能命令
。
也可以有半智能的命令,命令對象知道部分實現,其它的還是需要調用接收者來完成,也就是說命令的功能由命令對象和接收者共同來完成。
- 發起請求的對象和真正實現的對象是解耦的
請求究竟由誰處理,如何處理,發起請求的對象是不知道的,也就是發起請求的對象和真正實現的對象是解耦的。發起請求的對象只管發出命令,其它的就不管了。
- 命令模式的調用順序示意圖
使用命令模式的過程分成兩個階段,一個階段是組裝命令對象和接收者對象的過程,另外一個階段是觸發調用Invoker,來讓命令真正執行的過程
。 先看看組裝過程的調用順序示意圖,如圖所示:
接下來再看看真正執行命令時的調用順序示意圖,如圖所示:
3.2 參數化配置##
所謂命令模式的參數化配置,指的是:可以用不同的命令對象,去參數化配置客戶的請求。
像前面描述的那樣:客戶按下一個按鈕,到底是開機還是重啟,那要看參數化配置的是哪一個具體的按鈕對象,如果參數化的是開機的命令對象,那就執行開機的功能,如果參數化的是重啟的命令對象,那就執行重啟的功能。雖然按下的是同一個按鈕,相當于是同一個請求,但是為請求配置不同的按鈕對象,那就會執行不同的功能。
把這個功能用代碼實現出來,一起來體會一下命令模式的參數化配置。
- 同樣先定義主板接口吧,現在想要添加一個重啟的按鈕,因此主板需要添加一個方法來實現重啟的功能,示例代碼如下:
/**
* 主板的接口
*/
public interface MainBoardApi {
/**
* 主板具有能開機的功能
*/
public void open();
/**
* 主板具有實現重啟的功能
*/
public void reset();
}
接口發生了改變,實現類也得有相應的改變,由于兩個主板的實現示意差不多,因此還是只示例一個,示例代碼如下:
/**
* 技嘉主板類,命令的真正實現者,在Command模式中充當Receiver
*/
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("機器已經正常打開,請操作");
}
/**
* 真正的重新啟動機器命令的實現
*/
public void reset(){
System.out.println("技嘉主板現在正在重新啟動機器,請等候");
System.out.println("機器已經正常打開,請操作");
}
}
- 該來定義命令和按鈕了,命令接口沒有任何變化,原有的開機命令的實現也沒有任何變化,只是新添加了一個重啟命令的實現,示例代碼如下:
/**
* 重啟機器命令的實現,實現Command接口,
* 持有重啟機器命令的真正實現,通過調用接收者的方法來實現命令
*/
public class ResetCommand implements Command{
/**
* 持有真正實現命令的接收者——主板對象
*/
private MainBoardApi mainBoard = null;
/**
* 構造方法,傳入主板對象
* @param mainBoard 主板對象
*/
public ResetCommand(MainBoardApi mainBoard) {
this.mainBoard = mainBoard;
}
public void execute() {
//對于命令對象,根本不知道如何重啟機器,會轉調主板對象
//讓主板去完成重啟機器的功能
this.mainBoard.reset();
}
}
- 持有命令的機箱也需要修改,現在不只一個命令按鈕了,有兩個了,所以需要在機箱類里面新添加重啟的按鈕,為了簡單,沒有做成集合。示例代碼如下:
/**
* 機箱對象,本身有按鈕,持有按鈕對應的命令對象
*/
public class Box {
private Command openCommand;
public void setOpenCommand(Command command){
this.openCommand = command;
}
public void openButtonPressed(){
//按下按鈕,執行命令
openCommand.execute();
}
/**
* 重啟機器命令對象
*/
private Command resetCommand;
/**
* 設置重啟機器命令對象
* @param command
*/
public void setResetCommand(Command command){
this.resetCommand = command;
}
/**
* 提供給客戶使用,接收并相應用戶請求,相當于重啟按鈕被按下觸發的方法
*/
public void resetButtonPressed(){
//按下按鈕,執行命令
resetCommand.execute();
}
}
- 看看客戶如何使用這兩個按鈕,示例代碼如下:
public class Client {
public static void main(String[] args) {
//1:把命令和真正的實現組合起來,相當于在組裝機器,
//把機箱上按鈕的連接線插接到主板上。
MainBoardApi mainBoard = new GigaMainBoard();
//創建開機命令
OpenCommand openCommand = new OpenCommand(mainBoard);
//創建重啟機器的命令
ResetCommand resetCommand = new ResetCommand(mainBoard);
//2:為機箱上的按鈕設置對應的命令,讓按鈕知道該干什么
Box box = new Box();
//先正確配置,就是開機按鈕對開機命令,重啟按鈕對重啟命令
box.setOpenCommand(openCommand);
box.setResetCommand(resetCommand);
//3:然后模擬按下機箱上的按鈕
System.out.println("正確配置下------------------------->");
System.out.println(">>>按下開機按鈕:>>>");
box.openButtonPressed();
System.out.println(">>>按下重啟按鈕:>>>");
box.resetButtonPressed();
//然后來錯誤配置一回,反正是進行參數化配置
//就是開機按鈕對重啟命令,重啟按鈕對開機命令
box.setOpenCommand(resetCommand);
box.setResetCommand(openCommand);
//4:然后還是來模擬按下機箱上的按鈕
System.out.println("錯誤配置下------------------------->");
System.out.println(">>>按下開機按鈕:>>>");
box.openButtonPressed();
System.out.println(">>>按下重啟按鈕:>>>");
box.resetButtonPressed();
}
}
運行一下看看,很有意思,結果如下:
3.3 可撤銷的操作##
可撤銷操作的意思就是:放棄該操作,回到未執行該操作前的狀態
。這個功能是一個非常重要的功能,幾乎所有GUI應用里面都有撤消操作的功能。GUI的菜單是命令模式最典型的應用之一,所以你總是能在菜單上找到撤銷這樣的菜單項。
既然這么常用,那該如何實現呢?
有兩種基本的思路來實現可撤銷的操作,一種是補償式,又稱反操作式
:比如被撤銷的操作是加的功能,那撤消的實現就變成減的功能;同理被撤銷的操作是打開的功能,那么撤銷的實現就變成關閉的功能。
另外一種方式是存儲恢復式,意思就是把操作前的狀態記錄下來,然后要撤銷操作的時候就直接恢復回去就可以了
。
這里先講第一種方式,就是補償式或者反操作式,第二種方式放到備忘錄模式中去講解。為了讓大家更好的理解可撤銷操作的功能,還是用一個例子來說明會比較清楚。
- 范例需求
考慮一個計算器的功能,最簡單的那種,只能實現加減法運算,現在要讓這個計算器支持可撤銷的操作。
- 補償式或者反操作式的解決方案
(1)在實現命令接口之前,先來定義真正實現計算的接口,沒有它命令什么都做不了,操作運算的接口的示例代碼如下:
/**
* 操作運算的接口
*/
public interface OperationApi {
/**
* 獲取計算完成后的結果
* @return 計算完成后的結果
*/
public int getResult();
/**
* 設置計算開始的初始值
* @param result 計算開始的初始值
*/
public void setResult(int result);
/**
* 執行加法
* @param num 需要加的數
*/
public void add(int num);
/**
* 執行減法
* @param num 需要減的數
*/
public void substract(int num);
}
定義了接口,來看看真正執行加減法的實現,示例代碼如下:
/**
* 運算類,真正實現加減法運算
*/
public class Operation implements OperationApi{
/**
* 記錄運算的結果
*/
private int result;
public int getResult() {
return result;
}
public void setResult(int result) {
this.result = result;
}
public void add(int num) {
//實現加法 功能
result += num;
}
public void substract(int num) {
//實現減法 功能
result -= num;
}
}
(2)接下來,來抽象命令接口,由于要支持可撤銷的功能,所以除了跟前面一樣定義一個執行方法外,還需要定義一個撤銷操作的方法,示例代碼如下:
/**
* 命令接口,聲明執行的操作,支持可撤銷操作
*/
public interface Command {
/**
* 執行命令對應的操作
*/
public void execute();
/**
* 執行撤銷命令對應的操作
*/
public void undo();
}
(3)應該來實現命令了,具體的命令分成了加法命令和減法命令,先來看看加法命令的實現,示例代碼如下:
/**
* 具體的加法命令實現對象
*/
public class AddCommand implements Command{
/**
* 持有具體執行計算的對象
*/
private OperationApi operation = null;
/**
* 操作的數據,也就是要加上的數據
*/
private int opeNum;
public void execute() {
//轉調接收者去真正執行功能,這個命令是做加法
this.operation.add(opeNum);
}
public void undo() {
//轉調接收者去真正執行功能
//命令本身是做加法,那么撤銷的時候就是做減法了
this.operation.substract(opeNum);
}
/**
* 構造方法,傳入具體執行計算的對象
* @param operation 具體執行計算的對象
* @param opeNum 要加上的數據
*/
public AddCommand(OperationApi operation,int opeNum){
this.operation = operation;
this.opeNum = opeNum;
}
}
減法命令和加法類似,只是在實現的時候和加法反過來了,示例代碼如下:
/**
* 具體的減法命令實現對象
*/
public class SubstractCommand implements Command{
/**
* 持有具體執行計算的對象
*/
private OperationApi operation = null;
/**
* 操作的數據,也就是要減去的數據
*/
private int opeNum;
/**
* 構造方法,傳入具體執行計算的對象
* @param operation 具體執行計算的對象
* @param opeNum 要減去的數據
*/
public SubstractCommand(OperationApi operation,int opeNum){
this.operation = operation;
this.opeNum = opeNum;
}
public void execute() {
//轉調接收者去真正執行功能,這個命令是做減法
this.operation.substract(opeNum);
}
public void undo() {
//轉調接收者去真正執行功能
//命令本身是做減法,那么撤銷的時候就是做加法了
this.operation.add(opeNum);
}
}
(4)接下來應該看看計算器了,計算器就相當于Invoker,持有多個命令對象,計算器是實現可撤銷操作的地方。
為了大家更好的理解可撤銷的功能,先來看看不加可撤銷操作的計算器類什么樣子,然后再添加上可撤銷的功能示例。示例代碼如下:
/**
* 計算器類,計算器上有加法按鈕、減法按鈕
*/
public class Calculator {
/**
* 持有執行加法的命令對象
*/
private Command addCmd = null;
/**
* 持有執行減法的命令對象
*/
private Command substractCmd = null;
/**
* 設置執行加法的命令對象
* @param addCmd 執行加法的命令對象
*/
public void setAddCmd(Command addCmd) {
this.addCmd = addCmd;
}
/**
* 設置執行減法的命令對象
* @param substractCmd 執行減法的命令對象
*/
public void setSubstractCmd(Command substractCmd) {
this.substractCmd = substractCmd;
}
/**
* 提供給客戶使用,執行加法 功能
*/
public void addPressed(){
this.addCmd.execute();
}
/**
* 提供給客戶使用,執行減法 功能
*/
public void substractPressed(){
this.substractCmd.execute();
}
}
目前看起來跟前面的例子實現得差不多,現在就在這個基本的實現上來添加可撤銷操作的功能。
要想實現可撤銷操作,首先就需要把操作過的命令記錄下來,形成命令的歷史列表,撤銷的時候就從最后一個開始執行撤銷
。因此我們先在計算器類里面加上命令歷史列表,示例代碼如下:
/**
* 命令的操作的歷史記錄,在撤銷時候用
*/
private List<Command> undoCmds = new ArrayList<Command>();
什么時候向命令的歷史記錄里面加值呢?
很簡單,答案是在每個操作按鈕被按下的時候,也就是你操作加法按鈕或者減法按鈕的時候,示例代碼如下:
public void addPressed(){
this.addCmd.execute();
//把操作記錄到歷史記錄里面
undoCmds.add(this.addCmd);
}
public void substractPressed(){
this.substractCmd.execute();
//把操作記錄到歷史記錄里面
undoCmds.add(this.substractCmd);
}
然后在計算器類里面添加上一個撤銷的按鈕,如果它被按下,那么就從命令歷史記錄里取出最后一個命令來撤銷,撤消完成后要把已經撤銷的命令從歷史記錄里面刪除掉,相當于沒有執行過該命令了,示例代碼如下:
public void undoPressed(){
if(this.undoCmds.size()>0){
//取出最后一個命令來撤銷
Command cmd = this.undoCmds.get(this.undoCmds.size()-1);
cmd.undo();
//然后把最后一個命令刪除掉,
this.undoCmds.remove(cmd);
}else{
System.out.println("很抱歉,沒有可撤銷的命令");
}
}
同樣的方式,還可以實現恢復的功能,也為恢復設置一個可恢復的列表,需要恢復的時候從列表里面取最后一個命令進行重新執行就好了,示例代碼如下:
/**
* 命令被撤銷的歷史記錄,在恢復時候用
*/
private List<Command> redoCmds = new ArrayList<Command>();
那么什么時候向這個集合里面賦值呢?
大家要注意,恢復的命令數據是來源于撤銷的命令,也就是說有撤銷才會有恢復,所以在撤銷的時候向這個集合里面賦值
,注意要在撤銷的命令被刪除前賦值。示例代碼如下:
public void undoPressed(){
if(this.undoCmds.size()>0){
//取出最后一個命令來撤銷
Command cmd = this.undoCmds.get(this.undoCmds.size()-1);
cmd.undo();
//如果還有恢復的功能,那就把這個命令記錄到恢復的歷史記錄里面
this.redoCmds.add(cmd);
//然后把最后一個命令刪除掉
this.undoCmds.remove(cmd);
}else{
System.out.println("很抱歉,沒有可撤銷的命令");
}
}
那么如何實現恢復呢?請看示例代碼:
public void redoPressed() {
if(this.redoCmds.size()>0) {
//取出最后一個命令來重做
Command cmd = this.redoCmds.get(this.redoCmds.size()-1);
cmd.execute();
//把這個命令記錄到可撤銷的歷史記錄里面
this.undoCmds.add(cmd);
//然后把最后一個命令刪除掉
this.redoCmds.remove(cmd);
}else{
System.out.println("很抱歉,沒有可恢復的命令");
}
}
好了,分步講解了計算器類,一起來看看完整的計算器類的代碼:
/**
* 計算器類,計算器上有加法按鈕、減法按鈕,還有撤銷和恢復的按鈕
*/
public class Calculator {
/**
* 命令的操作的歷史記錄,在撤銷時候用
*/
private List<Command> undoCmds = new ArrayList<Command>();
/**
* 命令被撤銷的歷史記錄,在恢復時候用
*/
private List<Command> redoCmds = new ArrayList<Command>();
private Command addCmd = null;
private Command substractCmd = null;
public void setAddCmd(Command addCmd) {
this.addCmd = addCmd;
}
public void setSubstractCmd(Command substractCmd) {
this.substractCmd = substractCmd;
}
public void addPressed(){
this.addCmd.execute();
//把操作記錄到歷史記錄里面
undoCmds.add(this.addCmd);
}
public void substractPressed(){
this.substractCmd.execute();
//把操作記錄到歷史記錄里面
undoCmds.add(this.substractCmd);
}
public void undoPressed(){
if(this.undoCmds.size()>0){
//取出最后一個命令來撤銷
Command cmd = this.undoCmds.get(undoCmds.size()-1);
cmd.undo();
//如果還有恢復的功能,那就把這個命令記錄到恢復的歷史記錄里面
this.redoCmds.add(cmd );
//然后把最后一個命令刪除掉,
this.undoCmds.remove(cmd);
}else{
System.out.println("很抱歉,沒有可撤銷的命令");
}
}
public void redoPressed(){
if(this.redoCmds.size()>0){
//取出最后一個命令來重做
Command cmd = this.redoCmds.get(redoCmds.size()-1);
cmd.execute();
//把這個命令記錄到可撤銷的歷史記錄里面
this.undoCmds.add(cmd);
//然后把最后一個命令刪除掉
this.redoCmds.remove(cmd);
}else{
System.out.println("很抱歉,沒有可恢復的命令");
}
}
}
(5)終于到可以收獲的時候了,寫個客戶端,組裝好命令和接收者,然后操作幾次命令,來測試一下撤銷和恢復的功能,示例代碼如下:
public class Client {
public static void main(String[] args) {
//1:組裝命令和接收者
//創建接收者
OperationApi operation = new Operation();
//創建命令對象,并組裝命令和接收者
AddCommand addCmd = new AddCommand(operation,5);
SubstractCommand substractCmd = new SubstractCommand(operation,3);
//2:把命令設置到持有者,就是計算器里面
Calculator calculator = new Calculator();
calculator.setAddCmd(addCmd);
calculator.setSubstractCmd(substractCmd);
//3:模擬按下按鈕,測試一下
calculator.addPressed();
System.out.println("一次加法運算后的結果為:" +operation.getResult());
calculator.substractPressed();
System.out.println("一次減法運算后的結果為:" +operation.getResult());
//測試撤消
calculator.undoPressed();
System.out.println("撤銷一次后的結果為:" +operation.getResult());
calculator.undoPressed();
System.out.println("再撤銷一次后的結果為:" +operation.getResult());
//測試恢復
calculator.redoPressed();
System.out.println("恢復操作一次后的結果為:" +operation.getResult());
calculator.redoPressed();
System.out.println("再恢復操作一次后的結果為:" +operation.getResult());
}
}
(6)運行一下,看看結果,享受一下可以撤銷和恢復的操作,結果如下:
一次加法運算后的結果為:5
一次減法運算后的結果為:2
撤銷一次后的結果為:5
再撤銷一次后的結果為:0
恢復操作一次后的結果為:5
再恢復操作一次后的結果為:2
3.4 宏命令##
什么是宏命令呢?簡單點說就是包含多個命令的命令,是一個命令的組合
。舉個例子來說吧,設想一下你去飯店吃飯的過程:
(1)你走進一家飯店,找到座位坐下
(2)服務員走過來,遞給你菜譜
(3)你開始點菜,服務員開始記錄菜單,菜單是三聯的,點菜完畢,服務員就會把菜單分成三份,一份給后廚,一份給收銀臺,一份保留備查。
(4)點完菜,你坐在座位上等候,后廚會按照菜單做菜
(5)每做好一份菜,就會由服務員送到你桌子上
(6)然后你就可以大快朵頤了
事實上,到飯店點餐是一個很典型的命令模式應用,作為客戶的你,只需要發出命令,就是要吃什么菜,每道菜就相當于一個命令對象,服務員會在菜單上記錄你點的菜,然后把菜單傳遞給后廚,后廚拿到菜單,會按照菜單進行飯菜制作,后廚就相當于接收者,是命令的真正執行者,廚師才知道每道菜具體怎么實現。
在這個過程中,地位比較特殊的是服務員,在不考慮更復雜的管理,比如后廚管理的時候,負責命令和接收者的組裝的就是服務員。比如你點了涼菜、熱菜,你其實是不知道到底涼菜由誰來完成,熱菜由誰來完成的,因此你只管發命令,而組裝的工作就由服務員完成了,服務員知道涼菜送到涼菜部,那是已經做好的了,熱菜才送到后廚,需要廚師現做,看起來服務員是一個組裝者。
同時呢,服務員還持有命令對象,也就是菜單,最后啟動命令執行的也是服務員。因此,服務員就相當于標準命令模式中的Client和Invoker的融合
。
畫個圖來描述上述對應關系,如圖所示:
- 宏命令在哪里?
仔細觀察上面的過程,再想想前面的命令模式的實現,看出點什么沒有?
前面實現的命令模式,都是客戶端發出一個命令,然后馬上就執行了這個命令,但是在上面的描述里面呢?是點一個菜,服務員就告訴廚師,然后廚師就開始做嗎?很明顯不是的,服務員會一直等,等到你點完菜,當你說“點完了”的時候,服務員才會啟動命令的執行,請注意,這個時候執行的就不是一個命令了,而是執行一堆命令。
描述這一堆命令的就是菜單,如果把菜單也抽象成為一個命令,就相當于一個大的命令,當客戶說“點完了”的時候,就相當于觸發這個大的命令,意思就是執行菜單這個命令就可以了,這個菜單命令包含多個命令對象,一個命令對象就相當于一道菜。
那么這個菜單就相當于我們說的宏命令。
- 如何實現宏命令
(1)先來定義接收者,就是廚師的接口和實現,先看接口,示例代碼如下:
/**
* 廚師的接口
*/
public interface CookApi {
/**
* 示意,做菜的方法
* @param name 菜名
*/
public void cook(String name);
}
廚師又分成兩類,一類是做熱菜的師傅,一類是做涼菜的師傅,先看看做熱菜的廚師的實現示意,示例代碼如下:
/**
* 廚師對象,做熱菜
*/
public class HotCook implements CookApi{
public void cook(String name) {
System.out.println("本廚師正在做:"+name);
}
}
/**
* 廚師對象,做涼菜
*/
public class CoolCook implements CookApi {
public void cook(String name) {
System.out.println("涼菜"+name+"已經做好,本廚師正在裝盤。" );
}
}
(2)接下來,來定義命令接口,跟以前一樣,示例代碼如下:
/**
* 命令接口,聲明執行的操作
*/
public interface Command {
/**
* 執行命令對應的操作
*/
public void execute();
}
(3)定義好了命令的接口,該來具體實現命令了:
實現方式跟以前一樣,持有接收者,當執行命令的時候,轉調接收者,讓接收者去真正實現功能,這里的接收者就是廚師。
這里實現命令的時候,跟標準的命令模式的命令實現有一點不同,標準的命令模式的命令實現的時候,是通過構造方法傳入接收者對象,這里改成了使用setter的方式來設置接收者對象,也就是說可以動態的切換接收者對象,而無須重新構建對象。
示例中定義了三道菜,分別是兩道熱菜:北京烤鴨、綠豆排骨煲,一道涼菜:蒜泥白肉,三個具體的實現類非常類似,只是菜名不同,為了節省篇幅,這里就只看一個命令對象的具體實現。代碼示例如下:
/**
* 命令對象,綠豆排骨煲
*/
public class ChopCommand implements Command{
/**
* 持有具體做菜的廚師的對象
*/
private CookApi cookApi = null;
/**
* 設置具體做菜的廚師的對象
* @param cookApi 具體做菜的廚師的對象
*/
public void setCookApi(CookApi cookApi) {
this.cookApi = cookApi;
}
public void execute() {
this.cookApi.cook("綠豆排骨煲");
}
}
(4)該來組合菜單對象了,也就是宏命令對象。
首先宏命令就其本質還是一個命令,所以一樣要實現Command接口
其次宏命令跟普通命令的不同在于:
宏命令是多個命令組合起來的,因此在宏命令對象里面會記錄多個組成它的命令對象
第三,既然是包含多個命令對象,得有方法讓這多個命令對象能被組合進來
第四,既然宏命令包含了多個命令對象,執行宏命令對象就相當于依次執行這些命令對象,也就是循環執行這些命令對象
看看代碼示例會更清晰些,代碼示例如下:
/**
* 菜單對象,是個宏命令對象
*/
public class MenuCommand implements Command {
/**
* 用來記錄組合本菜單的多道菜品,也就是多個命令對象
*/
private Collection<Command> col = new ArrayList<Command>();
/**
* 點菜,把菜品加入到菜單中
* @param cmd 客戶點的菜
*/
public void addCommand(Command cmd){
col.add(cmd);
}
public void execute() {
//執行菜單其實就是循環執行菜單里面的每個菜
for(Command cmd : col){
cmd.execute();
}
}
}
(5)該服務員類重磅登場了,它實現的功能,相當于標準命令模式實現中的Client加上Invoker,前面都是文字講述,看看代碼如何實現,示例代碼如下:
/**
* 服務員,負責組合菜單,負責組裝每個菜和具體的實現者,
* 還負責執行調用,相當于標準Command模式的Client+Invoker
*/
public class Waiter {
/**
* 持有一個宏命令對象——菜單
*/
private MenuCommand menuCommand = new MenuCommand();
/**
* 客戶點菜
* @param cmd 客戶點的菜,每道菜是一個命令對象
*/
public void orderDish(Command cmd){
//客戶傳過來的命令對象是沒有和接收者組裝的
//在這里組裝吧
CookApi hotCook = new HotCook();
CookApi coolCook = new CoolCook();
//判讀到底是組合涼菜師傅還是熱菜師傅
//簡單點根據命令的原始對象的類型來判斷
if(cmd instanceof DuckCommand){
((DuckCommand)cmd).setCookApi(hotCook);
}else if(cmd instanceof ChopCommand){
((ChopCommand)cmd).setCookApi(hotCook);
}else if(cmd instanceof PorkCommand){
//這是個涼菜,所以要組合涼菜的師傅
((PorkCommand)cmd).setCookApi(coolCook);
}
//添加到菜單中
menuCommand.addCommand(cmd);
}
/**
* 客戶點菜完畢,表示要執行命令了,這里就是執行菜單這個組合命令
*/
public void orderOver(){
this.menuCommand.execute();
}
}
(6)費了這么大力氣,終于可以坐下來歇息一下,點菜吃飯吧,一起來看看客戶端怎么使用這個宏命令,其實在客戶端非常簡單,根本看不出宏命令來,代碼示例如下:
public class Client {
public static void main(String[] args) {
//客戶只是負責向服務員點菜就好了
//創建服務員
Waiter waiter = new Waiter();
//創建命令對象,就是要點的菜
Command chop = new ChopCommand();
Command duck = new DuckCommand();
Command pork = new PorkCommand();
//點菜,就是把這些菜讓服務員記錄下來
waiter.orderDish(chop);
waiter.orderDish(duck);
waiter.orderDish(pork);
//點菜完畢
waiter.orderOver();
}
}
運行一下,享受一下成果,結果如下:
本廚師正在做:綠豆排骨煲
本廚師正在做:北京烤鴨
涼菜蒜泥白肉已經做好,本廚師正在裝盤。