1. 程式人生 > >JAVA設計模式(13):行為型-責任鏈模式(Responsibility)

JAVA設計模式(13):行為型-責任鏈模式(Responsibility)

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

 

1 採購單的分級審批

      Sunny

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

採購單分級審批示意圖

      如何在軟體中實現採購單的分級審批?Sunny

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

//採購單處理類  
public 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) {  
        //程式碼省略  
    }  
}  

copy

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

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

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

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

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


2 職責鏈模式概述

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

      職責鏈模式定義如下

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

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

 

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

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

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

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

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

abstract class Handler {  
    //維持對下家的引用  
protected Handler successor;  
      
    public void setSuccessor(Handler successor) {  
        this.successor=successor;  
    }  
      
    public abstract void handleRequest(String request);  
}  


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

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

public class ConcreteHandler extends Handler {  
    public void handleRequest(String request) {  
        if (請求滿足條件) {  
            //處理請求  
        }  
        else {  
            this.successor.handleRequest(request);  //轉發請求  
        }  
    }  
}  

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

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

 

思考

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


3 完整解決方案

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

        在圖3中,抽象類Approver充當抽象處理者(抽象傳遞者),DirectorVicePresidentPresidentCongress充當具體處理者(具體傳遞者),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() + "。");        //處理請求  
    }      
}  


      編寫如下客戶端測試程式碼:  
public 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 純與不純的職責鏈模式

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

 

       (1) 純的職責鏈模式

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

 

       (2)不純的職責鏈模式

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

 

5 職責鏈模式總結

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

 

       1.主要優點

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

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

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

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

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

      

       2.主要缺點

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

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

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

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

 

       3.適用場景

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

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

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

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

 

 

練習

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