設(shè)計(jì)模式——責(zé)任鏈模式

設(shè)計(jì)模式——責(zé)任鏈模式

一. 簡介

責(zé)任鏈模式(Chain of Responsibility) 是行為型設(shè)計(jì)模式之一邪财,其將鏈中每一個(gè)節(jié)點(diǎn)看作是一個(gè)對象挽封,每個(gè)節(jié)點(diǎn)處理的請求均不同铁坎,且內(nèi)部自動(dòng)維護(hù)一個(gè)下一節(jié)點(diǎn)對象。當(dāng)一個(gè)請求從鏈?zhǔn)降氖锥税l(fā)出時(shí),會(huì)沿著鏈的路徑依次傳遞給每一個(gè)節(jié)點(diǎn)對象痴荐,直至有對象處理這個(gè)請求為止。

主要解決

責(zé)任鏈模式 解耦了請求與處理恰画,客戶只需將請求發(fā)送到鏈上即可宾茂,無需關(guān)心請求的具體內(nèi)容和處理細(xì)節(jié),請求會(huì)自動(dòng)進(jìn)行傳遞直至有節(jié)點(diǎn)對象進(jìn)行處理拴还。

二. 優(yōu)缺點(diǎn)

優(yōu)點(diǎn)

  • 解耦了請求與處理跨晴;
  • 請求處理者(節(jié)點(diǎn)對象)只需關(guān)注自己感興趣的請求進(jìn)行處理即可,對于不感興趣的請求片林,直接轉(zhuǎn)發(fā)給下一級(jí)節(jié)點(diǎn)對象端盆;
  • 具備鏈?zhǔn)絺鬟f處理請求功能怀骤,請求發(fā)送者無需知曉鏈路結(jié)構(gòu),只需等待請求處理結(jié)果焕妙;
  • 鏈路結(jié)構(gòu)靈活蒋伦,可以通過改變鏈路結(jié)構(gòu)動(dòng)態(tài)地新增或刪減責(zé)任;
  • 易于擴(kuò)展新的請求處理類(節(jié)點(diǎn))焚鹊,符合 開閉原則痕届;

缺點(diǎn)

  • 責(zé)任鏈路過長時(shí),可能對請求傳遞處理效率有影響末患;
  • 如果節(jié)點(diǎn)對象存在循環(huán)引用時(shí)研叫,會(huì)造成死循環(huán),導(dǎo)致系統(tǒng)崩潰璧针;

三.使用場景

  • 多個(gè)對象可以處理同一請求嚷炉,但具體由哪個(gè)對象處理則在運(yùn)行時(shí)動(dòng)態(tài)決定;
  • 在不明確指定接收者的情況下探橱,向多個(gè)對象中的一個(gè)提交一個(gè)請求申屹;
  • 可動(dòng)態(tài)指定一組對象處理請求;

三.場景demo

首先來看下 責(zé)任鏈模式 的通用 UML 類圖:


UML

從 UML 類圖中走搁,我們可以看到独柑,責(zé)任鏈模式 主要包含兩種角色:

  • 抽象處理者(Handler):定義一個(gè)請求處理的方法,并維護(hù)一個(gè)下一個(gè)處理節(jié)點(diǎn) Handler 對象的引用私植;
  • 具體處理者(ConcreteHandler):對請求進(jìn)行處理忌栅,如果不感興趣,則進(jìn)行轉(zhuǎn)發(fā)曲稼;

以下是 責(zé)任鏈模式 的通用代碼:

class Client {
    public static void main(String[] args) {
        Handler handlerA = new ConcreteHandlerA();
        Handler handlerB = new ConcreteHandlerB();
        handlerA.setnextHanlder(handlerB);
        handlerA.handleRequest("requestB");
    }

    static abstract class Handler {
        protected Handler mNextHandler;

        public void setnextHanlder(Handler successor) {
            this.mNextHandler = successor;
        }

        public abstract void handleRequest(String request);
    }

    static class ConcreteHandlerA extends Handler {

