1. 程式人生 > >最近學習了責任鏈模式

最近學習了責任鏈模式

前言

來菜鳥這個大家庭10個月了,總得來說比較融入了環境,同時在忙碌的工作中也深感技術積累不夠,在優秀的人身邊工作必須更加花時間去提升自己的技術能力、技術視野,所以開一個系列文章,標題就輕鬆一點叫做最近學習了XXX吧,記錄一下自己的學習心得。

由於最近想對系統進行一個小改造,想到使用責任鏈模式會非常適合,因此就係統地學習總結了一下責任鏈模式,分享給大家。

 

責任鏈模式的定義與特點

責任鏈模式的定義:使多個物件都有機會處理請求,從而避免請求的傳送者和接受者之間的耦合關係,將這個物件連成一條鏈,並沿著這條鏈傳遞該請求,直到有一個物件處理他為止

標準的責任鏈模式,個人總結下來有如下幾個特點:

  • 鏈上的每個物件都有機會處理請求
  • 鏈上的每個物件都持有下一個要處理物件的引用
  • 鏈上的某個物件無法處理當前請求,那麼它會把相同的請求傳給下一個物件

用一張圖表示以下使用了責任鏈模式之後的架構:

也就是說,責任鏈模式滿足了請求傳送者與請求處理者之間的鬆耦合,抽象非核心的部分,以鏈式呼叫的方式對請求物件進行處理

這麼說不明白?那麼下面通過實際例子讓你明白。

 

不使用責任鏈模式

為什麼要使用責任鏈模式,那麼我們得知道不使用責任鏈模式有什麼壞處,然後通過使用責任鏈模式如何將程式碼優化。

現在有一個場景:小明要去上學,媽媽給小明列了一些上學前需要做的清單(洗頭、吃早飯、洗臉),小明必須按照媽媽的要求,把清單上打鉤的事情做完了才可以上學。

首先我們定義一個準備列表PreparationList:

複製程式碼

1 public class PreparationList {
 2 
 3     /**
 4      * 是否洗臉
 5      */
 6     private boolean washFace;
 7     
 8     /**
 9      * 是否洗頭
10      */
11     private boolean washHair;
12     
13     /**
14      * 是否吃早餐
15      */
16     private boolean haveBreakfast;
17 
18     public boolean isWashFace() {
19         return washFace;
20     }
21 
22     public void setWashFace(boolean washFace) {
23         this.washFace = washFace;
24     }
25 
26     public boolean isWashHair() {
27         return washHair;
28     }
29 
30     public void setWashHair(boolean washHair) {
31         this.washHair = washHair;
32     }
33 
34     public boolean isHaveBreakfast() {
35         return haveBreakfast;
36     }
37 
38     public void setHaveBreakfast(boolean haveBreakfast) {
39         this.haveBreakfast = haveBreakfast;
40     }
41 
42     @Override
43     public String toString() {
44         return "ThingList [washFace=" + washFace + ", washHair=" + washHair + ", haveBreakfast=" + haveBreakfast + "]";
45     }
46     
47 }

複製程式碼

定義了三件事情:洗頭、洗臉、吃早餐。

接著定義一個學習類,按媽媽要求,把媽媽要求的事情做完了再去上學:

複製程式碼

1 public class Study {
 2 
 3     public void study(PreparationList preparationList) {
 4         if (preparationList.isWashHair()) {
 5             System.out.println("洗臉");
 6         }
 7         if (preparationList.isWashHair()) {
 8             System.out.println("洗頭");
 9         }
10         if (preparationList.isHaveBreakfast()) {
11             System.out.println("吃早餐");
12         }
13         
14         System.out.println("我可以去上學了!");
15     }
16     
17 }

複製程式碼

這個例子實現了我們的需求,但是不夠優雅,我們的主流程是學習,但是把要準備做的事情這些動作耦合在學習中,這樣有兩個問題:

  • PreparationList中增加一件事情的時候,比如增加化妝、打掃房間,必須修改study方法進行適配
  • 當這些事情的順序需要發生變化的時候,必須修改study方法,比如先洗頭再洗臉,那麼7~9行的程式碼必須和4~6行的程式碼互換位置

