設計模式之【職責鏈模式】

Gopher大威發表於2022-04-11

金三銀四,表妹拿到大廠offer了...

表妹:哥啊,經過過五關,斬六將,我終於拿到某廠的offer啦。

:哇!恭喜恭喜呀!

表妹:真是太不容易了。

:說說看,怎麼不容易呀?

表妹:一二面都順利通過了,HR面也很順利,但是最後在談薪環節,我這邊提出更高的薪資,結果又安排多一輪總監面。

:結果怎麼樣呢,通過了嘛?

表妹:還好通過啦。

你看,一輪面試,二輪面試,HR面試,後面又根據情況,加了一輪總監面。只有前面的面試通過了,才有機會進入到下一輪,這些面試串成一條鏈,不是很像我們設計模式中的職責鏈模式嘛?

使多個物件都有機會處理請求,從而避免了請求的傳送者和接收者之間的耦合關係。將這些物件連成一條鏈,並沿著這條鏈傳遞該請求,直到有物件處理它為止。

我覺得這句話更好理解,“將鏈中的每一個節點看作是一個物件,每個節點處理的請求均不同,且每個節點內部自動維護了其後繼節點物件,當一個請求在鏈路的頭部出發時,會沿著鏈的路徑依次傳遞給每一個節點物件,直到有物件處理這個請求為止。”---責任鏈模式原理和示例及其在Spring原始碼中的運用

 

我們知道,使用者生成的內容可能包含一些敏感的詞,比如涉黃、廣告、或者是政治敏感詞。現在有這麼一個需求,就是稽核使用者生成的內容,如果含有這方面詞彙的話,就不允許釋出。

我們先來看一下第一種實現方式:

 1 public class SensitiveWordFilter {
 2    // 如果內容中不包含敏感資訊的話,就返回true
 3     public boolean filter(Content content) {
 4         if (!filterSexyWord(content)) {
 5             return false;
 6         }
 7         if (!filterAdsWord(content)) {
 8             return false;
 9         }
10         if (!filterPoliticalWord(word)) {
11             return false;
12         }
13         
14         return true;
15     }
16     
17     private boolean filterSexyWord(Content content) {
18         boolean legal = true;
19         // 判斷是否含有黃色資訊
20         return legal;
21     }
22     
23     private boolean filterAdsWord(Content content) {
24         boolean legal = true;
25         // 判斷是否含有廣告
26         return legal;
27     }
28     
29     private boolean filterPoliticalWord(Content content) {
30         boolean legal = true; 
31         // 判斷是否含有政治敏感資訊
32         return legal;
33     }
34 }

這樣寫確實是能夠實現需求,但是呢,很明顯可以看到的是,有太多的if分支了,如果還要判斷是否包含隱私資訊,非法暴力資訊等,那就會有更多的if分支。其次就是違背了開-閉原則,如果新增隱私資訊過濾器,不但要在這個類裡面新增一個filterPrivacyWord()方法,還要在filter()方法中增加一個if分支。

這時候,職責鏈模式就派上用場啦~

職責鏈模式

  • Hanlder:抽象處理者角色,定義一個處理請求的介面,包含抽象處理方法和一個後繼處理器。

  • ConcreteHandler:Handler實現類,處理它所負責的請求。

  • Client:向一個鏈上的具體處理者物件提交請求。

我們應用設計模式,主要是為了應對程式碼的複雜性,讓其滿足開閉原則,提高程式碼的擴充套件性。

Handler

1 public abstract SensitiveWordFilter {
2     protected SensitiveWordFilter successor;
3     
4     public void setSuccessor(SensitiveWordFilter successor) {
5         this.successor = successor;
6     }
7     
8     public abstract void doFilter(Content content);
9 }

