16.責任鏈模式Chain Of Responsibility

1.初識職責鏈模式

使多個對象都有機會處理請求,從而避免請求的發(fā)送者和接收者之間的耦合關系。將這些對象連成一條鏈,并沿著這條鏈傳遞該請求,直到有一個對象處理它為止。

  • Handler:定義職責的接口,通常在這里定義處理請求的方法,可以在這里實現(xiàn)后繼鏈。
    ConcreteHandler:實現(xiàn)職責的類,在這個類里面,實現(xiàn)對在它職責范圍內請求的處理,如果不處理,就繼續(xù)轉發(fā)請求給后繼者。
    Client:職責鏈的客戶端,向鏈上的具體處理者對象提交請求,讓職責鏈負責處理。

2.體會職責鏈模式

2.1 場景問題——申請聚餐費

用來考慮這樣一個功能:申請聚餐費用的管理。

申請聚餐費用的大致流程一般是:由申請人先填寫申請單,然后交給領導審查,如果申請批準下來了,領導會通知申請人審批通過,然后申請人去財務核領費用,如果沒有核準,領導會通知申請人審批未通過,此事也就此作罷了。

不同級別的領導,對于審批的額度是不一樣的,比如:項目經(jīng)理只能審批 500元以內的申請;部門經(jīng)理能審批1000元以內的申請;而總經(jīng)理可以審核任意額度的申請。

也就是說,當某人提出聚餐費用申請的請求后,該請求會由項目經(jīng)理、部門經(jīng)理、總經(jīng)理之中的某一位領導來進行相應的處理,但是提出申請的人并不知道最終會由誰來處理他的請求,一般申請人是把自己的申請?zhí)峤唤o項目經(jīng)理,或許最后是由總經(jīng)理來處理他的請求,但是申請人并不知道應該由總經(jīng)理來處理他申請請求。

那么該怎樣實現(xiàn)這樣的功能呢?

2.2 不用模式的解決方案

有何問題:
上面的實現(xiàn)很簡單,基本上沒難度。仔細想想,這么實現(xiàn)有沒有問題呢?
仔細分析申請聚餐費用的業(yè)務功能和目前的實現(xiàn),主要面臨著如下問題:

  • 1)聚餐費用申請的處理流程是可能會變動的。
  • 2)各個處理環(huán)節(jié)的業(yè)務處理也是會變動的。

如果采用上面的實現(xiàn),要是處理的邏輯發(fā)生了變化,解決的方法,一個是生成一個子類,覆蓋requestToProjectManager方法,然后在里面實現(xiàn)新的處理;另一個方法就是修改處理申請方法的源碼。

總之都不是什么好方法,也就是說,如果出現(xiàn)聚餐費用申請的處理流程變化的情況,或者是出現(xiàn)各個處理環(huán)節(jié)的功能變化的時候,上面的實現(xiàn)方式是很難靈活的變化來適應新功能的要求的。

把上面的問題抽象一下:客戶端發(fā)出一個請求,會有很多對象都可以來處理這個請求,而且不同對象的處理邏輯是不一樣的。對于客戶端而言,無所謂誰來處理,反正有對象處理就可以了。

而且在上述處理中,還希望處理流程是可以靈活變動的,而處理請求的對象需要能方便的修改或者是被替換掉,以適應新的業(yè)務功能的需要。

請問如何才能實現(xiàn)上述要求?

2.3 使用模式的解決方案

2.3.1 使用模式來解決的思路

仔細分析上面的場景,當客戶端提出一個聚餐費用的申請,后續(xù)處理這個申請的對象,項目經(jīng)理、部門經(jīng)理和總經(jīng)理,自然的形成了一個鏈,從項目經(jīng)理.部門經(jīng)理.總經(jīng)理,客戶端的申請請求就在這個鏈中傳遞,直到有領導處理為止。看起來,上面的功能要求很適合采用職責鏈來處理這個業(yè)務。

要讓處理請求的流程可靈活的變動,一個基本的思路,那就是動態(tài)構建流程步驟,這樣隨時都可以重新組合出新的流程來。而要讓處理請求的對象也要很靈活,那就要讓它足夠簡單,最好是只實現(xiàn)單一的功能,或者是有限的功能,這樣更有利于修改和復用。

