1. 程式人生 > >設計模式之十一個行為型模式的相關知識,簡單易懂。

設計模式之十一個行為型模式的相關知識,簡單易懂。

一、    職責鏈模式-Chain of Responsibility Pattern

1)  請求的鏈式處理——職責鏈模式(一)

“一對二”,“過”,“過”……這聲音熟悉嗎?你會想到什麼?對!紙牌。在類似“鬥地主”這樣的紙牌遊戲中,某人出牌給他的下家,下家看看手中的牌,如果要不起上家的牌則將出牌請求再轉發給他的下家,其下家再進行判斷。一個迴圈下來,如果其他人都要不起該牌,則最初的出牌者可以打出新的牌。在這個過程中,牌作為一個請求沿著一條鏈在傳遞,每一位紙牌的玩家都可以處理該請求。在設計模式中,我們也有一種專門用於處理這種請求鏈式傳遞的模式,它就是本章將要介紹的職責鏈模式。

16.1 採購單的分級審批

Sunny軟體公司承接了某企業SCM(SupplyChain Management,供應鏈管理)系統的開發任務,其中包含一個採購審批子系統。該企業的採購審批是分級進行的,即根據採購金額的不同由不同層次的主管人員來審批,主任可以審批5萬元以下(不包括5萬元)的採購單,副董事長可以審批5萬元至10萬元(不包括10萬元)的採購單,董事長可以審批10萬元至50萬元(不包括50萬元)的採購單,50萬元及以上的採購單就需要開董事會討論決定。如圖16-1所示:

圖16-1 採購單分級審批示意圖

如何在軟體中實現採購單的分級審批?Sunny軟體公司開發人員提出了一個初始解決方案,在系統中提供一個採購單處理類PurchaseRequestHandler用於統一處理採購單,其框架程式碼如下所示:

//採購單處理類 

class PurchaseRequestHandler { 

   //遞交採購單給主任 

   public void sendRequestToDirector(PurchaseRequest request) { 

       if (request.getAmount() < 50000) { 

           //主任可審批該採購單 

           this.handleByDirector(request); 

       } 

       else if (request.getAmount() < 100000) { 

           //副董事長可審批該採購單 

           this.handleByVicePresident(request); 

       } 

       else if (request.getAmount() < 500000) { 

           //董事長可審批該採購單 

           this.handleByPresident(request); 

       } 

       else { 

           //董事會可審批該採購單 

           this.handleByCongress(request); 

       } 

   } 

   //主任審批採購單 

   public void handleByDirector(PurchaseRequest request) { 

       //程式碼省略 

   } 

   //副董事長審批採購單 

   public void handleByVicePresident(PurchaseRequest request) { 

       //程式碼省略 

    }

   //董事長審批採購單 

   public void handleByPresident(PurchaseRequest request) { 

       //程式碼省略 

    }

   //董事會審批採購單 

   public void handleByCongress(PurchaseRequest request) { 

       //程式碼省略 

   } 

}

問題貌似很簡單,但仔細分析,發現上述方案存在如下幾個問題:

(1)PurchaseRequestHandler類較為龐大,各個級別的審批方法都集中在一個類中,違反了“單一職責原則”,測試和維護難度大。

(2)如果需要增加一個新的審批級別或調整任何一級的審批金額和審批細節(例如將董事長的審批額度改為60萬元)時都必須修改原始碼並進行嚴格測試,此外,如果需要移除某一級別(例如金額為10萬元及以上的採購單直接由董事長審批,不再設副董事長一職)時也必須對原始碼進行修改,違反了“開閉原則”。

(3)審批流程的設定缺乏靈活性,現在的審批流程是“主任-->副董事長-->董事長-->董事會”,如果需要改為“主任-->董事長-->董事會”,在此方案中只能通過修改原始碼來實現,客戶端無法定製審批流程。

如何針對上述問題對系統進行改進?Sunny公司開發人員迫切需要一種新的設計方案,還好有職責鏈模式,通過使用職責鏈模式我們可以最大程度地解決這些問題,下面讓我們正式進入職責鏈模式的學習。

2)  請求的鏈式處理——職責鏈模式(二)

16.2 職責鏈模式概述

很多情況下,在一個軟體系統中可以處理某個請求的物件不止一個,例如SCM系統中的採購單審批,主任、副董事長、董事長和董事會都可以處理採購單,他們可以構成一條處理採購單的鏈式結構,採購單沿著這條鏈進行傳遞,這條鏈就稱為職責鏈。職責鏈可以是一條直線、一個環或者一個樹形結構,最常見的職責鏈是直線型,即沿著一條單向的鏈來傳遞請求。鏈上的每一個物件都是請求處理者,職責鏈模式可以將請求的處理者組織成一條鏈,並讓請求沿著鏈傳遞,由鏈上的處理者對請求進行相應的處理,客戶端無須關心請求的處理細節以及請求的傳遞,只需將請求傳送到鏈上即可,實現請求傳送者和請求處理者解耦。

職責鏈模式定義如下: 職責鏈模式(Chain of ResponsibilityPattern):避免請求傳送者與接收者耦合在一起,讓多個物件都有可能接收請求,將這些物件連線成一條鏈,並且沿著這條鏈傳遞請求,直到有物件處理它為止。職責鏈模式是一種物件行為型模式。

職責鏈模式結構的核心在於引入了一個抽象處理者。職責鏈模式結構如圖16-2所示:

在職責鏈模式結構圖中包含如下幾個角色:

● Handler(抽象處理者):它定義了一個處理請求的介面,一般設計為抽象類,由於不同的具體處理者處理請求的方式不同,因此在其中定義了抽象請求處理方法。因為每一個處理者的下家還是一個處理者,因此在抽象處理者中定義了一個抽象處理者型別的物件(如結構圖中的successor),作為其對下家的引用。通過該引用,處理者可以連成一條鏈。

● ConcreteHandler(具體處理者):它是抽象處理者的子類,可以處理使用者請求,在具體處理者類中實現了抽象處理者中定義的抽象請求處理方法,在處理請求之前需要進行判斷,看是否有相應的處理許可權,如果可以處理請求就處理它,否則將請求轉發給後繼者;在具體處理者中可以訪問鏈中下一個物件,以便請求的轉發。