ConcreteHandler

 1 public class SexyWordFilter extends SensitiveWordFilter {
 2     @Override
 3     public void doFilter(Content content) {
 4         boolean legal = true;
 5         // 實現判斷是否有涉黃敏感資訊的邏輯
 6         if (legal) {
 7             successor.doFilter(content);   // 不包含涉黃敏感資訊,繼續檢測其他敏感資訊
 8         } else {
 9             System.out.println("內容不合法,不可以釋出!");
10         }
11     }
12 }
13 14 public class AdsWordFilter extends SensitiveWordFilter {
15     @Override 
16     public void doFilter(Content content) {
17         boolean legal = true;
18         // 實現判斷是否有廣告資訊的邏輯
19         if (legal) {
20             successor.doFilter(content);   // 不包含廣告資訊,繼續監測其他敏感資訊
21         } else {
22             System.out.println("內容不合法,不可以釋出!");
23         }
24     }
25 }
26 27 public class PoliticalWordFilter extends SensitiveWordFilter {
28     @Override 
29     public void doFilter(Content content) {
30         boolean legal = true;
31         // 實現判斷是否有政治敏感資訊的邏輯
32         if (legal) {
33             System.out.println("內容合法,可以釋出!");   // 因為PoliticalWordFilter是最後一個節點了,所以,無需再傳遞到下一個節點
34         } else {
35             System.out.println("內容不合法,不可以釋出!");
36         }
37     }
38 }

Client

public class Demo {
    public static void main(String[] args) {
        SensitiveWordFilter sexyWordFilter = new SexyWordFilter();
        SensitiveWordFilter adsWordFilter = new AdsWordFilter();
        SensitiveWordFilter politicalWordFilter = new PoliticalWordFilter();
        sexyWordFilter.setSuccessor(adsWordFilter);
        adsWordFilter.setSuccessor(politicalWordFilter);
        
        sexyWordFilter.doFilter(new Content());  // 從職責鏈開始呼叫
    }
}

你看,我們使用職責鏈模式之後,把各個敏感詞過濾函式繼續拆分出來,設計成獨立的類,進一步簡化了SensitiveWordFilter類,讓SensitiveWordFilter類的程式碼不會過多,過複雜。

其次,當我們要擴充套件新的過濾演算法的時候,比如,我們還需要過濾隱私資訊,只需要新增一個Filter類,並且在客戶端中將它新增到FilterChain中即可,其他程式碼完全不用修改。這就遵守了開閉原則。

其實,職責鏈模式可以細分為兩種。

純與不純的職責鏈模式

純的職責鏈模式:一個具體處理者角色只能對請求作出兩種行為中的一個:一個是自己處理(承擔責任),另一個是把責任推給下家。不允許出現某一個具體處理者物件在承擔了一部分責任後又將責任向下傳的情況。請求在責任鏈中必須被處理,不能出現無果而終的結局。

不純的職責鏈模式:允許某個請求被一個具體處理者部分處理後再向下傳遞,或者一個具體處理者處理完某請求後,其後繼處理器可以繼續處理該請求,而且一個請求可以最終不被任何處理者物件接受。

我們上面例子中,使用的就是純的職責鏈模式。

職責鏈模式的優點

  • 降低了物件之前的耦合度。

    該模式使得一個物件無需知道到底是哪一個物件處理其請求以及鏈的結構,傳送者和接收者也無需擁有對方的明確資訊。

  • 增強了系統的可擴充套件性。

    可以根據需要增加新的請求處理類,滿足開閉原則

  • 增強了給物件指派職責的靈活性。

    當工作流程發生變化,可以動態地改變鏈內的成員或者調動它們的次序,也可動態地新增或者刪除責任。

  • 責任鏈簡化了物件之間的連線。

    每個物件只需保持一個指向其後繼者的引用,不需保持其他所有處理者的引用,這避免了眾多if...else...語句。

  • 責任分擔。

    每個類只需要處理自己該處理的工作,不該處理的傳遞給下一個物件完成,明確各類的責任範圍,符合類的單一職責原則。

職責鏈模式的缺點

  • 不能保證每個請求一定被處理。

    由於一個請求沒有明確的接收者,所以不能保證它一定會被處理,該請求可能一直傳到鏈的末端都不能得到處理。

  • 對比較長的職責鏈,請求的處理可能涉及多個處理物件,系統效能受到一定影響。

  • 職責鏈建立的合理性要靠客戶端來保證,增加了客戶端的複雜性,可能會由於職責鏈的錯誤設定而導致系統出錯,如可能會造成迴圈呼叫。

職責鏈模式的應用場景