最糟糕的寫法,只是為了滿足功能罷了,違背開閉原則,即當我們擴充套件功能的時候需要去修改主流程,無法做到對修改關閉、對擴充套件開放。

 

使用責任鏈模式

接著看一下使用責任鏈模式的寫法,既然責任鏈模式的特點是“鏈上的每個物件都持有下一個物件的引用”,那麼我們就這麼做。

先抽象出一個AbstractPrepareFilter:

複製程式碼

1 public abstract class AbstractPrepareFilter {
 2 
 3     private AbstractPrepareFilter nextPrepareFilter;
 4     
 5     public AbstractPrepareFilter(AbstractPrepareFilter nextPrepareFilter) {
 6         this.nextPrepareFilter = nextPrepareFilter;
 7     }
 8 
 9     public void doFilter(PreparationList preparationList, Study study) {
10         prepare(preparationList);
11         
12         if (nextPrepareFilter == null) {
13             study.study();
14         } else {
15             nextPrepareFilter.doFilter(preparationList, study);
16         }
17     }
18     
19     public abstract void prepare(PreparationList preparationList);
20     
21 }

複製程式碼

留一個抽象方法prepare給子類去實現,在抽象類中持有下一個物件的引用nextPrepareFilter,如果有,則執行;如果沒有表示鏈上所有物件都執行完畢,執行Study類的study()方法:

複製程式碼

1 public class Study {
 2 
 3     public void study() {
 4         System.out.println("學習");
 5     }
 6     
 7 }

複製程式碼

接著我們實現AbstractPrepareList,就比較簡單了,首先是洗頭:

複製程式碼

1 public class WashFaceFilter extends AbstractPrepareFilter {
 2     
 3     public WashFaceFilter(AbstractPrepareFilter nextPrepareFilter) {
 4         super(nextPrepareFilter);
 5     }
 6 
 7     @Override
 8     public void prepare(PreparationList preparationList) {
 9         if (preparationList.isWashFace()) {
10             System.out.println("洗臉");
11         }
12         
13     }
14     
15 }

複製程式碼

接著洗臉:

複製程式碼

1 public class WashHairFilter extends AbstractPrepareFilter {
 2     
 3     public WashHairFilter(AbstractPrepareFilter nextPrepareFilter) {
 4         super(nextPrepareFilter);
 5     }
 6 
 7     @Override
 8     public void prepare(PreparationList preparationList) {
 9         if (preparationList.isWashHair()) {
10             System.out.println("洗頭");
11         }
12         
13     }
14     
15 }

複製程式碼

最後吃早餐:

複製程式碼

1 public class HaveBreakfastFilter extends AbstractPrepareFilter {
 2     
 3     public HaveBreakfastFilter(AbstractPrepareFilter nextPrepareFilter) {
 4         super(nextPrepareFilter);
 5     }
 6 
 7     @Override
 8     public void prepare(PreparationList preparationList) {
 9         if (preparationList.isHaveBreakfast()) {
10             System.out.println("吃早餐");
11         }
12         
13     }
14     
15 }

複製程式碼

最後我們看一下呼叫方如何編寫:

複製程式碼

1 @Test
 2 public void testResponsibility() {
 3     PreparationList preparationList = new PreparationList();
 4     preparationList.setWashFace(true);
 5     preparationList.setWashHair(false);
 6     preparationList.setHaveBreakfast(true);
 7         
 8     Study study = new Study();
 9         
10     AbstractPrepareFilter haveBreakfastFilter = new HaveBreakfastFilter(null);
11     AbstractPrepareFilter washFaceFilter = new WashFaceFilter(haveBreakfastFilter);
12     AbstractPrepareFilter washHairFilter = new WashHairFilter(washFaceFilter);
13         
14     washHairFilter.doFilter(preparationList, study);
15 }

複製程式碼

