責(zé)任鏈模式的學(xué)習(xí)與應(yīng)用

定義

責(zé)任鏈模式(Chain of Responsibility)是多個(gè)對(duì)象都有機(jī)會(huì)處理請(qǐng)求,從而避免請(qǐng)求的發(fā)送者和接受者之間的耦合關(guān)系.將這些對(duì)象連成一條鏈,并沿著這條鏈傳遞該請(qǐng)求,直到有對(duì)象能夠處理.

場(chǎng)景

員工想要請(qǐng)假,在OA系統(tǒng)中需要審批.
對(duì)于不同的請(qǐng)假天數(shù),審批人是不一樣的.小于3天,組長(zhǎng)審批,大于三天小于7天,主管審批,大于7天小于15天CTO審批.

典型實(shí)現(xiàn)

    public Response handle(Request req) {
        if (req.day < 0 || req.day > 15) {
            throw new IllegalArgumentException();
        }
        if (req.day <= 3) {
            return new Leader().handle(req);
        } else if (req.day <= 7) {
            return new Director().handle(req);
        } else {
            return new CTO().handle(req);
        }
    }

實(shí)際場(chǎng)景可能要比這個(gè)復(fù)雜一些,比如Leader,Director,CTO可能是通過(guò)注入而不是new的.

目前的類圖:

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

  1. 代碼清晰明了.

缺點(diǎn):

  1. 耦合度高,客戶端代碼依賴所有處理類. 如果后續(xù)想要繼續(xù)添加處理類,就需要繼續(xù)添加else if.
  2. 順序也是定死的.如果要改順序.就必須重新修改.

改進(jìn)[責(zé)任鏈]

既然我們的所有處理類都實(shí)現(xiàn)了IHandler接口.那我們的執(zhí)行流程在編譯層面就是標(biāo)準(zhǔn)化的.那我們能不能通過(guò)鏈接各個(gè)處理類的方式,讓Client依賴的實(shí)現(xiàn)最小呢?當(dāng)然可以.和鏈表類似.我們可以構(gòu)造一個(gè)處理器鏈.只需要在IHandler里添加setNext()方法即可.

改進(jìn)后代碼

IHandler實(shí)現(xiàn)類:

class Leader implements IHandler {

    private IHandler next;

    public Response handle(Request req) {
        if (req.getDay() <= 3) {
            return new Response("Leader audit");
        }
        return this.next.handle(req);
    }

    @Override
    public void setNext(IHandler handler) {
        this.next = handler;
    }
}

其他類似,就不貼了.

業(yè)務(wù)代碼:

    public Response handle(Request req) {
        if (req.day < 0 || req.day > 15) {
            throw new IllegalArgumentException();
        }
        return handler.handle(req);
    }

客戶端代碼:

    public static void main(String[] args) {
        IHandler handler = new Leader();
        IHandler director = new Director();
        IHandler cto = new CTO();
        handler.setNext(director);
        director.setNext(cto);
        CORDemo01 demo = new CORDemo01();
        demo.handler = handler;
        Response response = demo.handle(new Request(6));
        System.out.println(response.getMessage());
    }

改進(jìn)后的類圖

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

  1. 不再依賴所有實(shí)現(xiàn).而是只依賴第一個(gè)實(shí)現(xiàn).而其他的實(shí)現(xiàn)通過(guò)鏈接的方式進(jìn)行組裝.
  2. 我們已經(jīng)可以通過(guò)某些方式動(dòng)態(tài)的組裝我們的鏈了.

總結(jié)

純粹的責(zé)任鏈模式這樣就介紹完了.
從傳統(tǒng)的編程方式過(guò)度到責(zé)任鏈模式.實(shí)際上還是很容易理解了.為的就是減少調(diào)用類與處理類之間的耦合.
另外值得注意的是,純粹的責(zé)任鏈模式,只有一個(gè)處理器進(jìn)行處理.而其他的并不參與執(zhí)行.
而我們?cè)趯?shí)際使用的場(chǎng)景中,比如過(guò)濾器,則是多個(gè)處理器都會(huì)參與執(zhí)行.
下面就討論一下.責(zé)任鏈的幾個(gè)變種.

