春節(jié)期間,我用責任鏈模式重構了業(yè)務代碼
本文轉載自微信公眾號「源碼興趣圈」,作者龍臺。轉載本文請聯(lián)系源碼興趣圈公眾號。
前言
文章開篇,拋出一個老生常談的問題,學習設計模式有什么作用?
設計模式主要是為了應對代碼的復雜性,讓其滿足開閉原則,提高代碼的擴展性
另外,學習的設計模式 一定要在業(yè)務代碼中落實,只有理論沒有真正實施,是無法真正掌握并且靈活運用設計模式的
這篇文章主要說 責任鏈設計模式,認識此模式是在讀 Mybatis 源碼時, Interceptor 攔截器主要使用的就是責任鏈,當時讀過后就留下了很深的印象(內心 OS:還能這樣玩)
文章先從基礎概念說起,另外分析一波 Mybatis 源碼中是如何運用的,最后按照 "習俗",設計一個真實業(yè)務場景上的應用
責任鏈設計模式大綱如下:
- 什么是責任鏈模式
- 完成真實的責任鏈業(yè)務場景設計
- Mybatis Interceptor 底層實現(xiàn)
- 責任鏈模式總結
什么是責任鏈模式
舉個例子,SpringMvc 中可以定義攔截器,并且可以定義多個。當一個用戶發(fā)起請求時,順利的話請求會經過所有攔截器,最終到達業(yè)務代碼邏輯,SpringMvc 攔截器設計就是使用了責任鏈模式
為什么說順利的話會經過所有攔截器?因為請求不滿足攔截器自定義規(guī)則會被打回,但這并不是責任鏈模式的唯一處理方式,繼續(xù)往下看
在責任鏈模式中,多個處理器(參照上述攔截器)依次處理同一個請求。一個請求先經過 A 處理器處理,然后再把請求傳遞給 B 處理器,B 處理器處理完后再傳遞給 C 處理器,以此類推,形成一個鏈條,鏈條上的每個處理器 各自承擔各自的處理職責
責任鏈模式中多個處理器形成的處理器鏈在進行處理請求時,有兩種處理方式:
- 請求會被 所有的處理器都處理一遍,不存在中途終止的情況,這里參照 MyBatis 攔截器理解
- 二則是處理器鏈執(zhí)行請求中,某一處理器執(zhí)行時,如果不符合自制定規(guī)則的話,停止流程,并且剩下未執(zhí)行處理器就不會被執(zhí)行,大家參照 SpringMvc 攔截器理解
這里通過代碼的形式對兩種處理方式作出解答,方便讀者更好的理解。首先看下第一種,請求會經過所有處理器執(zhí)行的情況
圖1 責任鏈模式一種實現(xiàn)
IHandler 負責抽象處理器行為,handle() 則是不同處理器具體需要執(zhí)行的方法,HandleA、HandleB 為具體需要執(zhí)行的處理器類,HandlerChain 則是將處理器串成一條鏈執(zhí)行的處理器鏈
- public class ChainApplication {
- public static void main(String[] args) {
- HandlerChain handlerChain = new HandlerChain();
- handlerChain.addHandler(Lists.newArrayList(new HandlerA(), new HandlerB()));
- handlerChain.handle();
- /**
- * 程序執(zhí)行結果:
- * HandlerA打?。簣?zhí)行 HandlerA
- * HandlerB打印:執(zhí)行 HandlerB
- */
- }
- }
這種責任鏈執(zhí)行方式會將所有的 處理器全部執(zhí)行一遍,不會被打斷。Mybatis 攔截器用的正是此類型,這種類型 重點在對請求過程中的數據或者行為進行改變
圖2 參考Mybatis攔截器實現(xiàn)
而另外一種責任鏈模式實現(xiàn),則是會對請求有阻斷作用,阻斷產生的前置條件是在處理器中自定義的,代碼中的實現(xiàn)較簡單,讀者可以聯(lián)想 SpringMvc 攔截器的實現(xiàn)流程
圖3 責任鏈模式一種實現(xiàn)
根據代碼看的出來,在每一個 IHandler 實現(xiàn)類中會返回一個布爾類型的返回值,如果返回布爾值為 false,那么責任鏈發(fā)起類會中斷流程,剩余處理器將不會被執(zhí)行。就像我們定義在 SpringMvc 中的 Token 攔截器,如果 Token 失效就不能繼續(xù)訪問系統(tǒng),處理器將請求打回
- public class ChainApplication {
- public static void main(String[] args) {
- HandlerChain handlerChain = new HandlerChain();
- handlerChain.addHandler(Lists.newArrayList(new HandlerA(), new HandlerB()));
- boolean resultFlag = handlerChain.handle();
- if (!resultFlag) {
- System.out.println("責任鏈中處理器不滿足條件");
- }
- }
- }
讀者可以自己在 IDEA 中實現(xiàn)兩種不同的責任鏈模式,對比其中的不同,設想下業(yè)務中真實的應用場景,再或者可以跑 SpringBoot 項目,創(chuàng)建多個攔截器來佐證文中的說辭
圖4 參考SpringMvc攔截器實現(xiàn)
本章節(jié)介紹了責任鏈設計模式的具體語義,以及不同責任鏈實現(xiàn)類型代碼舉例,并以 Mybatis、SpringMvc 攔截器為參照點,介紹各自不同的代碼實現(xiàn)以及應用場景
責任鏈業(yè)務場景設計
趁熱打鐵,本小節(jié)對使用的真實業(yè)務場景進行舉例說明。假設業(yè)務場景是這樣的,我們 系統(tǒng)處在一個下游服務,因為業(yè)務需求,系統(tǒng)中所使用的 基礎數據需要從上游中臺同步到系統(tǒng)數據庫
基礎數據包含了很多類型數據,雖然數據在中臺會有一定驗證,但是 數據只要是人為錄入就極可能存在問題,遵從對上游系統(tǒng)不信任原則,需要對數據接收時進行一系列校驗
最初是要進行一系列驗證原則才能入庫的,后來因為工期問題只放了一套非空驗證,趁著春節(jié)期間時間還算寬裕,把這套驗證規(guī)則骨架放進去
從我們系統(tǒng)的接入數據規(guī)則而言,個人覺得需要支持以下幾套規(guī)則
- 必填項校驗,如果數據無法滿足業(yè)務所必須字段要求,數據一旦落入庫中就會產生一系列問題
- 非法字符校驗,因為數據如何錄入,上游系統(tǒng)的錄入規(guī)則是什么樣的我們都不清楚,這一項規(guī)則也是必須的
- 長度校驗,理由同上,如果系統(tǒng)某字段長度限制 50,但是接入來的數據 500長度,這也會造成問題
為了讓讀者了解業(yè)務嵌入責任鏈模式的前因,這里列舉了三套校驗規(guī)則,當然真實中可能不止這三套。但是 一旦將責任鏈模式嵌入數據同步流程,就會 完全符合文初所提的開閉原則,提高代碼的擴展性
本案例設計模式中的開閉原則通過 Spring 提供支持,后續(xù)添加新的校驗規(guī)則就可以不必修改原有代碼
這里要再強調下,設計模式的應用場景一定要靈活掌握,只有這樣才能在合適的業(yè)務場景合理運用對象的設計模式
既然設計模式場景說過了,最后說一下需要達成的業(yè)務需求。將一個批量數據經過處理器鏈的處理,返回出符合要求的數據分類
定義頂級驗證接口和一系列處理器實現(xiàn)類沒什么難度,但是應該如何進行鏈式調用呢?
這一塊代碼需要有一定 Spring 基礎才能理解,一起來看下 VerifyHandlerChain 如何將所有處理器串成一條鏈
VerifyHandlerChain 處理流程如下:
- 實現(xiàn)自 InitializingBean 接口,在對應實現(xiàn)方法中獲取 IOC 容器中類型為 VerifyHandler 的 Bean,也就是 EmptyVerifyHandler、SexyVerifyHandler
- 將 VerifyHandler 類型的 Bean 添加到處理器鏈容器中
- 定義校驗方法 verify(),對入參數據展開處理器鏈的全部調用,如果過程中發(fā)現(xiàn)已無需要驗證的數據,直接返回
這里使用 SpringBoot 項目中默認測試類,來測試一下如何調用
- @SpringBootTest
- class ChainApplicationTests {
- @Autowired
- private VerifyHandlerChain verifyHandlerChain;
- @Test
- void contextLoads() {
- List<Object> verify = verifyHandlerChain.verify(Lists.newArrayList("源碼興趣圈", "@龍臺"));
- System.out.println(verify);
- }
- }
這樣的話,如果客戶或者產品提校驗相關的需求時,我們只需要實現(xiàn) VerifyHandler 接口新建個校驗規(guī)則實現(xiàn)類就 OK 了,這樣符合了設計模式的原則:滿足開閉原則,提高代碼的擴展性
熟悉之前作者寫過設計模式的文章應該知道,強調設計模式重語義,而不是具體的實現(xiàn)過程。所以,你看咱們這個校驗代碼,把責任鏈兩種模式結合了使用
上面的代碼只是示例代碼,實際業(yè)務中的實現(xiàn)要比這復雜很多,比如:
- 如何定義處理器的先后調用順序。比如說某一個處理器執(zhí)行時間很長并且過濾數據很少,所以希望把它放到最后面執(zhí)行
- 這是為當前業(yè)務的所有數據類型進行過濾,如何自定義單個數據類型過濾。比如你接入學生數據,學號有一定校驗規(guī)則,這種處理器類肯定只適合單一類型
還有很多的業(yè)務場景,所以設計模式強調的應該是一種思想,而不是固定的代碼寫法,需要結合業(yè)務場景靈活變通
責任鏈模式的好處
一定要使用責任鏈模式么?不使用能不能完成業(yè)務需求?
回答是肯定可以,設計模式只是幫助減少代碼的復雜性,讓其滿足開閉原則,提高代碼的擴展性。如果不使用同樣可以完成需求
如果不使用責任鏈模式,上面說的真實同步場景面臨兩個問題
- 如果把上述說的代碼邏輯校驗規(guī)則寫到一起,毫無疑問這個類或者說這個方法函數奇大無比。減少代碼復雜性一貫方法是:將大塊代碼邏輯拆分成函數,將大類拆分成小類,是應對代碼復雜性的常用方法。如果此時說:可以把不同的校驗規(guī)則拆分成不同的函數,不同的類,這樣不也可以滿足減少代碼復雜性的要求么。這樣拆分是能解決代碼復雜性,但是這樣就會面臨第二個問題
- 開閉原則:添加一個新的功能應該是,在已有代碼基礎上擴展代碼,而非修改已有代碼。大家設想一下,假設你寫了三套校驗規(guī)則,運行過一段時間,這時候領導讓加第四套,是不是要在原有代碼上改動
綜上所述,在合適的場景運用適合的設計模式,能夠讓代碼設計復雜性降低,變得更為健壯。朝更遠的說也能讓自己的編碼設計能力有所提高,告別被人吐槽的爛代碼...
Mybatis Interceptor底層實現(xiàn)
上面說了那么多,框架底層源碼是怎么設計并且使用責任鏈模式的?之前在看 Mybatis 3.4.x 源碼時了解到 Interceptor 底層實現(xiàn)就是責任鏈模式,這里和讀者分享 Interceptor 具體實現(xiàn)
開門見山,直接把視線聚焦到 Mybatis 源碼,版本號 3.4.7-SNAPSHOT
熟悉么?是不是和我們上面用到的責任鏈模式差不太多,有處理器集合 interceptors,有添加處理器方法
Mybatis Interceptor 不僅用到了責任鏈,還用到了動態(tài)代理,服務于 Mybatis 四大 "護教法王",在創(chuàng)建對象時通過動態(tài)代理和責任鏈相結合組裝而成插件模塊
- ParameterHandler
- ResultSetHandler
- StatementHandler
- Executor
使用過 Mybatis 的讀者應該知道,查詢 SQL 的分頁語句就是使用 Interceptor 實現(xiàn),比如市場上的 PageHelper、Mybatis-Plus 分頁插件再或者我們自實現(xiàn)的分頁插件(應該沒有項目組使用顯示調用多條語句組成分頁吧)
拿查詢語句舉例,如果定義了多個查詢相關的攔截器,會先經過攔截器的代碼加工,所有的攔截器執(zhí)行完畢后才會走真正查詢數據庫操作
扯的話就扯遠了,能夠知道如何用、在哪用就可以了。通過 Interceptor 也能知道一點,想要讀框架源碼,需要一定的設計模式基礎。如果對責任鏈、動態(tài)代理不清楚,那么就不能理解這一塊的精髓
結言
文章通過圖文并茂的方式幫助大家理解責任鏈設計模式,在兩種類型示例代碼以及舉例實際業(yè)務場景下,相信小伙伴已經掌握了如何在合適的場景使用責任鏈設計模式
看完文章后可以結合 Mybatis、SpringMvc 攔截器更深入掌握責任鏈模式的應用場景以及使用手法。另外可以結合項目中實際業(yè)務場景靈活使用,相信真正使用后的你會對責任鏈模式產生更深入的了解
文章參考:《設計模式之美:職責鏈模式》