至此使用責任鏈模式修改這段邏輯完成,看到我們完成了學習與準備工作之間的解耦,即核心的事情我們是要學習,此時無論加多少準備工作,都不需要修改study方法,只需要修改呼叫方即可。

但是這種寫法好嗎?個人認為這種寫法雖然符合開閉原則,但是兩個明顯的缺點對客戶端並不友好:

  • 增加、減少責任鏈物件,需要修改客戶端程式碼,即比如我這邊想要增加一個打掃屋子的操作,那麼testResponsibility()方法需要改動
  • AbstractPrepareFilter washFaceFilter = new WashFaceFilter(haveBreakfastFilter)這種呼叫方式不夠優雅,客戶端需要思考一下,到底真正呼叫的時候呼叫三個Filter中的哪個Filter

為此,我們來個終極版的、升級版的責任鏈模式。

 

升級版責任鏈模式

上面我們寫了一個責任鏈模式,這種是一種初級的符合責任鏈模式的寫法,最後也寫了,這種寫法是有明顯的缺點的,那麼接著我們看一下升級版的責任鏈模式如何寫,解決上述問題。

以下的寫法也是Servlet的實現方式,首先還是抽象一個Filter:

1 public interface StudyPrepareFilter {
 2 
 3     public void doFilter(PreparationList preparationList, FilterChain filterChain);
 4     
 5 }

注意這裡多了一個FilterChain,也就是責任鏈,是用於串起所有的責任物件的,它也是StudyPrepareFilter的一個子類:

複製程式碼

1 public class FilterChain implements StudyPrepareFilter {
 2 
 3     private int pos = 0;
 4 
 5     private Study study;
 6     
 7     private List<StudyPrepareFilter> studyPrepareFilterList;
 8     
 9     public FilterChain(Study study) {
10         this.study = study;
11     }
12 
13     public void addFilter(StudyPrepareFilter studyPrepareFilter) {
14         if (studyPrepareFilterList == null) {
15             studyPrepareFilterList = new ArrayList<StudyPrepareFilter>();
16         }
17         
18         studyPrepareFilterList.add(studyPrepareFilter);
19     }
20     
21     @Override
22     public void doFilter(PreparationList thingList, FilterChain filterChain) {
23         // 所有過濾器執行完畢
24         if (pos == studyPrepareFilterList.size()) {
25             study.study();
26         }
27         
28         studyPrepareFilterList.get(pos++).doFilter(thingList, filterChain);
29     }
30     
31 }

複製程式碼

即這裡有一個計數器,假設所有的StudyPrepareFilter沒有呼叫完畢,那麼呼叫下一個,否則執行Study的study()方法。

接著就比較簡單了,實現StudyPrepareFilter類即可,首先還是洗頭:

複製程式碼

1 public class WashHairFilter implements StudyPrepareFilter {
 2 
 3     @Override
 4     public void doFilter(PreparationList preparationList, FilterChain filterChain) {
 5         if (preparationList.isWashHair()) {
 6             System.out.println("洗完頭髮");
 7         }
 8         
 9         filterChain.doFilter(preparationList, filterChain);
10     }
11     
12 }

複製程式碼

注意,這裡每個實現類需要顯式地呼叫filterChain的doFilter方法。洗臉:

複製程式碼

1 public class WashFaceFilter implements StudyPrepareFilter {
 2 
 3     @Override
 4     public void doFilter(PreparationList preparationList, FilterChain filterChain) {
 5         if (preparationList.isWashFace()) {
 6             System.out.println("洗完臉");
 7         }
 8         
 9         filterChain.doFilter(preparationList, filterChain);
10     }
11     
12 }

複製程式碼

吃早飯:

複製程式碼

1 public class HaveBreakfastFilter implements StudyPrepareFilter {
 2 
 3     @Override
 4     public void doFilter(PreparationList preparationList, FilterChain filterChain) {
 5         if (preparationList.isHaveBreakfast()) {
 6             System.out.println("吃完早飯");
 7         }
 8         
 9         filterChain.doFilter(preparationList, filterChain);
10     }
11     
12 }