職責鏈模式主要是解耦了請求與處理,使用者只需要將請求傳送到鏈上即可,無需關心請求的具體內容和處理細節,請求會自動進行傳遞直至有節點進行處理。常用在框架開發中,用來實現框架的過濾器、攔截器功能,讓框架的使用者不需要修改框架原始碼的情況下,新增新的過濾攔截功能。除此之外,還適用於以下場景:

  • 多個物件可以處理同一個請求,但具體由哪個物件處理則在執行時動態決定。

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

  • 可以動態指定一組物件的處理請求。

職責鏈模式在開原始碼中的應用

職責鏈模式最常用來開發框架的過濾器和攔截器,比如RPC中的鏈式攔截器,Java開發中常用的元件Servlet Filter和Spring Interceptor。我們來看一下Spring Interceptor。它是Spring MVC框架的一部分,由Spring MVC框架來提供實現。客戶端傳送的請求,會先經過Servlet Filter,然後再經過Spring Interceptor,最後到達具體的業務程式碼中。整體流程如下圖所示:

 

攔截器 HandlerInterceptor定義了3個方法 preHandle、postHandle、afterCompletion。preHandle()對請求的攔截,postHandle()是對響應的攔截。

 1 public interface HandlerInterceptor {
 2     //Called after HandlerMapping determined an appropriate handler object, but before HandlerAdapter invokes the handler
 3     boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
 4     
 5     //Called after HandlerAdapter actually invoked the handler, but before the DispatcherServlet renders the view
 6     void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception;
 7     
 8     // Callback after completion of request processing, that is, after renderingthe view
 9     void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception;
10  
11 }

接下來,我們來剖析一下,Spring Interceptor底層是如何實現的。

它是基於職責鏈模式實現的,其中,HandlerExecutionChain類是職責鏈模式中的處理器鏈。它的實現相較於Tomcat中的ApplicationFilterChain來說,邏輯更清晰,不需要使用遞迴來實現,主要是因為它將請求和響應的攔截工作,拆分到了兩個函式中實現。

 1 public class HandlerExecutionChain {
 2     private final Object handler; 
 3     private HandlerInterceptor[] interceptors;
 4     
 5     //執行鏈中攔截器的 preHandle 方法,遇到返回 false,執行 triggerAfterCompletion 方法觸發
 6     boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
 7         HandlerInterceptor[] interceptors = getInterceptors();
 8         if (!ObjectUtils.isEmpty(interceptors)) {
 9             for (int i = 0; i < interceptors.length; i++) {
10                 HandlerInterceptor interceptor = interceptors[i];
11                 if (!interceptor.preHandle(request, response, this.handler)) {
12                     triggerAfterCompletion(request, response, null);
13                     return false;
14                 }
15                 this.interceptorIndex = i;
16             }
17         }
18         return true;
19     }
20     
21     //DispatcherServlet 中 doDispatch 方法呼叫,呼叫鏈中攔截器的 postHandle 方法
22     void applyPostHandle(HttpServletRequest request, HttpServletResponse response, ModelAndView mv) throws Exception {
23         HandlerInterceptor[] interceptors = getInterceptors();
24         if (!ObjectUtils.isEmpty(interceptors)) {
25             for (int i = interceptors.length - 1; i >= 0; i--) {
26                 HandlerInterceptor interceptor = interceptors[i];
27                 interceptor.postHandle(request, response, this.handler, mv);
28             }
29         }
30     }
31  
32     //執行鏈中截止到 applyPreHandle 返回 false 的 攔截器的 afterCompletion 方法
33     void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, Exception ex) throws Exception {
34         HandlerInterceptor[] interceptors = getInterceptors();
35         if (!ObjectUtils.isEmpty(interceptors)) {
36             for (int i = this.interceptorIndex; i >= 0; i--) {
37                 HandlerInterceptor interceptor = interceptors[i];
38                 try {
39                     interceptor.afterCompletion(request, response, this.handler, ex);
40                 }
41                 catch (Throwable ex2) {
42                     logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
43                 }
44             }
45         }
46     }
47 }

在Spring框架中,DispatcherServlet的doDispatch()方法來分發請求,它在真正的業務邏輯執行前後,執行HandlerExecutionChain中的applyPreHandle()和applyPostHandle()函式,用來實現攔截的功能。

總結

各人自掃門前雪,莫管他人瓦上霜。

參考

極客時間專欄《設計模式之美》

 

相關文章