        @Override
        public void handleRequest(String request) {
            if ("requestA".equals(request)) {
                System.out.println(String.format("%s deal with request: %s", this.getClass().getSimpleName(), request));
                return;
            }
            if (this.mNextHandler != null) {
                this.mNextHandler.handleRequest(request);
            }
        }
    }
    static class ConcreteHandlerB extends Handler {

        @Override
        public void handleRequest(String request) {
            if ("requestB".equals(request)) {
                System.out.println(String.format("%s deal with request: %s", this.getClass().getSimpleName(), request));
                return;
            }
            if (this.mNextHandler != null) {
                this.mNextHandler.handleRequest(request);
            }
        }
    }
}

上面的代碼中索绪,其實(shí)我們把消息硬編碼為String類型,而真實(shí)業(yè)務(wù)中贫悄,消息是具備多樣性的瑞驱,可以是int、String或者是自定義類型····因此窄坦,我們可以在上面代碼中的基礎(chǔ)上唤反,將消息類型進(jìn)行抽象Request,增強(qiáng)了消息的包容性鸭津。

責(zé)任鏈模式 的本質(zhì)是:解耦請求與處理彤侍,讓請求在處理鏈中能進(jìn)行傳遞與被處理;理解 責(zé)任鏈模式 應(yīng)當(dāng)理解的是其模式(道)而不是其具體實(shí)現(xiàn)(術(shù))逆趋,責(zé)任鏈模式 的獨(dú)到之處是其將節(jié)點(diǎn)處理者組合成了鏈?zhǔn)浇Y(jié)構(gòu)盏阶,并允許節(jié)點(diǎn)自身決定是否進(jìn)行請求處理或轉(zhuǎn)發(fā),相當(dāng)于讓請求流動(dòng)了起來闻书。

個(gè)人認(rèn)為:責(zé)任鏈模式就相當(dāng)于一個(gè)鏈表的遞歸調(diào)用

三.OkHttp源碼之?dāng)r截器鏈

對于okhttp來說名斟,它的攔截器各位肯定是非常熟悉的脑慧,正是由于它的存在,okhttp的擴(kuò)展性極強(qiáng)砰盐,對于調(diào)用方來說可謂是非常友好闷袒。而實(shí)現(xiàn)這個(gè)攔截器的就是大名鼎鼎的責(zé)任鏈模式了,其實(shí)整個(gè)okhttp的核心功能都是基于這個(gè)攔截器的楞卡,由各種不同的攔截器實(shí)現(xiàn)的霜运。所以今天我們分析下okhttp的責(zé)任鏈模式。

基本源碼

對于okhttp的Call來說蒋腮,發(fā)起請求最終其實(shí)都調(diào)用了一個(gè)方法獲取到Response的淘捡,同步異步都是一樣的:

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
  }

前面只是加入一些必要的攔截器,是一些具體功能的實(shí)現(xiàn)池摧,我們今天并不是分析這些具體功能焦除,而是分析這種特殊的架構(gòu),所以這些攔截器就略過作彤,我們重點(diǎn)看這里

Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

return chain.proceed(originalRequest);

核心就是構(gòu)造了一個(gè)chain膘魄,這個(gè)chain我們重點(diǎn)關(guān)注的是interceptors和index(此時(shí)是第一個(gè)頭節(jié)點(diǎn),傳入的是0)竭讳,也就是 RetryAndFollowUpInterceptor创葡,然后調(diào)用chain.proceed()方法獲取最終結(jié)果。
那么我們繼續(xù)跟進(jìn)去:

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
     //此處省略其他代碼

    // Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
        connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
        writeTimeout);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);
     //此處省略其他代碼
    return response;
  })