職責鏈模式就很好的體現(xiàn)了上述的基本思路,首先職責鏈模式會定義一個所有處理請求的對象都要繼承實現(xiàn)的抽象類,這樣就有利于隨時切換新的實現(xiàn);其次每個處理請求對象只實現(xiàn)業(yè)務流程中的一步業(yè)務處理,這樣使其變得簡單;最后職責鏈模式會動態(tài)的來組合這些處理請求的對象,把它們按照流程動態(tài)組合起來,并要求它們依次調用,這樣就動態(tài)的實現(xiàn)了流程。

這樣一來,如果流程發(fā)生了變化,只要重新組合就好了;如果某個處理的業(yè)務功能發(fā)生了變化,一個方案是修改該處理對應的處理對象,另一個方案是直接提供一個新的實現(xiàn),然后在組合流程的時候,用新的實現(xiàn)替換掉舊的實現(xiàn)就可以了。

2.3.2 使用模式來解決的類圖

3.理解職責鏈模式

3.1 認識職責鏈模式

3.1.1 職責鏈模式的功能

職責鏈模式主要用來處理: “客戶端發(fā)出一個請求,有多個對象都有機會來處理這一個請求,但是客戶端不知道究竟誰會來處理他的請求 ”,這樣的情況。也就是需要讓請求者和接收者解耦,這樣就可以動態(tài)的切換和組合接收者了。

要注意在標準的職責鏈模式里面,是只要有對象處理了請求,這個請求就到此為止,不再被傳遞和處理了。

如果是要變形使用職責鏈,就可以讓這個請求繼續(xù)傳遞,每個職責對象對這個請求進行一定的功能處理,從而形成一個處理請求的功能鏈。

3.1.2 隱式接收者

當客戶端發(fā)出請求的時候,客戶端并不知道誰會真正處理他的請求,客戶端只知道他提交請求的第一個對象。從第一個處理對象開始,整個職責鏈里面的對象,要么自己處理請求,要么繼續(xù)轉發(fā)給下一個接收者。

也就是對于請求者而言,并不知道最終的接收者是誰,但是一般情況下,總是會有一個對象來處理的,因此稱為隱式接收者。

3.1.3 如何構建鏈

職責鏈的鏈怎么構建呢?這是個大問題,實現(xiàn)的方式也是五花八門,歸結起來大致有以下一些方式。

首先是按照實現(xiàn)的地方來說:

  • 1)可以實現(xiàn)在客戶端,在提交請求前組合鏈,也就是在使用的時候動態(tài)組合鏈,稱為外部鏈;
  • 2)可以在Handler里面實現(xiàn)鏈的組合,算是內部鏈的一種;
  • 3)可以在各個職責對象里面,由各個職責對象自行決定后續(xù)的處理對象,這種實現(xiàn)方式要求每個職責對象除了進行業(yè)務處理外,還必須了解整個業(yè)務流程。

按照構建鏈的數(shù)據(jù)來源,也就是決定了按照什么順序來組合鏈的數(shù)據(jù),又分為幾種:

  • 1)一種就是在程序里面動態(tài)組合;
  • 2)可以通過外部,如數(shù)據(jù)庫來獲取組合的數(shù)據(jù),這種屬于數(shù)據(jù)庫驅動的方式;
  • 3)還有一種方式就是通過配置文件傳遞進來,也可以是流程的配置文件。

如果是從外部獲取數(shù)據(jù)來構建鏈,那么在程序運行的時候,會讀取這些數(shù)據(jù),然后根據(jù)數(shù)據(jù)的要求來獲取相應的對象,并組合起來。

還有一種是不需要構建鏈,因為已有的對象已經(jīng)自然構成鏈了,這種情況多出現(xiàn)在組合模式構建的對象樹中,這樣子對象可以很自然的向上找到自己的父對象。就像部門人員的組織結構一樣,頂層是總經(jīng)理,總經(jīng)理下面是各個部門的經(jīng)理,部門經(jīng)理下面是項目經(jīng)理,項目經(jīng)理下面是各個普通員工,自然就可以形成:普通員工->項目經(jīng)理->部門經(jīng)理->總經(jīng)理這樣的鏈。

3.1.4 誰來處理