責(zé)任鏈模式列表方式實(shí)現(xiàn)

我們看到純的責(zé)任鏈模式中,使用了鏈表的形式.并且暴露了第一個(gè)執(zhí)行單元.
這種鏈表的方式在組裝的時(shí)候如果有很多個(gè)節(jié)點(diǎn),將十分繁瑣.對(duì)于這一部分,有一種常見的優(yōu)化方案.
是將所有的處理器按順序放到一個(gè)List中進(jìn)行處理.代碼如下

IHandler實(shí)現(xiàn)類:

class Leader implements IHandler {

    public Response handle(Request req) {
        if (req.getDay() <= 3) {
            return new Response("Leader audit");
        }
        return null;
    }
}

其他類似,就不貼了.

HandlerChain

class HandlerChain {

    private List<IHandler> processList = Lists.newArrayList();

    public HandlerChain addHandler(IHandler handler) {
        if (handler != null) {
            processList.add(handler);
        }
        // 鏈?zhǔn)秸{(diào)用.
        return this;
    }

    public void setProcessList(List<IHandler> processList) {
        this.processList = processList;
    }

    public Response process(Request req) {
        for (IHandler handler : processList) {
            Response response = handler.handle(req);
            if (response == null) {
                continue;
            }
            return response;
        }
        throw new IllegalArgumentException();
    }
}

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

  1. 通過(guò)這種方式,我們可以較為輕松的組裝我們的處理鏈.
  2. 通過(guò)setProcessList方法.我們也可以動(dòng)態(tài)的更改處理流程.

不純粹的責(zé)任鏈

過(guò)濾器

不純粹的責(zé)任鏈?zhǔn)侵?進(jìn)入鏈之后,并不是只有一個(gè)處理器才能執(zhí)行.而是所有的處理器都可能參與執(zhí)行.
典型的場(chǎng)景就是過(guò)濾器

下面我們簡(jiǎn)單實(shí)現(xiàn)一個(gè)過(guò)濾器.我們有一個(gè)List的String.我們將其中長(zhǎng)度大于1,首字母是w的字符串都挑選出來(lái).如果用過(guò)Java8的朋友肯定知道,使用stream表達(dá)式,filter就可以很容易的實(shí)現(xiàn)這個(gè)需求.但是實(shí)際場(chǎng)景中,可能要比這個(gè)復(fù)雜的多.這里只是舉個(gè)簡(jiǎn)單的例子.來(lái)說(shuō)明這種不純粹的責(zé)任鏈的實(shí)現(xiàn).實(shí)現(xiàn)方式采取兩種.
傳統(tǒng)方式.利用Java8 Function的antThen組裝多個(gè)Function的方式實(shí)現(xiàn)過(guò)濾器.

  • 傳統(tǒng)方式

    接口類

    public interface IFilter {
        List<String> filter(List<String> toFilter);
    }   
    

    組裝類

    class FilterChain {
    
        private List<IFilter> filterList;
    
        public List<String> process(List<String> toFilter) {
            for (IFilter filter : filterList) {
                toFilter = filter.filter(toFilter);
            }
            return toFilter;
        }
    }
    

    兩個(gè)實(shí)現(xiàn)類

    class LengthFilter implements IFilter {
        @Override
        public List<String> filter(List<String> toFilter) {
            List<String> result = Lists.newArrayList();
            for (String str : toFilter) {
                if (StringUtils.isNotBlank(str) && str.length() > 1) {
                    result.add(str);
                }
            }
            return result;
        }
    }
    
    class StartCharFilter implements IFilter {
        @Override
        public List<String> filter(List<String> toFilter) {
            List<String> result = Lists.newArrayList();
            for (String str : toFilter) {
                if (StringUtils.isNotBlank(str) && str.startsWith("w")) {
                    result.add(str);
                }
            }
            return result;
        }
    }
    
    

    值得注意的是有很多模板代碼.這些模板代碼在傳統(tǒng)方式下,可以通過(guò)模板模式進(jìn)行簡(jiǎn)化.

  • Java8 Function方式

    public class HandlerChain {
    
        private static UnaryOperator<List<String>> filterByLength =
                (List<String> text) -> text.stream().filter(item -> item.length() > 1).collect(Collectors.toList());
    
        private static UnaryOperator<List<String>> spellCheckerProcessing =
                (List<String> text) -> text.stream().filter(item -> item.startsWith("w")).collect(Collectors.toList());
    
        private static Function<List<String>, List<String>> filterChain =
                filterByLength.andThen(spellCheckerProcessing);
    
        public static List<String> filter(List<String> input) {
            return filterChain.apply(input);
        }
    }
    
    