可以看到绢慢,每一個(gè)chain都是鏈條上的一個(gè)節(jié)點(diǎn)灿渴,chain的proceed()方法首先是獲取下一個(gè)節(jié)點(diǎn),然后獲取當(dāng)前節(jié)點(diǎn)對應(yīng)的攔截器胰舆,將下一個(gè)節(jié)點(diǎn)當(dāng)做參數(shù)給了攔截器的intercept()方法骚露,在interceptor中會(huì)手動(dòng)調(diào)用下一個(gè)chain的chain.proceed方法,將這條鏈走下去缚窿。這里有一點(diǎn)要注意棘幸,此處是先獲取的下一個(gè)chain,也就是說我們在第n個(gè)攔截器中會(huì)調(diào)用第n+1個(gè)chain的proceed()方法倦零,這樣才能做到請求之前添加自定義行為(調(diào)用第n+1個(gè)chain的proceed()之前)和請求之后(調(diào)用第n+1個(gè)chain的proceed()之后)添加自定義行為误续。

疑問

現(xiàn)在,大家應(yīng)該都知道這條鏈?zhǔn)窃趺磦飨氯サ纳蠹铱赡軙?huì)感到很好奇女嘲,為什么這里會(huì)有兩個(gè)角色,interceptor和chain诞帐,而且這兩個(gè)結(jié)構(gòu)似乎不是傳統(tǒng)意義上的client和handler的角色(經(jīng)典的責(zé)任鏈模式主要是這兩個(gè)角色),因?yàn)檫@里interceptor中會(huì)調(diào)用chain的方法爆雹,這一行為似乎有點(diǎn)反常(此處的client角色應(yīng)該是RealCall)停蕉。由于暴露給外界的接口其實(shí)是interceptor愕鼓,這里我們嘗試拋棄chain看會(huì)怎樣,下面是一個(gè)鏈表結(jié)構(gòu)的經(jīng)典責(zé)任鏈實(shí)現(xiàn)

public class InterceptorChain {
    Response proceed(Request request){
        //此處意味著拿到第一個(gè)節(jié)點(diǎn)慧起,具體怎么拿到不用管菇晃,僅僅做示意
        Interceptor first = getFirstInterceptor();
        return first.intercept(request);
    }

    public abstract class  Interceptor{
        protected Interceptor next;
        abstract Response intercept(Request request);
        void setNext(Interceptor interceptor){
            next = interceptor;
        }
    }
}

經(jīng)典的責(zé)任鏈兩個(gè)角色,client(此處的InterceptorChain)和handler(此處的Interceptor)
此處我們嘗試去實(shí)現(xiàn)okhttp的日志攔截器蚓挤,主要實(shí)現(xiàn):

public class LogInterceptor extends Interceptor{

        @Override
        Response intercept(Request request) {
            //此處getRequestParams是偽代碼磺送,僅做示意
            Map<String,String> requestParams = request.getRequestParams();
            Response response= next.intercept(request);
            //此處getResult是偽代碼,僅做示意
            String result = response.getResult();
            return response;
        }
}

可以看到灿意,沒有chain這個(gè)類估灿,我們只是借助interceptor也是能實(shí)現(xiàn)現(xiàn)有的結(jié)構(gòu)的。那么這兩種結(jié)構(gòu)有本質(zhì)區(qū)別嗎缤剧?我們再看下正常的okhttp的日志攔截器:

public class LogInterceptor2 implements okhttp3.Interceptor{

        @Override
        public Response intercept(Chain chain) throws IOException {
            //此處getRequestParams是偽代碼馅袁,僅做示意
            Request request = chain.request();
            Map<String,String> requestParams = request.getRequestParams();
            Response response= chain.proceed(request);
            //此處getResult是偽代碼,僅做示意
            String result = response.getResult();
            return response;
        }
}

可以看到代碼幾乎一模一樣荒辕,只是一個(gè)直接調(diào)用interceptor實(shí)現(xiàn)汗销,一個(gè)通過chain來實(shí)現(xiàn)的,然而區(qū)別就在于這個(gè)chain.proceed()方法中:

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();

    calls++;

    // If we already have a stream, confirm that the incoming request will use it.
    if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must retain the same host and port");
    }

    // If we already have a stream, confirm that this is the only call to chain.proceed().
    if (this.httpCodec != null && calls > 1) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must call proceed() exactly once");
    }

    // Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
        connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
        writeTimeout);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);

    // Confirm that the next interceptor made its required call to chain.proceed().
    if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
      throw new IllegalStateException("network interceptor " + interceptor
          + " must call proceed() exactly once");
    }

    // Confirm that the intercepted response isn't null.
    if (response == null) {
      throw new NullPointerException("interceptor " + interceptor + " returned null");
    }

    if (response.body() == null) {
      throw new IllegalStateException(
          "interceptor " + interceptor + " returned a response with no body");
    }
    return response;
  }

