3.7 命令模式的優缺點
- 更鬆散的耦合
命令模式使得發起命令的對象——客戶端,和具體實現命令的對象——接收者對象完全解耦,也就是說發起命令的對象,完全不知道具體實現對象是誰,也不知道如何實現。 - 更動態的控制
命令模式把請求封裝起來,可以動態對它進行參數化、隊列化和日誌化等操作,從而使得系統更靈活。 - 能很自然的複合命令
命令模式中的命令對象,能夠很容易的組合成爲複合命令,就是前面講的宏命令,從而使系統操作更簡單,功能更強大。 - 更好的擴展性
由於發起命令的對象和具體的實現完全解耦,因此擴展新的命令就很容易,只需要實現新的命令對象,然後在裝配的時候,把具體的實現對象設置到命令對象裏面,然後就可以使用這個命令對象,已有的實現完全不用變化。
3.8 思考命令模式
1:命令模式的本質
命令模式的本質:封裝請求。
前面講了,命令模式的關鍵就是把請求封裝成爲命令對象,然後就可以對這個對象進行一系列的處理了,比如上面講到的參數化配置、可撤銷操作、宏命令、隊列請求、日誌請求等功能處理。
2:何時選用命令模式
建議在如下情況中,選用命令模式:
- 如果需要抽象出需要執行的動作,並參數化這些對象,可以選用命令模式,把這些需要執行的動作抽象成爲命令,然後實現命令的參數化配置
- 如果需要在不同的時刻指定、排列和執行請求,可以選用命令模式,把這些請求封裝成爲命令對象,然後實現把請求隊列化
- 如果需要支持取消操作,可以選用命令模式,通過管理命令對象,能很容易的實現命令的恢復和重做的功能
- 如果需要支持當系統崩潰時,能把對系統的操作功能重新執行一遍,可以選用命令模式,把這些操作功能的請求封裝成命令對象,然後實現日誌命令,就可以在系統恢復回來後,通過日誌獲取命令列表,從而重新執行一遍功能
- 在需要事務的系統中,可以選用命令模式,命令模式提供了對事務進行建模的方法,命令模式有一個別名就是Transaction。
3.9 退化的命令模式
在領會了命令模式本質後,來思考一個命令模式退化的情況。
前面講到了智能命令,如果命令的實現對象超級智能,實現了命令所要求的功能,那麼就不需要接收者了,既然沒有了接收者,那麼也就不需要組裝者了。
(1)舉個最簡單的示例來說明
比如現在要實現一個打印服務,由於非常簡單,所以基本上就沒有什麼講述,依次來看,命令接口定義如下:
- public interface Command {
- public void execute();
- }
命令的實現示例代碼如下:
- public class PrintService implements Command{
- /**
- * 要輸出的內容
- */
- private String str = "";
- /**
- * 構造方法,傳入要輸出的內容
- * @param s 要輸出的內容
- */
- public PrintService(String s){
- str = s;
- }
- public void execute() {
- //智能的體現,自己知道怎麼實現命令所要求的功能,並真的實現了相應的功能,不再轉調接收者了
- System.out.println("打印的內容爲="+str);
- }
- }
此時的Invoker示例代碼如下:
- public class Invoker {
- /**
- * 持有命令對象
- */
- private Command cmd = null;
- /**
- * 設置命令對象
- * @param cmd 命令對象
- */
- public void setCmd(Command cmd){
- this.cmd = cmd;
- }
- /**
- * 開始打印
- */
- public void startPrint(){
- //執行命令的功能
- this.cmd.execute();
- }
- }
最後看看客戶端的代碼,示例如下:
- public class Client {
- public static void main(String[] args) {
- //準備要發出的命令
- Command cmd = new PrintService("退化的命令模式示例");
- //設置命令給持有者
- Invoker invoker = new Invoker();
- invoker.setCmd(cmd);
- //按下按鈕,真正啓動執行命令
- invoker.startPrint();
- }
- }
測試結果如下:
- 打印的內容爲=退化的命令模式示例
(2)繼續變化
如果此時繼續變化,Invoker也開始變得智能化,在Invoker的startPrint方法裏面,Invoker加入了一些實現,同時Invoker對持有命令也有意見,覺得自己是個傀儡,要求改變一下,直接在調用方法的時候傳遞命令對象進來,示例代碼如下:
- public class Invoker {
- public void startPrint(Command cmd){
- System.out.println("在Invoker中,輸出服務前");
- cmd.execute();
- System.out.println("輸出服務結束");
- }
- }
看起來Invoker退化成一個方法了。
這個時候Invoker很高興,宣稱自己是一個智能的服務,不再是一個傻傻的轉調者,而是有自己功能的服務了。這個時候Invoker調用命令對象的執行方法,也不叫轉調,改名叫“回調”,意思是在我Invoker需要的時候,會回調你命令對象,命令對象你就乖乖的寫好實現,等我“回調”你就可以了。
事實上這個時候的命令模式的實現,基本上就等同於Java回調機制的實現,可能有些朋友看起來感覺還不是佷像,那是因爲在Java回調機制的常見實現上,經常沒有單獨的接口實現類,而是採用匿名內部類的方式來實現的。
(3)再進一步
把單獨實現命令接口的類改成用匿名內部類實現,這個時候就只剩下命令的接口、Invoker類,還有客戶端了。
爲了使用匿名內部類,還要設置要輸出的值,對命令接口做點小改動,增加一個設置輸出值的方法,示例代碼如下:
- public interface Command {
- public void execute();
- /**
- * 設置要輸出的內容
- * @param s 要輸出的內容
- */
- public void setStr(String s);
- }
此時Invoker就是上面那個,而客戶端會有些改變,客戶端的示例代碼如下:
- public class Client {
- public static void main(String[] args) {
- //準備要發出的命令,沒有具體實現類了
- //匿名內部類來實現命令
- Command cmd = new Command(){
- private String str = "";
- public void setStr(String s){
- str = s;
- }
- public void execute() {
- System.out.println("打印的內容爲="+str);
- }
- };
- cmd.setStr("退化的命令模式類似於Java回調的示例");
- //這個時候的Invoker或許該稱爲服務了
- Invoker invoker = new Invoker();
- //按下按鈕,真正啓動執行命令
- invoker.startPrint(cmd);
- }
- }
運行測試一下,結果如下:
- 在Invoker中,輸出服務前
- 打印的內容爲=退化的命令模式類似於Java回調的示例
- 輸出服務結束
(4)現在是不是看出來了,這個時候的命令模式的實現,基本上就等同於Java回調機制的實現。這也是很多人大談特談命令模式可以實現Java回調的意思。
當然更狠的是連Invoker也不要了,直接把那個方法搬到Client中,那樣測試起來就更方便了。在實際開發中,應用命令模式來實現回調機制的時候,Invoker通常還是有的,但可以智能化實現,更準確的說Invoker充當客戶調用的服務實現,而回調的方法只是實現服務功能中的一個或者幾個步驟。
3.10 相關模式
- 命令模式和組合模式
這兩個模式可以組合使用。
在命令模式中,實現宏命令的功能,就可以使用組合模式來實現。前面的示例並沒有按照組合模式來做,那是爲了保持示例的簡單,還有突出命令模式的實現,這點請注意。 - 命令模式和備忘錄模式
這兩個模式可以組合使用。
在命令模式中,實現可撤銷操作功能時,前面講了有兩種實現方式,其中有一種就是保存命令執行前的狀態,撤銷的時候就把狀態恢復回去。如果採用這種方式實現,就可以考慮使用備忘錄模式。
如果狀態存儲在命令對象裏面,那麼還可以使用原型模式,把命令對象當作原型來克隆一個新的對象,然後把克隆出來的對象通過備忘錄模式存放。 - 命令模式和模板方法模式
這兩個模式從某種意義上有相似的功能,命令模式可以作爲模板方法的一種替代模式,也就是說命令模式可以模仿實現模板方法模式的功能。
如同前面講述的退化的命令模式可以實現Java的回調,而Invoker智能化後向服務進化,如果Invoker的方法就是一個算法骨架,其中有兩步在這個骨架裏面沒有具體實現,需要外部來實現,這個時候就可以通過回調命令接口來實現。
而類似的功能在模板方法裏面,一個算法骨架,其中有兩步在這個骨架裏面沒有具體實現,是先調用抽象方法,然後等待子類來實現。
可以看出雖然實現方式不一樣,但是可以實現相同的功能。