設計模式之責任鏈模式案例解析

此文來源於源碼閱讀網的魯班大叔,此文以設計一個緩存模塊爲引題,然後闡述責任鏈模式。

一、設計一個緩存模塊

接下來我們在分析緩存模塊的需求如下:

  1. 存儲在內存中,想要的時候立⻢可取(速度快)
  2. 存儲在硬盤,斷電不會丟失緩存數據
  3. 存儲在第三方應用中(Redis\MongoDb),擴展性好和第三方軟件進行集程 4. 淘汰策略:先進先出(FiFo)
  4. 淘汰策略:最近最少使用(LRU)
  5. 緩存過期:
  6. 記錄緩存命中率
  7. 保證線程安全,對緩存的操作必須是同步的
  8. 緩存阻塞,防止緩存擊穿
  9. 序列化,與反序列化
    在這裏插入圖片描述

解決方案一:

使用一個類專⻔類實現緩存需求,每個方法對應一個功能。爲降低使用的複雜性,只對外開放 必須的方法。
Propertites configs;
getObject() 獲取緩存
putObject() 設置緩存 clear() 清除緩存 store() 存儲
logging()
記錄緩存命中率
方案優點:內聚性強,所有相關緩存相關的功能全部集中在一起,閱讀代碼方便,不用在各個 類文件中跳來跳去。
方案缺點:偶合性高、一些必須和不必須的全部偶合到了一起,擴展功能時只能在這個類中繼 續添加,久而久之這個類就會很臃腫,擴展性不強。
在這裏插入圖片描述

解決方案二:

將一些相關性高的功能抽像成接口:

  1. 存儲機制
  2. 過期淘汰策略
  3. 命中率統計
  4. 阻塞 然後將各個部分進行組合成一個緩存器。基於配置來決定使用哪個部件。
    方案優點:如果要增加新的存儲機制,只要在存儲接口中新添加實現類即可,所以相對於第一 種方案靈活性更高。 方案缺點:由於新增了多個接口和類,整個實現的複雜度是成陪上升。這種複雜度的上升對開 發者不會有太大的影響。但對於源碼的維 護者卻是一個災難,而且是核災,在梳理流程或調試問題時,需要在各個類和接口之間跳來跳 去。另外它的擴展性也並沒有想像中的好。因爲所有的擴展必須在已知的接口之上進行擴展, 當新增的需求不符合任何一個接口定義時,就傻眼了。比如說我現在要添加一個功能實現緩存 阻塞,以防止緩存穿透。這時要麼新增接口定義,要麼在緩存上下文中添中。但無論怎麼做都 會使複雜度進一步一上升。最後無法控制。
    工作不滿1年的人會用方案一,因爲沒有什麼設計能力,只能是要啥功能就直接加。而工作1 到5年的喜歡用方案二,有一定設計能力,但設計不到點子上,反而會經常犯過度設計的毛 病。5年以上的又會使用回方案一 因爲這時他更關注快速實現需求。
    在這裏插入圖片描述

解決方案三:

怎樣設計纔是最優方案呢?
來看 下 大神Clinton Begin 他是怎麼做的。Clinton Begin 它就是mybatis作者,他20年前寫 的代碼現在依然值得我們去學習。
他是這麼做的。首先沒有緩存上下文組件。把所有功能都抽像成一個接口,所有功能都去實現 這個接口。存儲、淘汰機制、緩存過期、緩存阻塞。各個功能實現,完全分開,互相之間沒有 任何依賴。最大程度保證了低耦合。那各個功能實現怎麼進行關聯組合呢?前兩個方案是通過 一個緩存上下文將各功能部件進行了組合。Clinton Begin 的設計是讓各個部件之間串聯成一 個鏈條。具體做法是A部件引用B部件實例、B部件引用C、C以在引用D 類推下去。那這種引 用,不就不成導致高度耦合了嗎?其實並沒有,因各部件在引用下個部件時,它並沒有直接去 依賴下個部件,而是通過接口間接進行的引用。所以它即可以引用A,又可以引用B,只要是 這個接口的實現即可,至於實現是什麼不用去關心。 有了這個鏈條,只要在鏈條的頭部發起調用,整個鏈條中的邏輯都會被執行。
現在一起來看看MyBat is具體實現代碼。
緩存的各個部件實現:
在這裏插入圖片描述
在這裏插入圖片描述