在職責鏈模式裡,很多物件由每一個物件對其下家的引用而連線起來形成一條鏈。請求在這個鏈上傳遞,直到鏈上的某一個物件決定處理此請求。發出這個請求的客戶端並不知道鏈上的哪一個物件最終處理這個請求,這使得系統可以在不影響客戶端的情況下動態地重新組織鏈和分配責任。

職責鏈模式的核心在於抽象處理者類的設計,抽象處理者的典型程式碼如下所示:

abstract class Handler { 

   //維持對下家的引用 

       protectedHandler successor; 

   public void setSuccessor(Handler successor) { 

       this.successor=successor; 

   } 

   public abstract void handleRequest(String request); 

}

上述程式碼中,抽象處理者類定義了對下家的引用物件,以便將請求轉發給下家,該物件的訪問符可設為protected,在其子類中可以使用。在抽象處理者類中聲明瞭抽象的請求處理方法,具體實現交由子類完成。

具體處理者是抽象處理者的子類,它具有兩大作用:第一是處理請求,不同的具體處理者以不同的形式實現抽象請求處理方法handleRequest();第二是轉發請求,如果該請求超出了當前處理者類的許可權,可以將該請求轉發給下家。具體處理者類的典型程式碼如下:

class ConcreteHandler extends Handler{ 

   public void handleRequest(String request) { 

       if (請求滿足條件) { 

           //處理請求 

       } 

       else { 

           this.successor.handleRequest(request); //轉發請求 

       } 

   } 

}

在具體處理類中通過對請求進行判斷可以做出相應的處理。

需要注意的是,職責鏈模式並不建立職責鏈,職責鏈的建立工作必須由系統的其他部分來完成,一般是在使用該職責鏈的客戶端中建立職責鏈。職責鏈模式降低了請求的傳送端和接收端之間的耦合,使多個物件都有機會處理這個請求。

思考

如何在客戶端建立一條職責鏈?

3)  請求的鏈式處理——職責鏈模式(三)

16.3 完整解決方案 為了讓採購單的審批流程更加靈活,並實現採購單的鏈式傳遞和處理,Sunny公司開發人員使用職責鏈模式來實現採購單的分級審批,其基本結構如圖16-3所示:

在圖16-3中,抽象類Approver充當抽象處理者(抽象傳遞者),Director、VicePresident、President和Congress充當具體處理者(具體傳遞者),PurchaseRequest充當請求類。完整程式碼如下所示:

//採購單:請求類 

class PurchaseRequest { 

   private double amount;  //採購金額 

   private int number;  //採購單編號 

   private String purpose;  //採購目的

   public PurchaseRequest(double amount, int number, String purpose) { 

       this.amount = amount; 

       this.number = number; 

       this.purpose = purpose; 

    }

   public void setAmount(double amount) { 

       this.amount = amount; 

    }

   public double getAmount() { 

       return this.amount; 

    }

   public void setNumber(int number) { 

        this.number = number; 

    }

   public int getNumber() { 

       return this.number; 

    }

   public void setPurpose(String purpose) { 

       this.purpose = purpose; 

    }

   public String getPurpose() { 

       return this.purpose; 

   } 

}

//審批者類:抽象處理者 

abstract class Approver { 

   protected Approver successor; //定義後繼物件 

   protected String name; //審批者姓名

   public Approver(String name) { 

       this.name = name; 

    }

   //設定後繼者 

   public void setSuccessor(Approver successor) {  

       this.successor = successor; 

    }

   //抽象請求處理方法 

   public abstract void processRequest(PurchaseRequest request); 

}

//主任類:具體處理者 

class Director extends Approver { 

   public Director(String name) { 

       super(name); 

    }

   //具體請求處理方法 

   public void processRequest(PurchaseRequest request) { 

       if (request.getAmount() < 50000) { 

           System.out.println("主任" + this.name +"審批採購單:" + request.getNumber() + ",金額:" + request.getAmount() + "元,採購目的:"+ request.getPurpose() + "。");  //處理請求 

       } 

       else { 

           this.successor.processRequest(request); //轉發請求 

       }    

   } 

}

//副董事長類:具體處理者 

class VicePresident extends Approver { 

   public VicePresident(String name) { 

       super(name); 

    }

   //具體請求處理方法 

   public void processRequest(PurchaseRequest request) { 

       if (request.getAmount() < 100000) { 

           System.out.println("副董事長" + this.name+ "審批採購單:" + request.getNumber() + ",金額:" + request.getAmount() + "元,採購目的:"+ request.getPurpose() + "。");  //處理請求 

       } 

       else { 

           this.successor.processRequest(request); //轉發請求 

       }    

   } 

}

//董事長類:具體處理者 

class President extends Approver { 

   public President(String name) { 

       super(name); 

    }

   //具體請求處理方法 

   public void processRequest(PurchaseRequest request) { 

       if (request.getAmount() < 500000) { 

           System.out.println("董事長" + this.name +"審批採購單:" + request.getNumber() + ",金額:" + request.getAmount() + "元,採購目的:"+ request.getPurpose() + "。");  //處理請求 

       } 

       else { 

           this.successor.processRequest(request); //轉發請求 

       } 

   } 

}

//董事會類:具體處理者 

class Congress extends Approver { 

   public Congress(String name) { 

       super(name); 

    }

   //具體請求處理方法 

   public void processRequest(PurchaseRequest request) { 

       System.out.println("召開董事會審批採購單:" +request.getNumber() + ",金額:" +request.getAmount() + "元,採購目的:" +request.getPurpose() + "。");        //處理請求 

   }     

}

編寫如下客戶端測試程式碼:

class Client { 

   public static void main(String[] args) { 

       Approver wjzhang,gyang,jguo,meeting; 

       wjzhang = new Director("張無忌"); 

       gyang = new VicePresident("楊過"); 

       jguo = new President("郭靖"); 

       meeting = new Congress("董事會");

       //建立職責鏈

       wjzhang.setSuccessor(gyang); 

       gyang.setSuccessor(jguo); 

       jguo.setSuccessor(meeting);

       //建立採購單 

       PurchaseRequest pr1 = new PurchaseRequest(45000,10001,"購買倚天劍"); 

       wjzhang.processRequest(pr1);

       PurchaseRequest pr2 = new PurchaseRequest(60000,10002,"購買《葵花寶典》"); 

       wjzhang.processRequest(pr2);

       PurchaseRequest pr3 = new PurchaseRequest(160000,10003,"購買《金剛經》"); 

       wjzhang.processRequest(pr3);

       PurchaseRequest pr4 = new PurchaseRequest(800000,10004,"購買桃花島"); 

       wjzhang.processRequest(pr4); 

   } 

}