職責鏈中那么多處理對象,到底誰來處理請求呢,這個是在運行時期動態(tài)決定的。當請求被傳遞到某個處理對象的時候,這個對象會按照已經(jīng)設定好的條件來判斷,是否屬于自己處理的范圍,如果是就處理,如果不是就轉發(fā)請求給下一個對象。

3.1.5 請求一定會被處理嗎?

在職責鏈模式中,請求不一定會被處理,因為可能沒有合適的處理者,請求在職責鏈里面從頭傳遞到尾,每個處理對象都判斷不屬于自己處理,最后請求就沒有對象來處理。這一點是需要注意的。

可以在職責鏈的末端始終加上一個不支持此功能處理的職責對象,這樣如果傳遞到這里,就會出現(xiàn)提示,本職責鏈沒有對象處理這個請求。

3.2 處理多種請求

前面的示例都是同一個職責鏈處理一種請求的情況,現(xiàn)在有這樣的需求,還是費用申請的功能,這次是申請預支差旅費,假設還是同一流程,也就是組合同一個職責鏈,從項目經(jīng)理.傳遞給部門經(jīng)理.傳遞給總經(jīng)理,雖然流程相同,但是每個處理類需要處理兩種請求,它們的具體業(yè)務邏輯是不一樣的,那么該如何實現(xiàn)呢?

1.簡單的處理方式
要解決這個問題,也不是很困難,一個簡單的方法就是為每種業(yè)務單獨定義一個方法,然后客戶端根據(jù)不同的需要調用不同的方法,還是通過代碼來示例一下。注意這里故意的把兩個方法做的有些不一樣,一個是返回String類型的值,一個是返回boolean類型的值;另外一個是返回到客戶端再輸出信息,一個是直接在職責處理里面就輸出信息。

2.通用請求的處理方式
上面的實現(xiàn)看起來很容易,但是仔細想想,這樣實現(xiàn)有沒有什么問題呢?

有一個很明顯的問題,那就是只要增加一個業(yè)務,就需要修改職責的接口,這是很不靈活的,Java開發(fā)中很強調面向接口編程,因此接口應該相對保持穩(wěn)定,接口一改,需要修改的地方就太多了,頻繁修改接口絕對不是個好主意。

那有沒有什么好方法來實現(xiàn)呢?分析一下現(xiàn)在變化的東西:

  • 1)一是不同的業(yè)務需要傳遞的業(yè)務數(shù)據(jù)不同;
  • 2)二是不同的業(yè)務請求的方法不同;
  • 3)三是不同的職責對象處理這些不同的業(yè)務請求的業(yè)務邏輯不同

現(xiàn)在有一種簡單的方式,可以較好的解決這些問題。首先定義一套通用的調用框架,用一個通用的請求對象來封裝請求傳遞的參數(shù);然后定義一個通用的調用方法,這個方法不去區(qū)分具體業(yè)務,所有的業(yè)務都是這一個方法,那么具體的業(yè)務如何區(qū)分呢,就是在通用的請求對象里面會有一個業(yè)務的標記;到了職責對象里面,愿意處理就跟原來一樣的處理方式,如果不愿意處理,就傳遞到下一個處理對象就好了。

接下來看看如何在不改動現(xiàn)有的框架的前提下,擴展新的業(yè)務,這樣才能說明這種設計的靈活性。

3.3 功能鏈

在實際開發(fā)中,經(jīng)常會出現(xiàn)一個把職責鏈稍稍變形的用法。在標準的職責鏈中,一個請求在職責鏈中傳遞,只要有一個對象處理了這個請求,就會停止。

現(xiàn)在稍稍變一下,改成一個請求在職責鏈中傳遞,每個職責對象負責處理請求的某一方面功能,處理完成后,不是停止,而是繼續(xù)向下傳遞請求,當請求通過很多職責對象處理過后,功能也就處理完了,把這樣的職責鏈稱為功能鏈。

考慮這樣一個功能,在實際應用開發(fā)中,在進行業(yè)務處理之前,通常需要進行權限檢查、通用數(shù)據(jù)校驗、數(shù)據(jù)邏輯校驗等處理,然后才開始真正的業(yè)務邏輯實現(xiàn)??梢园堰@些功能分散到一個功能鏈中,這樣做的目的是使程序結構更加靈活,而且復用性會更好,比如通用的權限檢查就只需要做一份,然后就可以在多個功能鏈中使用了。