緩存配置:

public @interface CacheNamespace {
Class<? extends org.apache.ibatis.cache.Cache> implementation() default
PerpetualCache.class;
Class<? extends org.apache.ibatis.cache.Cache> eviction() default
LruCache.class;
long flushInterval() default 0; int size() default 1024;
boolean readWrite() default true; boolean blocking() default false; /**
* Property values for a implementation object. * @since 3.4.2
*/
Property[] properties() default {}b }

在這裏插入圖片描述


整個鏈條的執行過程

可以看到Cache實例結構就是一個個緩存零部件組成的鏈條。無論是調用實例中的哪個方法都 會依次往下傳遞。 當我們修改配置之後,鏈條又會重新進行組合。最大限度保證靈活性。在擴展性方面,只需實 現Cache接口,然後把實現器織入鏈條即可。不需要在額外定義接口。
在這裏插入圖片描述
如上面講的緩存例子 整個鏈條就包括:緩存序列化->記錄緩存命中率->處理過期緩存->淘汰 策略->存儲。另外這種模式還有一個優點就是它的,業務表述清晰,把鏈條一展現,我們就 知道他要看幹嘛了。所以各大框架都在大量的使用這種模式。比如Dubbo在它的各個場景當中 就大量的使用責任鏈,如客戶端發起遠程調用過程包含:Mock處理、負載均衡、集羣容錯、 初始上下文、監控統計、異步轉同步等邏輯就是通過責任鏈模式組合而成。還有他的服務響應 也是大量使用責任鏈模式,所以如果不提前看學習責任鏈設計模式,這Dubbo源碼是沒法看 的。

經典責任鏈:

聽到這有些同學可能會存在疑問,這和我們在書本上學的責任鏈模式好像不一樣呀。書本 上的責任鏈講的都是。請求只會被唯一的節點處理,如果當前節點處理不了,就會交給下個請 求。如果能處理,就不在向下傳遞。其實這兩種設計都屬於責任鏈模式,前者是採用多個節點 分散承擔責任,後者由特定的節點來承擔責任。 後者比前者出現的早,所以也叫經典責任鏈 模式。而後者我們可稱作變種責任鏈。
接下來我們看一個經典責任鏈模式在Spring MVC當中的一個例子。
在一個Spring MVC應用當中,會存在很多的請求處理方法,即處理器。當請求到達時應該由哪 個處理器進行處理處理呢?Spring的設計是,將多個處理器映射組個一個列表,然後每次都遍 歷這個列表直到有某個映射器返回處理器爲止。這就是一個經典的責任鏈模式。
在這裏插入圖片描述

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
	if (this.handlerMappings != null) {
		for (HandlerMapping mapping : this.handlerMappings) {
		HandlerExecutionChain handler = mapping.getHandler(request); 
		if (handler != null) {
			return handler; 
			}
		} 
	}
	return null; 
}

總結:

責任鏈由多個節點(處理器)組成。在行爲模式上有兩種:一種是遍歷鏈條上的節點,直到找到 對應節點,然後處理爲止,這種叫 模式。另一種是由各個節點依次處理,共享負擔 責任的一部分,我們稱它他 。兩種模式在結構上也會不一樣,前者基於數組進行遍 歷,後者通過引用組成鏈表。

作業練習:

  1. 我們平常所看到的 (Filter)過濾器、(Interceptor)攔截器、(pipeline)管道這些都屬於責任 鏈模式嗎?如果是請說出你在哪些框架源碼中有⻅過這些場景?它們屬於經典責任鏈,還 是變種責任鏈?
  2. 基於以下場景設計一個經典責任鏈模式。 請假審批流程:1天以內由HR直接審批,2至3天由部⻔主管審批、3天以上由經理進行審批。

以上來源: 魯班大叔

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章