最近學習了責任鏈模式
前言
來菜鳥這個大家庭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作為一個責任物件進行鏈式呼叫,優雅實現,易複用可擴充套件。
==================================================================================
我不能保證寫的每個地方都是對的,但是至少能保證不復制、不黏貼,保證每一句話、每一行程式碼都經過了認真的推敲、仔細的斟酌。每一篇文章的背後,希望都能看到自己對於技術、對於生活的態度。
我相信喬布斯說的,只有那些瘋狂到認為自己可以改變世界的人才能真正地改變世界。面對壓力,我可以挑燈夜戰、不眠不休;面對困難,我願意迎難而上、永不退縮。
其實我想說的是,我只是一個程式設計師,這就是我現在純粹人生的全部。
===============================================================================