有些朋友看到這里,可能會想,這不是可以使用裝飾模式來實現(xiàn)嗎?沒錯,可以使用裝飾模式來實現(xiàn)這樣的功能,但是職責鏈會更靈活一些,因為裝飾模式是在已有的功能上增加新的功能,多個裝飾器之間會有一定的聯(lián)系;而職責鏈模式的各個職責對象實現(xiàn)的功能,相互之間是沒有關聯(lián)的,是自己實現(xiàn)屬于自己處理的那一份功能。

可能有些朋友會想到這很類似于在Web應用開發(fā)中的過濾器Filter,沒錯,過濾器鏈就類似于一個功能鏈,每個過濾器負責自己的處理,然后轉交給下一個過濾器,直到把所有的過濾器都走完,然后進入到Servlet里面進行處理。最常見的過濾器功能,比如權限檢查、字符集轉換等,基本上都是Web應用的標配。

接下來在示例中,實現(xiàn)這樣的功能:實現(xiàn)商品銷售的業(yè)務處理,在真正進行銷售的業(yè)務處理之前,需要對傳入處理的數(shù)據(jù),進行權限檢查、通用數(shù)據(jù)檢查和數(shù)據(jù)邏輯檢查,只有這些檢查都能通過的情況下,才說明傳入的數(shù)據(jù)是正確的、有效的數(shù)據(jù),才可以進行真正的業(yè)務功能處理。

3.4 職責鏈模式的優(yōu)缺點

  • 請求者和接收者松散耦合
  • 動態(tài)組合職責
  • 產(chǎn)生很多細粒度對象
  • 不一定能被處理

4.思考職責鏈模式

4.1 職責鏈模式的本質

職責鏈模式的本質是:分離職責,動態(tài)組合

4.2 何時選用

  • 1)如果有多個對象可以處理同一個請求,但是具體由哪個對象來處理該請求,是運行時刻動態(tài)確定的。這種情況可以使用職責鏈模式,把處理請求的對象實現(xiàn)成為職責對象,然后把它們構成一個職責鏈,當請求在這個鏈中傳遞的時候,具體由哪個職責對象來處理,會在運行時動態(tài)判斷
  • 2)如果你想在不明確指定接收者的情況下,向多個對象中的一個提交一個請求的話,可以使用職責鏈模式,職責鏈模式實現(xiàn)了請求者和接收者之間的解耦,請求者不需要知道究竟是哪一個接收者對象來處理了請求。
  • 3)如果想要動態(tài)指定處理一個請求的對象集合,可以使用職責鏈模式,職責鏈模式能動態(tài)的構建職責鏈,也就是動態(tài)的來決定到底哪些職責對象來參與到處理請求中來,相當于是動態(tài)指定了處理一個請求的職責對象集合

5.示例——論壇帖子的過濾

實例代碼參考github
論壇用戶發(fā)表帖子,但是常常會有用戶一些不良的信息,如廣告信息,涉黃信息,涉及政治的敏感詞等。

定義所有責任鏈對象的父類:

/**
 * 帖子處理器
 */
public abstract class PostHandler {

    /**
     * 后繼者
     */
    protected PostHandler successor;

    public void setSuccessor(PostHandler handler){
        this.successor = handler;
    }

    public abstract void handlerRequest(Post post);

    protected final void next(Post post){
        if(this.successor != null){
            this.successor.handlerRequest(post);
        }
    }
}

父類 Handler 主要封裝了傳遞請求等方法,其中要注意的有:

  • successor,后繼者,這個屬性很重要,它保存了責任鏈中下一個處理器
  • 在 next() 方法中(方法名自己隨便?。斦埱髠鬟f到最后一個責任對象時,已經(jīng)沒有后繼者繼續(xù)處理請求了,因此要對 successor 做判空處理,避免拋出空指針異常。
  • 處理請求的handlerRequest 的入?yún)⒑头祷仡愋涂梢愿鶕?jù)實際情況修改,可以在該方法中拋出異常來中斷請求

廣告處理器:

/**
 * 廣告處理器
 */
public class AdHandler extends PostHandler {

    @Override
    public void handlerRequest(Post post) {
        //屏蔽廣告內容
        String content = post.getContent();
        //.....
        content = content.replace("廣告","**");
        post.setContent(content);

        System.out.println("過濾廣告...");
        //傳遞給下一個處理器
        next(post);
    }
}