總體看下來抵窒,本質(zhì)區(qū)別其實(shí)就一個(gè)弛针,okhttp的proceed()方法會(huì)被執(zhí)行多次(在每個(gè)interceptor中都會(huì)調(diào)用chain.proceed()獲取Response),每一個(gè)interceptor都會(huì)執(zhí)行該方法李皇,所以在該方法中我們可以對interceptor的實(shí)現(xiàn)做很多檢查削茁,做出一些約束。然而我們自己DIY的結(jié)構(gòu)疙赠,我們最理想的也只是把這些檢查封裝成一種util付材,由interceptor的實(shí)現(xiàn)人員手動(dòng)調(diào)用,然而靠interceptor開發(fā)人員調(diào)用的話很容易遺漏圃阳,所以這里應(yīng)該是借用了代理的思想厌衔,將它封裝在了chain中,必須通過chain的proceed()方法獲取Response捍岳,這些就能由框架做這些檢查和約束了富寿。

總結(jié)

okhttp的這套責(zé)任鏈模式的實(shí)現(xiàn),從代碼上來看確實(shí)復(fù)雜了不少锣夹,理解起來沒那么容易页徐,但是,這套模式能夠?qū)γ總€(gè)interceptor的行為做出一些基本的規(guī)范和檢查银萍,而不是都扔給interceptor的實(shí)現(xiàn)人員去做变勇,這點(diǎn)收益就大了,稍微難理解一點(diǎn)也是可以的,當(dāng)然我們更應(yīng)該學(xué)習(xí)的是這種將約束和檢查收斂到框架中的這種想和實(shí)現(xiàn)思路,比如這里就打破了常規(guī)的責(zé)任鏈搀绣,利用了代理的思想引入了一個(gè)chain飞袋。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市链患,隨后出現(xiàn)的幾起案子巧鸭,更是在濱河造成了極大的恐慌,老刑警劉巖麻捻,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纲仍,死亡現(xiàn)場離奇詭異,居然都是意外死亡贸毕,警方通過查閱死者的電腦和手機(jī)郑叠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來崖咨,“玉大人锻拘,你說我怎么就攤上這事』鞫祝” “怎么了署拟?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長歌豺。 經(jīng)常有香客問我推穷,道長,這世上最難降的妖魔是什么类咧? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任馒铃,我火速辦了婚禮,結(jié)果婚禮上痕惋,老公的妹妹穿的比我還像新娘区宇。我一直安慰自己,他們只是感情好值戳,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布议谷。 她就那樣靜靜地躺著,像睡著了一般堕虹。 火紅的嫁衣襯著肌膚如雪卧晓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天赴捞,我揣著相機(jī)與錄音逼裆,去河邊找鬼。 笑死赦政,一個(gè)胖子當(dāng)著我的面吹牛胜宇,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼掸屡,長吁一口氣:“原來是場噩夢啊……” “哼封寞!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起仅财,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎碗淌,沒想到半個(gè)月后盏求,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡亿眠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年碎罚,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片纳像。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡荆烈,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出竟趾,到底是詐尸還是另有隱情憔购,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布岔帽,位于F島的核電站玫鸟,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏犀勒。R本人自食惡果不足惜屎飘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望贾费。 院中可真熱鬧钦购,春花似錦、人聲如沸褂萧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽箱玷。三九已至怨规,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間锡足,已是汗流浹背波丰。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留舶得,地道東北人掰烟。 一個(gè)月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親纫骑。 傳聞我的和親對象是個(gè)殘疾皇子蝎亚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容