當(dāng)然在這個(gè)需求下,你也可以直接使用stream.將兩個(gè)filter連接起來(lái).
但是如果你考慮純粹的責(zé)任鏈模式.上面的Java8 Funciton的方式.確實(shí)可以節(jié)省很多代碼和類.

攔截器

Servlet,Struts2,Spring MVC,都有攔截器.而這些攔截器的實(shí)現(xiàn).則也是不純粹的責(zé)任鏈的一種.因?yàn)樗械臄r截器都會(huì)執(zhí)行一遍.如果確認(rèn)攔截,則直接返回.如果通過(guò),則繼續(xù)執(zhí)行.看起來(lái)和過(guò)濾器很像.

但是攔截器的功能不止于此,攔截器不僅支持preAction操作,還支持postAction操作.我們想要討論的就是這種設(shè)計(jì)是如何編碼實(shí)現(xiàn)的.

接口定義

public interface IInterceptor {

    boolean before();

    void after();
}

攔截器與目標(biāo)對(duì)象的組裝類

public class MainExecuteProxy {

    private Object target;

    private List<IInterceptor> interceptorList = Lists.newArrayList();

    public MainExecuteProxy addInterceptor(IInterceptor interceptor) {
        interceptorList.add(interceptor);
        return this;
    }

    public String execute() {
        return process(interceptorList.iterator(), target);
    }

    private String process(Iterator<IInterceptor> interceptorIterator, Object target) {
        if (interceptorIterator.hasNext()) {
            IInterceptor interceptor = interceptorIterator.next();
            boolean before = interceptor.before();
            if (!before) {
                return "執(zhí)行失敗";
            }
            String res = process(interceptorIterator, target);
            interceptor.after();
            return res;
        } else {
            // do action.
            System.out.println("real action");
            return "執(zhí)行成功";
        }
    }
}

代碼分析

MainExecuteProxy類里需要有一個(gè)目標(biāo)對(duì)象,用來(lái)執(zhí)行目標(biāo)方法.而interceptorList就是我們之前講到的責(zé)任鏈.那這種攔截是怎么實(shí)現(xiàn)的呢.主要的邏輯就是process方法.我們的邏輯很簡(jiǎn)單.就是當(dāng)我們的所有before方法都為true的時(shí)候執(zhí)行目標(biāo)對(duì)象的方法.當(dāng)有一個(gè)失敗的時(shí)候,就立馬攔截.而在攔截并且執(zhí)行完目標(biāo)方法后,我們需要在一個(gè)一個(gè)的反向執(zhí)行after方法.因?yàn)閎efore/after是配對(duì)的.

為了達(dá)到這種目的,我們使用了遞歸.原因就是遞歸是可以保留遞歸現(xiàn)場(chǎng)的.所以的方法在調(diào)用的時(shí)候進(jìn)入到方法棧,先進(jìn)去的在下面,后進(jìn)去的在上面.所以當(dāng)我們?cè)趫?zhí)行完目標(biāo)方法一層一層返回的時(shí)候,也是后調(diào)用的先執(zhí)行after.達(dá)到了效果.

Client代碼

public class Main {

    public static void main(String[] args) {
        MainExecuteProxy executeProxy = new MainExecuteProxy();

        String result = executeProxy.addInterceptor(new IInterceptor() {
            @Override
            public boolean before() {
                System.out.println("aaa");
                return true;
            }

            @Override
            public void after() {
                System.out.println("aaa");
            }
        }).addInterceptor(new IInterceptor() {
            @Override
            public boolean before() {
                System.out.println("bbb");
                return false;
            }

            @Override
            public void after() {
                System.out.println("bbb");
            }
        }).execute();

        System.out.println(result);
    }
}

執(zhí)行結(jié)果:

aaa
bbb
aaa
執(zhí)行失敗

先進(jìn)入攔截器一的before,輸出aaa,返回true,繼續(xù)執(zhí)行
進(jìn)入攔截器二的before,輸出bbb,返回false.停止繼續(xù)執(zhí)行,返回執(zhí)行失敗.
回到上一層的代碼.繼續(xù)執(zhí)行攔截器一的after.輸出aaa.然后返回.
和預(yù)期的相符.

總結(jié)

攔截器模式,無(wú)論純粹或變種,在實(shí)際開發(fā)和框架中都會(huì)時(shí)常看到,需要我們掌握.并且在合適的時(shí)機(jī)予以使用.

參考資料

  1. https://zhuanlan.zhihu.com/p/24737592
  2. 設(shè)計(jì)模式之禪
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末纺铭,一起剝皮案震驚了整個(gè)濱河市粘拾,隨后出現(xiàn)的幾起案子腿时,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡蕊肥,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門蛤肌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)壁却,“玉大人,你說(shuō)我怎么就攤上這事裸准≌苟” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵狼速,是天一觀的道長(zhǎng)琅锻。 經(jīng)常有香客問(wèn)我,道長(zhǎng)向胡,這世上最難降的妖魔是什么恼蓬? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮僵芹,結(jié)果婚禮上处硬,老公的妹妹穿的比我還像新娘。我一直安慰自己拇派,他們只是感情好荷辕,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布凿跳。 她就那樣靜靜地躺著,像睡著了一般疮方。 火紅的嫁衣襯著肌膚如雪控嗜。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天骡显,我揣著相機(jī)與錄音疆栏,去河邊找鬼。 笑死惫谤,一個(gè)胖子當(dāng)著我的面吹牛壁顶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播溜歪,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼若专,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了蝴猪?” 一聲冷哼從身側(cè)響起调衰,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎自阱,沒(méi)想到半個(gè)月后窖式,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡动壤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了淮逻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片琼懊。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖爬早,靈堂內(nèi)的尸體忽然破棺而出哼丈,到底是詐尸還是另有隱情,我是刑警寧澤筛严,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布醉旦,位于F島的核電站,受9級(jí)特大地震影響桨啃,放射性物質(zhì)發(fā)生泄漏车胡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一照瘾、第九天 我趴在偏房一處隱蔽的房頂上張望匈棘。 院中可真熱鬧,春花似錦析命、人聲如沸主卫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)簇搅。三九已至完域,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瘩将,已是汗流浹背吟税。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鸟蟹,地道東北人乌妙。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像建钥,于是被迫代替她去往敵國(guó)和親藤韵。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

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

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理熊经,服務(wù)發(fā)現(xiàn)泽艘,斷路器,智...
    卡卡羅2017閱讀 134,654評(píng)論 18 139
  • 從三月份找實(shí)習(xí)到現(xiàn)在镐依,面了一些公司匹涮,掛了不少,但最終還是拿到小米槐壳、百度然低、阿里、京東务唐、新浪雳攘、CVTE、樂(lè)視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,243評(píng)論 11 349
  • 1 場(chǎng)景問(wèn)題# 1.1 申請(qǐng)聚餐費(fèi)用## 來(lái)考慮這樣一個(gè)功能:申請(qǐng)聚餐費(fèi)用的管理枫笛。 很多公司都有這樣的福利吨灭,就是項(xiàng)...
    七寸知架構(gòu)閱讀 3,133評(píng)論 3 58
  • 今天的主題是兄弟姐妹,我畫了個(gè)圓寫上了妹妹刑巧、弟弟喧兄。發(fā)現(xiàn)我和弟弟妹妹總是有互補(bǔ)的地方。原來(lái)啊楚,我們加起來(lái)才是一個(gè)圓吠冤。 ...
    幸運(yùn)的遇見閱讀 297評(píng)論 2 1
  • 你見過(guò)如此美麗的貨幣嗎咨演?是不是對(duì)這些錢幣“一見鐘情”,然后就舍不得花掉它們呢蚯斯?嘿嘿薄风,這些漂亮的貨幣的名字叫做“手工...
    安洛可閱讀 568評(píng)論 0 1