涉黃處理器:

/**
 * 涉黃處理器
 */
public class YellowHandler extends PostHandler {

    @Override
    public void handlerRequest(Post post) {
        //屏蔽涉黃內容
        String content = post.getContent();
        //.....
        content = content.replace("涉黃","**");
        post.setContent(content);

        System.out.println("過濾涉黃內容...");
        //傳遞給下一個處理器
        next(post);
    }
}

敏感處理器:

/**
 * 敏感詞處理器
 */
public class SensitiveWordsHandler extends PostHandler {

    @Override
    public void handlerRequest(Post post) {
        //屏蔽敏感詞
        String content = post.getContent();
        //.....
        content = content.replace("敏感詞","**");
        post.setContent(content);

        System.out.println("過濾敏感詞...");
        //傳遞給下一個處理器
        next(post);
    }
}

測試:

//創(chuàng)建責任對象
PostHandler adHandler = new AdHandler();
PostHandler yellowHandler = new YellowHandler();
PostHandler swHandler = new SensitiveWordsHandler();

//形成責任鏈
yellowHandler.setSuccessor(swHandler);
adHandler.setSuccessor(yellowHandler);

Post post = new Post();
post.setContent("我是正常內容,我是廣告,我是涉黃,我是敏感詞,我是正常內容");

System.out.println("過濾前的內容為:"+post.getContent());

adHandler.handlerRequest(post);

System.out.println("過濾后的內容為:"+post.getContent());

5.1 為什么用責任鏈模式?

直接將過濾不良信息寫在一個方法里不行嗎?比如:

public class PostUtil {

    public void filterContent(Post post){

        String content = post.getContent();

        content = content.replace("廣告","**");
        content = content.replace("涉黃","**");
        content = content.replace("敏感詞","**");

        post.setContent(content);
    }
}

如果后面要增加其他的功能,過濾其他類型的內容,我們還得修改上面的 filterContent 方法,違背了開閉原則。
因此我們需要使用責任鏈模式,能夠在不修改已有代碼的情況下擴展新功能。

6.案例

6.1 Servlet 中的 Filter

Servlet 中的過濾器 Filter 就是典型的責任鏈模式,假如我們要給每一次Http 請求都打印一個 log,就可以使用 Filter過濾器來實現(xiàn):

public class LogFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        System.out.println("write log");
        filterChain.doFilter(servletRequest,servletResponse);

    }

    @Override
    public void destroy() {

    }
}

然后將這個過濾器配置到 web.xml 中:

<filter>
  <filter-name>LogFilter</filter-name>
  <filter-class>com.zhoujun.filter.LogFilter</filter-class>
</filter>

<filter-mapping>
  <filter-name>LogFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

在上面LogFilter類中,我們可以看到servlet的責任鏈是通過 Filter 來實現(xiàn)的,這是一個接口,在doFilter中還用到了 FilterChain ,也是一個接口。通過查找源碼,發(fā)現(xiàn)了 FilterChain 的其中一個實現(xiàn)類:

public class PassThroughFilterChain implements FilterChain {
    @Nullable
    private Filter filter;
    @Nullable
    private FilterChain nextFilterChain;
    @Nullable
    private Servlet servlet;

    public PassThroughFilterChain(Filter filter, FilterChain nextFilterChain) {
        Assert.notNull(filter, "Filter must not be null");
        Assert.notNull(nextFilterChain, "'FilterChain must not be null");
        this.filter = filter;
        this.nextFilterChain = nextFilterChain;
    }

    public PassThroughFilterChain(Servlet servlet) {
        Assert.notNull(servlet, "Servlet must not be null");
        this.servlet = servlet;
    }

    public void doFilter(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        if (this.filter != null) {
            this.filter.doFilter(request, response, this.nextFilterChain);
        } else {
            Assert.state(this.servlet != null, "Neither a Filter not a Servlet set");
            this.servlet.service(request, response);
        }

    }
}

該類中的 Private FilterChain nextFilterChain; 相當于 PostHandler 中的后繼者 Successor。

將我們自定義的 Filter 配置到 web.xml 中的操作就是將該對象添加到責任鏈上,Servlet 開發(fā)者幫我們完成了 setSuccessor() 的操作。

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容