複製程式碼

最後看一下呼叫方:

複製程式碼

1 @Test
 2 public void testResponsibilityAdvance() {
 3     PreparationList preparationList = new PreparationList();
 4     preparationList.setWashFace(true);
 5     preparationList.setWashHair(false);
 6     preparationList.setHaveBreakfast(true);
 7         
 8     Study study = new Study();
 9         
10     StudyPrepareFilter washFaceFilter = new WashFaceFilter();
11     StudyPrepareFilter washHairFilter = new WashHairFilter();
12     StudyPrepareFilter haveBreakfastFilter = new HaveBreakfastFilter();
13         
14     FilterChain filterChain = new FilterChain(study);
15     filterChain.addFilter(washFaceFilter);
16     filterChain.addFilter(washHairFilter);
17     filterChain.addFilter(haveBreakfastFilter);
18         
19     filterChain.doFilter(preparationList, filterChain);
20 }

複製程式碼

完美解決第一版責任鏈模式存在的問題,至此增加、修改責任物件客戶端呼叫程式碼都不需要再改動。

有的人可能會問,你這個增加、減少責任物件,testResponsibilityAdvance()方法,不是還得addFilter,或者刪除一行嗎?我們回想一下,Servlet我們增加或減少Filter需要改動什麼程式碼嗎?不用,我們需要改動的只是web.xml而已。同樣的道理,FilterChain裡面有studyPrepareFilterList,我們完全可以把FilterChain做成一個Spring Bean,所有的Filter具體實現類也都是Spring Bean,注入studyPrepareFilterList就好了,虛擬碼為:

複製程式碼

1 <bean id="filterChain" class="xxx.xxx.xxx.FilterChain">
2     <property name="studyPrepareFilterList">
3         <list>
4             <ref bean="washFaceFilter" />
5             <ref bean="washHairFilter" />
6             <ref bean="haveBreakfastFilter" />
7         </list>
8     </property>
9 </bean>

複製程式碼

這樣是不是完美解決了問題?我們新增、減少Filter,或者修改Filter順序,只需要修改.xml檔案即可,不僅核心邏輯符合開閉原則,呼叫方也符合開閉原則。

 

責任鏈模式的使用場景

這個就不多說了,最典型的就是Servlet中的Filter,有了上面的分析,大家應該也可以理解Servlet中責任鏈模式的工作原理了,然後為什麼一個一個的Filter需要配置在web.xml中。

 

責任鏈模式的結構

想想看,好像責任鏈模式也沒有什麼太複雜的結構,將責任抽象,實現責任介面,客戶端發起呼叫,網上找了一張圖表示一下:

 

 

責任鏈模式的優點及使用場景

最後說說責任鏈模式的優點吧,大致有以下幾點:

  • 實現了請求傳送者與請求處理者之間的鬆耦合
  • 可動態新增責任物件、刪除責任物件、改變責任物件順序,非常靈活
  • 每個責任物件專注於做自己的事情,職責明確

什麼時候需要用責任鏈模式?這個問題我是這麼想的:系統設計的時候,注意區分主次就好,即哪部分是核心流程,哪部分是輔助流程,輔助流程是否有N多if...if...if...的場景,如果是且每個if都有一個統一的抽象,那麼抽象輔助流程,把每個if作為一個責任物件進行鏈式呼叫,優雅實現,易複用可擴充套件。

================================================================================== 

我不能保證寫的每個地方都是對的,但是至少能保證不復制、不黏貼,保證每一句話、每一行程式碼都經過了認真的推敲、仔細的斟酌。每一篇文章的背後,希望都能看到自己對於技術、對於生活的態度。

我相信喬布斯說的,只有那些瘋狂到認為自己可以改變世界的人才能真正地改變世界。面對壓力,我可以挑燈夜戰、不眠不休;面對困難,我願意迎難而上、永不退縮。

其實我想說的是,我只是一個程式設計師,這就是我現在純粹人生的全部。

===============================================================================