編譯並執行程式,輸出結果如下:

主任張無忌審批採購單:10001,金額:45000.0元,採購目的:購買倚天劍。

副董事長楊過審批採購單:10002,金額:60000.0元,採購目的:購買《葵花寶典》。

董事長郭靖審批採購單:10003,金額:160000.0元,採購目的:購買《金剛經》。

召開董事會審批採購單:10004,金額:800000.0元,採購目的:購買桃花島。

如果需要在系統增加一個新的具體處理者,如增加一個經理(Manager)角色可以審批5萬元至8萬元(不包括8萬元)的採購單,需要編寫一個新的具體處理者類Manager,作為抽象處理者類Approver的子類,實現在Approver類中定義的抽象處理方法,如果採購金額大於等於8萬元,則將請求轉發給下家,程式碼如下所示:

//經理類:具體處理者 

class Manager extends Approver { 

   public Manager(String name) { 

       super(name); 

    }

   //具體請求處理方法 

   public void processRequest(PurchaseRequest request) { 

       if (request.getAmount() < 80000) { 

           System.out.println("經理" + this.name +"審批採購單:" + request.getNumber() + ",金額:" + request.getAmount() + "元,採購目的:"+ request.getPurpose() + "。");  //處理請求 

       } 

       else { 

           this.successor.processRequest(request); //轉發請求 

       }    

   } 

}

由於鏈的建立過程由客戶端負責,因此增加新的具體處理者類對原有類庫無任何影響,無須修改已有類的原始碼,符合“開閉原則”。

在客戶端程式碼中,如果要將新的具體請求處理者應用在系統中,需要建立新的具體處理者物件,然後將該物件加入職責鏈中。如在客戶端測試程式碼中增加如下程式碼:

Approver rhuang; 

rhuang = new Manager("黃蓉");

將建鏈程式碼改為:

//建立職責鏈

wjzhang.setSuccessor(rhuang); //將“黃蓉”作為“張無忌”的下家

rhuang.setSuccessor(gyang); //將“楊過”作為“黃蓉”的下家

gyang.setSuccessor(jguo);

jguo.setSuccessor(meeting);

重新編譯並執行程式,輸出結果如下:

主任張無忌審批採購單:10001,金額:45000.0元,採購目的:購買倚天劍。 經理黃蓉審批採購單:10002,金額:60000.0元,採購目的:購買《葵花寶典》。 董事長郭靖審批採購單:10003,金額:160000.0元,採購目的:購買《金剛經》。 召開董事會審批採購單:10004,金額:800000.0元,採購目的:購買桃花島。 ```

思考

如果將審批流程由“主任-->副董事長-->董事長-->董事會”調整為“主任-->董事長-->董事會”,系統將做出哪些改動?預測修改之後客戶端程式碼的輸出結果。

4)  請求的鏈式處理——職責鏈模式(四)

16.4 純與不純的職責鏈模式

職責鏈模式可分為純的職責鏈模式和不純的職責鏈模式兩種:

(1) 純的職責鏈模式

一個純的職責鏈模式要求一個具體處理者物件只能在兩個行為中選擇一個:要麼承擔全部責任,要麼將責任推給下家,不允許出現某一個具體處理者物件在承擔了一部分或全部責任後又將責任向下傳遞的情況。而且在純的職責鏈模式中,要求一個請求必須被某一個處理者物件所接收,不能出現某個請求未被任何一個處理者物件處理的情況。在前面的採購單審批例項中應用的是純的職責鏈模式。

(2)不純的職責鏈模式

在一個不純的職責鏈模式中允許某個請求被一個具體處理者部分處理後再向下傳遞,或者一個具體處理者處理完某請求後其後繼處理者可以繼續處理該請求,而且一個請求可以最終不被任何處理者物件所接收。Java AWT 1.0中的事件處理模型應用的是不純的職責鏈模式,其基本原理如下:由於視窗元件(如按鈕、文字框等)一般都位於容器元件中,因此當事件發生在某一個元件上時,先通過元件物件的handleEvent()方法將事件傳遞給相應的事件處理方法,該事件處理方法將處理此事件,然後決定是否將該事件向上一級容器元件傳播;上級容器元件在接到事件之後可以繼續處理此事件並決定是否繼續向上級容器元件傳播,如此反覆,直到事件到達頂層容器元件為止;如果一直傳到最頂層容器仍沒有處理方法,則該事件不予處理。每一級元件在接收到事件時,都可以處理此事件,而不論此事件是否在上一級已得到處理,還存在事件未被處理的情況。顯然,這就是不純的職責鏈模式,早期的Java AWT事件模型(JDK 1.0及更早)中的這種事件處理機制又叫事件浮升(Event Bubbling)機制。從Java.1.1以後,JDK使用觀察者模式代替職責鏈模式來處理事件。目前,在JavaScript中仍然可以使用這種事件浮升機制來進行事件處理。

16.5 職責鏈模式總結

職責鏈模式通過建立一條鏈來組織請求的處理者,請求將沿著鏈進行傳遞,請求傳送者無須知道請求在何時、何處以及如何被處理,實現了請求傳送者與處理者的解耦。在軟體開發中,如果遇到有多個物件可以處理同一請求時可以應用職責鏈模式,例如在Web應用開發中建立一個過濾器(Filter)鏈來對請求資料進行過濾,在工作流系統中實現公文的分級審批等等,使用職責鏈模式可以較好地解決此類問題。

1.主要優點

職責鏈模式的主要優點如下:

(1) 職責鏈模式使得一個物件無須知道是其他哪一個物件處理其請求,物件僅需知道該請求會被處理即可,接收者和傳送者都沒有對方的明確資訊,且鏈中的物件不需要知道鏈的結構,由客戶端負責鏈的建立,降低了系統的耦合度。

(2) 請求處理物件僅需維持一個指向其後繼者的引用,而不需要維持它對所有的候選處理者的引用,可簡化物件的相互連線。

(3) 在給物件分派職責時,職責鏈可以給我們更多的靈活性,可以通過在執行時對該鏈進行動態的增加或修改來增加或改變處理一個請求的職責。

(4) 在系統中增加一個新的具體請求處理者時無須修改原有系統的程式碼,只需要在客戶端重新建鏈即可,從這一點來看是符合“開閉原則”的。

2.主要缺點

職責鏈模式的主要缺點如下:

(1) 由於一個請求沒有明確的接收者,那麼就不能保證它一定會被處理,該請求可能一直到鏈的末端都得不到處理;一個請求也可能因職責鏈沒有被正確配置而得不到處理。

(2) 對於比較長的職責鏈,請求的處理可能涉及到多個處理物件,系統性能將受到一定影響,而且在進行程式碼除錯時不太方便。

(3) 如果建鏈不當,可能會造成迴圈呼叫,將導致系統陷入死迴圈。

3.適用場景

在以下情況下可以考慮使用職責鏈模式:

(1) 有多個物件可以處理同一個請求,具體哪個物件處理該請求待執行時刻再確定,客戶端只需將請求提交到鏈上,而無須關心請求的處理物件是誰以及它是如何處理的。

(2) 在不明確指定接收者的情況下,向多個物件中的一個提交一個請求。

(3) 可動態指定一組物件處理請求,客戶端可以動態建立職責鏈來處理請求,還可以改變鏈中處理者之間的先後次序。

練習

Sunny軟體公司的OA系統需要提供一個假條審批模組:如果員工請假天數小於3天,主任可以審批該假條;如果員工請假天數大於等於3天,小於10天,經理可以審批;如果員工請假天數大於等於10天,小於30天,總經理可以審批;如果超過30天,總經理也不能審批,提示相應的拒絕資訊。試用職責鏈模式設計該假條審批模組。

二、    命令模式-Command Pattern

1)  請求傳送者與接收者解耦——命令模式(一)

裝修新房的最後幾道工序之一是安裝插座和開關,通過開關可以控制一些電器的開啟和關閉,例如電燈或者排氣扇。在購買開關時,我們並不知道它將來到底用於控制什麼電器,也就是說,開關與電燈、排氣扇並無直接關係,一個開關在安裝之後可能用來控制電燈,也可能用來控制排氣扇或者其他電器裝置。開關與電器之間通過電線建立連線,如果開關開啟,則電線通電,電器工作;反之,開關關閉,電線斷電,電器停止工作。相同的開關可以通過不同的電線來控制不同的電器,如圖1所示:

圖1 開關與電燈、排氣扇示意圖

在圖1中,我們可以將開關理解成一個請求的傳送者,使用者通過它來發送一個“開燈”請求,而電燈是“開燈”請求的最終接收者和處理者,在圖中,開關和電燈之間並不存在直接耦合關係,它們通過電線連線在一起,使用不同的電線可以連線不同的請求接收者,只需更換一根電線,相同的傳送者(開關)即可對應不同的接收者(電器)。

在軟體開發中也存在很多與開關和電器類似的請求傳送者和接收者物件,例如一個按鈕,它可能是一個“關閉視窗”請求的傳送者,而按鈕點選事件處理類則是該請求的接收者。為了降低系統的耦合度,將請求的傳送者和接收者解耦,我們可以使用一種被稱之為命令模式的設計模式來設計系統,在命令模式中,傳送者與接收者之間引入了新的命令物件(類似圖1中的電線),將傳送者的請求封裝在命令物件中,再通過命令物件來呼叫接收者的方法。本章我們將學習用於將請求傳送者和接收者解耦的命令模式。

1 自定義功能鍵

Sunny軟體公司開發人員為公司內部OA系統開發了一個桌面版應用程式,該應用程式為使用者提供了一系列自定義功能鍵,使用者可以通過這些功能鍵來實現一些快捷操作。Sunny軟體公司開發人員通過分析,發現不同的使用者可能會有不同的使用習慣,在設定功能鍵的時候每個人都有自己的喜好,例如有的人喜歡將第一個功能鍵設定為“開啟幫助文件”,有的人則喜歡將該功能鍵設定為“最小化至托盤”,為了讓使用者能夠靈活地進行功能鍵的設定,開發人員提供了一個“功能鍵設定”視窗,該視窗介面如圖2所示:

圖2 “功能鍵設定”介面效果圖

通過如圖2所示介面,使用者可以將功能鍵和相應功能繫結在一起,還可以根據需要來修改功能鍵的設定,而且系統在未來可能還會增加一些新的功能或功能鍵。

Sunny軟體公司某開發人員欲使用如下程式碼來實現功能鍵與功能處理類之間的呼叫關係:

//FunctionButton:功能鍵類,請求傳送者 

class FunctionButton { 

   private HelpHandler help; //HelpHandler:幫助文件處理類,請求接收者 

   //在FunctionButton的onClick()方法中呼叫HelpHandler的display()方法 

       publicvoid onClick() { 

       help = new HelpHandler(); 

       help.display(); //顯示幫助文件 

   } 

}

在上述程式碼中,功能鍵類FunctionButton充當請求的傳送者,幫助文件處理類HelpHandler充當請求的接收者,在傳送者FunctionButton的onClick()方法中將呼叫接收者HelpHandler的display()方法。顯然,如果使用上述程式碼,將給系統帶來如下幾個問題:

(1) 由於請求傳送者和請求接收者之間存在方法的直接呼叫,耦合度很高,更換請求接收者必須修改傳送者的原始碼,如果需要將請求接收者HelpHandler改為WindowHanlder(視窗處理類),則需要修改FunctionButton的原始碼,違背了“開閉原則”。

(2) FunctionButton類在設計和實現時功能已被固定,如果增加一個新的請求接收者,如果不修改原有的FunctionButton類,則必須增加一個新的與FunctionButton功能類似的類,這將導致系統中類的個數急劇增加。由於請求接收者HelpHandler、WindowHanlder等類之間可能不存在任何關係,它們沒有共同的抽象層,因此也很難依據“依賴倒轉原則”來設計FunctionButton。

(3) 使用者無法按照自己的需要來設定某個功能鍵的功能,一個功能鍵類的功能一旦固定,在不修改原始碼的情況下無法更換其功能,系統缺乏靈活性。

不難得知,所有這些問題的產生都是因為請求傳送者FunctionButton類和請求接收者HelpHandler、WindowHanlder等類之間存在直接耦合關係,如何降低請求傳送者和接收者之間的耦合度,讓相同的傳送者可以對應不同的接收者?這是Sunny軟體公司開發人員在設計“功能鍵設定”模組時不得不考慮的問題。命令模式正為解決這類問題而誕生,此時,如果我們使用命令模式,可以在一定程度上解決上述問題(注:命令模式無法解決類的個數增加的問題),下面就讓我們正式進入命令模式的學習,看看命令模式到底如何實現請求傳送者和接收者解耦。

2 命令模式概述

在軟體開發中,我們經常需要向某些物件傳送請求(呼叫其中的某個或某些方法),但是並不知道請求的接收者是誰,也不知道被請求的操作是哪個,此時,我們特別希望能夠以一種鬆耦合的方式來設計軟體,使得請求傳送者與請求接收者能夠消除彼此之間的耦合,讓物件之間的呼叫關係更加靈活,可以靈活地指定請求接收者以及被請求的操作。命令模式為此類問題提供了一個較為完美的解決方案。

命令模式可以將請求傳送者和接收者完全解耦,傳送者與接收者之間沒有直接引用關係,傳送請求的物件只需要知道如何傳送請求,而不必知道如何完成請求。

命令模式定義如下:

命令模式(Command Pattern):將一個請求封裝為一個物件,從而讓我們可用不同的請求對客戶進行引數化;對請求排隊或者記錄請求日誌,以及支援可撤銷的操作。命令模式是一種物件行為型模式,其別名為動作(Action)模式或事務(Transaction)模式。

命令模式的定義比較複雜,提到了很多術語,例如“用不同的請求對客戶進行引數化”、“對請求排隊”,“記錄請求日誌”、“支援可撤銷操作”等,在後面我們將對這些術語進行一一講解。

命令模式的核心在於引入了命令類,通過命令類來降低傳送者和接收者的耦合度,請求傳送者只需指定一個命令物件,再通過命令物件來呼叫請求接收者的處理方法,其結構如圖3所示:

圖3 命令模式結構圖

在命令模式結構圖中包含如下幾個角色:

● Command(抽象命令類):抽象命令類一般是一個抽象類或介面,在其中聲明瞭用於執行請求的execute()等方法,通過這些方法可以呼叫請求接收者的相關操作。

● ConcreteCommand(具體命令類):具體命令類是抽象命令類的子類,實現了在抽象命令類中宣告的方法,它對應具體的接收者物件,將接收者物件的動作繫結其中。在實現execute()方法時,將呼叫接收者物件的相關操作(Action)。

● Invoker(呼叫者):呼叫者即請求傳送者,它通過命令物件來執行請求。一個呼叫者並不需要在設計時確定其接收者,因此它只與抽象命令類之間存在關聯關係。在程式執行時可以將一個具體命令物件注入其中,再呼叫具體命令物件的execute()方法,從而實現間接呼叫請求接收者的相關操作。

● Receiver(接收者):接收者執行與請求相關的操作,它具體實現對請求的業務處理。

命令模式的本質是對請求進行封裝,一個請求對應於一個命令,將發出命令的責任和執行命令的責任分割開。每一個命令都是一個操作:請求的一方發出請求要求執行一個操作;接收的一方收到請求,並執行相應的操作。命令模式允許請求的一方和接收的一方獨立開來,使得請求的一方不必知道接收請求的一方的介面,更不必知道請求如何被接收、操作是否被執行、何時被執行,以及是怎麼被執行的。

命令模式的關鍵在於引入了抽象命令類,請求傳送者針對抽象命令類程式設計,只有實現了抽象命令類的具體命令才與請求接收者相關聯。在最簡單的抽象命令類中只包含了一個抽象的execute()方法,每個具體命令類將一個Receiver型別的物件作為一個例項變數進行儲存,從而具體指定一個請求的接收者,不同的具體命令類提供了execute()方法的不同實現,並呼叫不同接收者的請求處理方法。 典型的抽象命令類程式碼如下所示:

abstract class Command { 

   public abstract void execute(); 

}

對於請求傳送者即呼叫者而言,將針對抽象命令類進行程式設計,可以通過構造注入或者設值注入的方式在執行時傳入具體命令類物件,並在業務方法中呼叫命令物件的execute()方法,其典型程式碼如下所示:

class Invoker { 

   private Command command; 

   //構造注入 

   public Invoker(Command command) { 

       this.command = command; 

   } 

   //設值注入 

   public void setCommand(Command command) { 

       this.command = command; 

   } 

   //業務方法,用於呼叫命令類的execute()方法 

   public void call() { 

       command.execute(); 

   } 

}

具體命令類繼承了抽象命令類,它與請求接收者相關聯,實現了在抽象命令類中宣告的execute()方法,並在實現時呼叫接收者的請求響應方法action(),其典型程式碼如下所示:

class ConcreteCommand extends Command{ 

   private Receiver receiver; //維持一個對請求接收者物件的引用 

   public void execute() { 

       receiver.action(); //呼叫請求接收者的業務處理方法action() 

    }

}

請求接收者Receiver類具體實現對請求的業務處理,它提供了action()方法,用於執行與請求相關的操作,其典型程式碼如下所示:

class Receiver { 

   public void action() { 

       //具體操作 

   } 

}

思考

一個請求傳送者能否對應多個請求接收者?如何實現?

2)  請求傳送者與接收者解耦——命令模式(二)

3 完整解決方案

為了降低功能鍵與功能處理類之間的耦合度,讓使用者可以自定義每一個功能鍵的功能,Sunny軟體公司開發人員使用命令模式來設計“自定義功能鍵”模組,其核心結構如圖4所示:

圖4 自定義功能鍵核心結構圖

在圖4中,FBSettingWindow是“功能鍵設定”介面類,FunctionButton充當請求呼叫者,Command充當抽象命令類,MinimizeCommand和HelpCommand充當具體命令類,WindowHanlder和HelpHandler充當請求接收者。完整程式碼如下所示:

import java.util.*;

//功能鍵設定視窗類 

class FBSettingWindow { 

   private String title; //視窗標題 

   //定義一個ArrayList來儲存所有功能鍵 

   private ArrayList<FunctionButton> functionButtons = newArrayList<FunctionButton>(); 

   public FBSettingWindow(String title) { 

       this.title = title; 

   } 

   public void setTitle(String title) { 

       this.title = title; 

   } 

   public String getTitle() { 

       return this.title; 

    }

   public void addFunctionButton(FunctionButton fb) { 

       functionButtons.add(fb); 

    }

   public void removeFunctionButton(FunctionButton fb) { 

       functionButtons.remove(fb); 

    }

   //顯示視窗及功能鍵 

   public void display() { 

       System.out.println("顯示視窗:" +this.title); 

       System.out.println("顯示功能鍵:"); 

       for (Object obj : functionButtons) { 

           System.out.println(((FunctionButton)obj).getName()); 

       } 

       System.out.println("------------------------------"); 

   }    

}

//功能鍵類:請求傳送者 

class FunctionButton { 

   private String name; //功能鍵名稱 

   private Command command; //維持一個抽象命令物件的引用 

   public FunctionButton(String name) { 

       this.name = name; 

    }

   public String getName() { 

       return this.name; 

    }

   //為功能鍵注入命令 

   public void setCommand(Command command) { 

       this.command = command; 

    }

   //傳送請求的方法 

   public void onClick() { 

       System.out.print("點選功能鍵:"); 

       command.execute(); 

   } 

}

//抽象命令類 

abstract class Command { 

   public abstract void execute(); 

}

//幫助命令類:具體命令類 

class HelpCommand extends Command { 

   private HelpHandler hhObj; //維持對請求接收者的引用 

   public HelpCommand() { 

       hhObj = new HelpHandler(); 

    }

   //命令執行方法,將呼叫請求接收者的業務方法 

   public void execute() { 

       hhObj.display(); 

   } 

}

//最小化命令類:具體命令類 

class MinimizeCommand extends Command{ 

   private WindowHanlder whObj; //維持對請求接收者的引用

   public MinimizeCommand() { 

       whObj = new WindowHanlder(); 

    }

       //命令執行方法,將呼叫請求接收者的業務方法 

   public void execute() { 

       whObj.minimize(); 

   } 

}

//視窗處理類:請求接收者 

class WindowHanlder { 

   public void minimize() { 

       System.out.println("將視窗最小化至托盤!"); 

   } 

}

//幫助文件處理類:請求接收者 

class HelpHandler { 

   public void display() { 

       System.out.println("顯示幫助文件!"); 

   } 

}

為了提高系統的靈活性和可擴充套件性,我們將具體命令類的類名儲存在配置檔案中,並通過工具類XMLUtil來讀取配置檔案並反射生成物件,XMLUtil類的程式碼如下所示:

import javax.xml.parsers.*; 

import org.w3c.dom.*; 

import org.xml.sax.SAXException; 

import java.io.*; 

public class XMLUtil { 

//該方法用於從XML配置檔案中提取具體類類名,並返回一個例項物件,可以通過引數的不同返回不同類名節點所對應的例項 

   public static Object getBean(int i) { 

       try { 

           //建立文件物件 

           DocumentBuilderFactory dFactory =DocumentBuilderFactory.newInstance(); 

           DocumentBuilder builder = dFactory.newDocumentBuilder(); 

           Document doc;                            

           doc = builder.parse(new File("config.xml"));  

           //獲取包含類名的文字節點 

           NodeList nl = doc.getElementsByTagName("className"); 

           Node classNode = null; 

           if (0 == i) { 

                classNode =nl.item(0).getFirstChild(); 

           } 

           else { 

                classNode =nl.item(1).getFirstChild(); 

           }  

           String cName = classNode.getNodeValue(); 

           //通過類名生成例項物件並將其返回 

           Class c = Class.forName(cName); 

           Object obj = c.newInstance(); 

           return obj; 

       }    

       catch(Exception e){ 

           e.printStackTrace(); 

           return null; 

       } 

    } 

}

配置檔案config.xml中儲存了具體建造者類的類名,程式碼如下所示:

<?xml version="1.0"?> 

<config> 

   <className>HelpCommand</className> 

   <className>MinimizeCommand</className> 

</config> 

編寫如下客戶端測試程式碼:

class Client { 

   public static void main(String args[]) { 

       FBSettingWindow fbsw = new FBSettingWindow("功能鍵設定"); 

       FunctionButton fb1,fb2; 

       fb1 = new FunctionButton("功能鍵1"); 

       fb2 = new FunctionButton("功能鍵1"); 

       Command command1,command2; 

       //通過讀取配置檔案和反射生成具體命令物件 

        command1 =(Command)XMLUtil.getBean(0); 

       command2 = (Command)XMLUtil.getBean(1); 

       //將命令物件注入功能鍵 

       fb1.setCommand(command1); 

       fb2.setCommand(command2); 

       fbsw.addFunctionButton(fb1); 

       fbsw.addFunctionButton(fb2); 

       fbsw.display(); 

       //呼叫功能鍵的業務方法 

       fb1.onClick(); 

       fb2.onClick(); 

   } 

}

編譯並執行程式,輸出結果如下:

顯示視窗:功能鍵設定

顯示功能鍵:

功能鍵1

功能鍵1

------------------------------

點選功能鍵:顯示幫助文件!

點選功能鍵:將視窗最小化至托盤!

如果需要修改功能鍵的功能,例如某個功能鍵可以實現“自動截圖”,只需要對應增加一個新的具體命令類,在該命令類與螢幕處理者(ScreenHandler)之間建立一個關聯關係,然後將該具體命令類的物件通過配置檔案注入到某個功能鍵即可,原有程式碼無須修改,符合“開閉原則”。在此過程中,每一個具體命令類對應一個請求的處理者(接收者),通過向請求傳送者注入不同的具體命令物件可以使得相同的傳送者對應不同的接收者,從而實現“將一個請求封裝為一個物件,用不同的請求對客戶進行引數化”,客戶端只需要將具體命令物件作為引數注入請求傳送者,無須直接操作請求的接收者。

3)  請求傳送者與接收者解耦——命令模式(三)

4 命令佇列的實現

有時候我們需要將多個請求排隊,當一個請求傳送者傳送一個請求時,將不止一個請求接收者產生響應,這些請求接收者將逐個執行業務方法,完成對請求的處理。此時,我們可以通過命令佇列來實現。

命令佇列的實現方法有多種形式,其中最常用、靈活性最好的一種方式是增加一個CommandQueue類,由該類來負責儲存多個命令物件,而不同的命令物件可以對應不同的請求接收者,CommandQueue類的典型程式碼如下所示:

import java.util.*; 

class CommandQueue { 

   //定義一個ArrayList來儲存命令佇列 

   private ArrayList<Command> commands = newArrayList<Command>(); 

   public void addCommand(Command command) { 

       commands.add(command); 

   } 

   public void removeCommand(Command command) { 

       commands.remove(command); 

    }

   //迴圈呼叫每一個命令物件的execute()方法 

   public void execute() { 

       for (Object command : commands) { 

           ((Command)command).execute(); 

       } 

   } 

}

在增加了命令佇列類CommandQueue以後,請求傳送者類Invoker將針對CommandQueue程式設計,程式碼修改如下:

class Invoker { 

   private CommandQueue commandQueue; //維持一個CommandQueue物件的引用 

   //構造注入 

   public Invoker(CommandQueue commandQueue) { 

       this. commandQueue = commandQueue; 

    }

   //設值注入 

   public void setCommandQueue(CommandQueue commandQueue) { 

       this.commandQueue = commandQueue; 

    }

   //呼叫CommandQueue類的execute()方法 

   public void call() { 

       commandQueue.execute(); 

   } 

}

命令佇列與我們常說的“批處理”有點類似。批處理,顧名思義,可以對一組物件(命令)進行批量處理,當一個傳送者傳送請求後,將有一系列接收者對請求作出響應,命令佇列可以用於設計批處理應用程式,如果請求接收者的接收次序沒有嚴格的先後次序,我們還可以使用多執行緒技術來併發呼叫命令物件的execute()方法,從而提高程式的執行效率。

4)  請求傳送者與接收者解耦——命令模式(四)

5 撤銷操作的實現

在命令模式中,我們可以通過呼叫一個命令物件的execute()方法來實現對請求的處理,如果需要撤銷(Undo)請求,可通過在命令類中增加一個逆向操作來實現。

擴充套件

除了通過一個逆向操作來實現撤銷(Undo)外,還可以通過儲存物件的歷史狀態來實現撤銷,後者可使用備忘錄模式(Memento Pattern)來實現。

下面通過一個簡單的例項來學習如何使用命令模式實現撤銷操作:

Sunny軟體公司欲開發一個簡易計算器,該計算器可以實現簡單的數學運算,還可以對運算實施撤銷操作。

Sunny軟體公司開發人員使用命令模式設計瞭如圖5所示結構圖,其中計算器介面類CalculatorForm充當請求傳送者,實現了資料求和功能的加法類Adder充當請求接收者,介面類可間接呼叫加法類中的add()方法實現加法運算,並且提供了可撤銷加法運算的undo()方法。

圖5 簡易計算器結構圖

本例項完整程式碼如下所示:

//加法類:請求接收者 

class Adder { 

   private int num=0; //定義初始值為0 

   //加法操作,每次將傳入的值與num作加法運算,再將結果返回 

   public int add(int value) { 

       num += value; 

       return num; 

   } 

//抽象命令類 

abstract class AbstractCommand { 

   public abstract int execute(int value); //宣告命令執行方法execute() 

   public abstract int undo(); //宣告撤銷方法undo() 

//具體命令類 

class ConcreteCommand extendsAbstractCommand { 

   private Adder adder = new Adder(); 

   private int value; 

   //實現抽象命令類中宣告的execute()方法,呼叫加法類的加法操作 

       publicint execute(int value) { 

       this.value=value; 

       return adder.add(value); 

    }

   //實現抽象命令類中宣告的undo()方法,通過加一個相反數來實現加法的逆向操作 

   public int undo() { 

       return adder.add(-value); 

   } 

}

//計算器介面類:請求傳送者 

class CalculatorForm { 

   private AbstractCommand command; 

   public void setCommand(AbstractCommand command) { 

       this.command = command; 

    }

   //呼叫命令物件的execute()方法執行運算 

   public void compute(int value) { 

       int i = command.execute(value); 

       System.out.println("執行運算,運算結果為:" +i); 

    }

   //呼叫命令物件的undo()方法執行撤銷 

   public void undo() { 

       int i = command.undo(); 

       System.out.println("執行撤銷,運算結果為:" +i); 

   } 

}

編寫如下客戶端測試程式碼:

class Client { 

   public static void main(String args[]) { 

       CalculatorForm form = new CalculatorForm(); 

       AbstractCommand command; 

       command = new ConcreteCommand(); 

       form.setCommand(command); //向傳送者注入命令物件 

       form.compute(10); 

       form.compute(5); 

       form.compute(10); 

       form.undo(); 

   } 

}

編譯並執行程式,輸出結果如下:

執行運算,運算結果為:10

執行運算,運算結果為:15

執行運算,運算結果為:25

執行撤銷,運算結果為:15

思考

如果連續呼叫“form.undo()”兩次,預測客戶端程式碼的輸出結果。

需要注意的是在本例項中只能實現一步撤銷操作,因為沒有儲存命令物件的歷史狀態,可以通過引入一個命令集合或其他方式來儲存每一次操作時命令的狀態,從而實現多次撤銷操作。除了Undo操作外,還可以採用類似的方式實現恢復(Redo)操作,即恢復所撤銷的操作(或稱為二次撤銷)。

練習

修改簡易計算器原始碼,使之能夠實現多次撤銷(Undo)和恢復(Redo)。

5)  請求傳送者與接收者解耦——命令模式(五)

6 請求日誌

請求日誌就是將請求的歷史記錄儲存下來,通常以日誌檔案(Log File)的形式永久儲存在計算機中。很多系統都提供了日誌檔案,例如Windows日誌檔案、Oracle日誌檔案等,日誌檔案可以記錄使用者對系統的一些操作(例如對資料的更改)。請求日誌檔案可以實現很多功能,常用功能如下:

(1) “天有不測風雲”,一旦系統發生故障,日誌檔案可以為系統提供一種恢復機制,在請求日誌檔案中可以記錄使用者對系統的每一步操作,從而讓系統能夠順利恢復到某一個特定的狀態;

(2) 請求日誌也可以用於實現批處理,在一個請求日誌檔案中可以儲存一系列命令物件,例如一個命令佇列;

(3) 可以將命令佇列中的所有命令物件都儲存在一個日誌檔案中,每執行一個命令則從日誌檔案中刪除一個對應的命令物件,防止因為斷電或者系統重啟等原因造成請求丟失,而且可以避免重新發送全部請求時造成某些命令的重複執行,只需讀取請求日誌檔案,再繼續執行檔案中剩餘的命令即可。

在實現請求日誌時,我們可以將命令物件通過序列化寫到日誌檔案中,此時命令類必須實現Java.io.Serializable介面。下面我們通過一個簡單例項來說明日誌檔案的用途以及如何實現請求日誌:

Sunny軟體公司開發了一個網站配置檔案管理工具,可以通過一個視覺化介面對網站配置檔案進行增刪改等操作,該工具使用命令模式進行設計,結構如圖6所示:

圖6 網站配置檔案管理工具結構圖

現在Sunny軟體公司開發人員希望將對配置檔案的操作請求記錄在日誌檔案中,如果網站重新部署,只需要執行儲存在日誌檔案中的命令物件即可修改配置檔案。

本例項完整程式碼如下所示:

import java.io.*; 

import java.util.*; 

//抽象命令類,由於需要將命令物件寫入檔案,因此它實現了Serializable介面 

abstract class Command implementsSerializable { 

   protected String name; //命令名稱 

   protected String args; //命令引數 

   protected ConfigOperator configOperator; //維持對接收者物件的引用

   public Command(String name) { 

       this.name = name; 

    }

   public String getName() { 

       return this.name; 

    }

   public void setName(String name) { 

       this.name = name; 

    }

   public void setConfigOperator(ConfigOperator configOperator) { 

       this.configOperator = configOperator; 

    }

   //宣告兩個抽象的執行方法execute() 

   public abstract void execute(String args); 

   public abstract void execute(); 

//增加命令類:具體命令 

class InsertCommand extends Command { 

   public InsertCommand(String name) { 

       super(name); 

   } 

   public void execute(String args) { 

       this.args = args; 

       configOperator.insert(args); 

    }

   public void execute() { 

       configOperator.insert(this.args); 

   } 

}

//修改命令類:具體命令 

class ModifyCommand extends Command { 

   public ModifyCommand(String name) { 

       super(name); 

    }

   public void execute(String args) { 

       this.args = args; 

       configOperator.modify(args); 

    }

   public void execute() { 

       configOperator.modify(this.args); 

   } 

}

//省略了刪除命令類DeleteCommand 

//配置檔案操作類:請求接收者。由於ConfigOperator類的物件是Command的成員物件,它也將隨Command物件一起寫入檔案,因此ConfigOperator也需要實現Serializable介面 

class ConfigOperator implementsSerializable { 

   public void insert(String args) { 

       System.out.println("增加新節點:" +args); 

    }

   public void modify(String args) { 

       System.out.println("修改節點:" +args); 

    }

   public void delete(String args) { 

       System.out.println("刪除節點:" +args); 

   } 

}

//配置檔案設定視窗類:請求傳送者 

class ConfigSettingWindow { 

   //定義一個集合來儲存每一次操作時的命令物件 

   private ArrayList<Command> commands = newArrayList<Command>(); 

   private Command command;

   //注入具體命令物件 

   public void setCommand(Command command) { 

       this.command = command; 

    }

   //執行配置檔案修改命令,同時將命令物件新增到命令集合中 

   public void call(String args) { 

       command.execute(args); 

       commands.add(command); 

    }

   //記錄請求日誌,生成日誌檔案,將命令集合寫入日誌檔案 

   public void save() { 

       FileUtil.writeCommands(commands); 

    }

   //從日誌檔案中提取命令集合,並迴圈呼叫每一個命令物件的execute()方法來實現配置檔案的重新設定 

   public void recover() { 

       ArrayList list; 

       list = FileUtil.readCommands();

       for (Object obj : list) { 

           ((Command)obj).execute(); 

       } 

   } 

}

//工具類:檔案操作類 

class FileUtil { 

   //將命令集合寫入日誌檔案 

   public static void writeCommands(ArrayList commands) { 

       try { 

           FileOutputStream file = newFileOutputStream("config.log"); 

           //建立物件輸出流用於將物件寫入到檔案中 

           ObjectOutputStream objout = new ObjectOutputStream(newBufferedOutputStream(file)); 

           //將物件寫入檔案 

           objout.writeObject(commands); 

           objout.close(); 

       } 

       catch(Exception e) { 

           System.out.println("命令儲存失敗!");   

           e.printStackTrace(); 

       } 

    }

   //從日誌檔案中提取命令集合 

   public static ArrayList readCommands() { 

       try { 

           FileInputStream file = new FileInputStream("config.log"); 

           //建立物件輸入流用於從檔案中讀取物件 

           ObjectInputStream objin = new ObjectInputStream(newBufferedInputStream(file));

           //將檔案中的物件讀出並轉換為ArrayList型別 

           ArrayList commands = (ArrayList)objin.readObject(); 

           objin.close(); 

           return commands; 

       } 

       catch(Exception e) { 

           System.out.println("命令讀取失敗!"); 

           e.printStackTrace(); 

           return null;     

       }        

   } 

}

編寫如下客戶端測試程式碼:

class Client { 

   public static void main(String args[]) { 

       ConfigSettingWindow csw = new ConfigSettingWindow(); //定義請求傳送者 

       Command command; //定義命令物件 

       ConfigOperator co = new ConfigOperator(); //定義請求接收者 

       //四次對配置檔案的更改 

       command = new InsertCommand("增加"); 

       command.setConfigOperator(co); 

       csw.setCommand(command); 

       csw.call("網站首頁");

       command = new InsertCommand("增加"); 

       command.setConfigOperator(co); 

       csw.setCommand(command); 

        csw.call("埠號");

       command = new ModifyCommand("修改"); 

       command.setConfigOperator(co); 

       csw.setCommand(command); 

       csw.call("網站首頁");

       command = new ModifyCommand("修改"); 

       command.setConfigOperator(co); 

        csw.setCommand(command);         

       csw.call("埠號");

       System.out.println("----------------------------"); 

       System.out.println("儲存配置"); 

       csw.save();

       System.out.println("----------------------------");  

       System.out.println("恢復配置"); 

       System.out.println("----------------------------");  

       csw.recover();   

   } 

}

編譯並執行程式,輸出結果如下:

增加新節點:網站首頁

增加新節點:埠號

修改節點:網站首頁

修改節點:埠號

----------------------------

儲存配置

----------------------------

恢復配置

----------------------------

增加新節點:網站首頁

增加新節點:埠號

修改節點:網站首頁

修改節點:埠號

6)  請求傳送者與接收者解耦——命令模式(六)

7 巨集命令

巨集命令(Macro Command)又稱為組合命令,它是組合模式和命令模式聯