責任鏈實戰的高級用法:多級校驗、工作流,這樣寫代碼才足夠優雅!
責任鏈模式,簡而言之,就是將多個操作組裝成一條鏈路進行處理。請求在鏈路上傳遞,鏈路上的每一個節點就是一個處理器,每個處理器都可以對請求進行處理,或者傳遞給鏈路上的下一個處理器處理。
應用場景
責任鏈模式的應用場景,在實際工作中,通常有如下兩種應用場景。
- 操作需要經過一系列的校驗,通過校驗后才執行某些操作。
- 工作流。企業中通常會制定很多工作流程,一級一級的去處理任務。
下面通過兩個案例來學習一下責任鏈模式。
案例一:創建商品多級校驗場景
以創建商品為例,假設商品創建邏輯分為以下三步完成:①創建商品、②校驗商品參數、③保存商品。
第②步校驗商品又分為多種情況的校驗,必填字段校驗、規格校驗、價格校驗、庫存校驗等等。這些檢驗邏輯像一個流水線,要想創建出一個商品,必須通過這些校驗。如下流程圖所示:
圖片
偽代碼如下:
創建商品步驟,需要經過一系列的參數校驗,如果參數校驗失敗,直接返回失敗的結果;通過所有的參數校驗后,最終保存商品信息。
圖片
如上代碼看起來似乎沒什么問題,它非常工整,而且代碼邏輯很清晰。
PS:我沒有把所有的校驗代碼都羅列在一個方法里,那樣更能產生對比性,但我覺得抽象并分離單一職責的函數應該是每個程序員最基本的規范!
但是隨著業務需求不斷地疊加,相關的校驗邏輯也越來越多,新的功能使代碼越來越臃腫,可維護性較差。更糟糕的是,這些校驗組件不可復用,當你有其他需求也需要用到一些校驗時,你又變成了Ctrl+C , Ctrl+V程序員,系統的維護成本也越來越高。如下圖所示:
圖片
偽代碼同上,這里就不贅述了。
終于有一天,你忍無可忍了,決定重構這段代碼。
使用責任鏈模式優化:創建商品的每個校驗步驟都可以作為一個單獨的處理器,抽離為一個單獨的類,便于復用。這些處理器形成一條鏈式調用,請求在處理器鏈上傳遞,如果校驗條件不通過,則處理器不再向下傳遞請求,直接返回錯誤信息;若所有的處理器都通過檢驗,則執行保存商品步驟。
圖片
案例一實戰:責任鏈模式實現創建商品校驗
UML圖:一覽眾山小
圖片
AbstractCheckHandler表示處理器抽象類,負責抽象處理器行為。其有3個子類,分別是:
- NullValueCheckHandler:空值校驗處理器
- PriceCheckHandler:價格校驗處理
- StockCheckHandler:庫存校驗處理器
AbstractCheckHandler 抽象類中, handle()定義了處理器的抽象方法,其子類需要重寫handle()方法以實現特殊的處理器校驗邏輯;
protected ProductCheckHandlerConfig config 是處理器的動態配置類,使用protected聲明,每個子類處理器都持有該對象。該對象用于聲明當前處理器、以及當前處理器的下一個處理器nextHandler,另外也可以配置一些特殊屬性,比如說接口降級配置、超時時間配置等。
AbstractCheckHandler nextHandler 是當前處理器持有的下一個處理器的引用,當前處理器執行完畢時,便調用nextHandler執行下一處理器的handle()校驗方法;
protected Result next() 是抽象類中定義的,執行下一個處理器的方法,使用protected聲明,每個子類處理器都持有該對象。當子類處理器執行完畢(通過)時,調用父類的方法執行下一個處理器nextHandler。
HandlerClient 是執行處理器鏈路的客戶端,HandlerClient.executeChain()方法負責發起整個鏈路調用,并接收處理器鏈路的返回值。
擼起袖子開始擼代碼吧 !
商品參數對象:保存商品的入參
ProductVO是創建商品的參數對象,包含商品的基礎信息。并且其作為責任鏈模式中多個處理器的入參,多個處理器都以ProductVO為入參進行特定的邏輯處理。實際業務中,商品對象特別復雜。咱們化繁為簡,簡化商品參數如下:
/**
* 商品對象
*/
@Data
@Builder
publicclass ProductVO {
/**
* 商品SKU,唯一
*/
private Long skuId;
/**
* 商品名稱
*/
private String skuName;
/**
* 商品圖片路徑
*/
private String Path;
/**
* 價格
*/
private BigDecimal price;
/**
* 庫存
*/
private Integer stock;
}抽象類處理器:抽象行為,子類共有屬性、方法
AbstractCheckHandler:處理器抽象類,并使用@Component注解注冊為由Spring管理的Bean對象,這樣做的好處是,我們可以輕松的使用Spring來管理這些處理器Bean。
/**
* 抽象類處理器
*/
@Component
publicabstractclass AbstractCheckHandler {
/**
* 當前處理器持有下一個處理器的引用
*/
@Getter
@Setter
protected AbstractCheckHandler nextHandler;
/**
* 處理器配置
*/
@Setter
@Getter
protected ProductCheckHandlerConfig config;
/**
* 處理器執行方法
* @param param
* @return
*/
public abstract Result handle(ProductVO param);
/**
* 鏈路傳遞
* @param param
* @return
*/
protected Result next(ProductVO param) {
//下一個鏈路沒有處理器了,直接返回
if (Objects.isNull(nextHandler)) {
return Result.success();
}
//執行下一個處理器
return nextHandler.handle(param);
}
}在AbstractCheckHandler抽象類處理器中,使用protected聲明子類可見的屬性和方法。使用 @Component注解,聲明其為Spring的Bean對象,這樣做的好處是可以利用Spring輕松管理所有的子類,下面會看到如何使用。抽象類的屬性和方法說明如下:
- public abstract Result handle():表示抽象的校驗方法,每個處理器都應該繼承AbstractCheckHandler抽象類處理器,并重寫其handle方法,各個處理器從而實現特殊的校驗邏輯,實際上就是多態的思想。
- protected ProductCheckHandlerConfig config:表示每個處理器的動態配置類,可以通過“配置中心”動態修改該配置,實現處理器的“動態編排”和“順序控制”。配置類中可以配置處理器的名稱、下一個處理器、以及處理器是否降級等屬性。
- protected AbstractCheckHandler nextHandler:表示當前處理器持有下一個處理器的引用,如果當前處理器handle()校驗方法執行完畢,則執行下一個處理器nextHandler的handle()校驗方法執行校驗邏輯。
- protected Result next(ProductVO param):此方法用于處理器鏈路傳遞,子類處理器執行完畢后,調用父類的next()方法執行在config 配置的鏈路上的下一個處理器,如果所有處理器都執行完畢了,就返回結果了。
ProductCheckHandlerConfig配置類 :
/**
* 處理器配置類
*/
@AllArgsConstructor
@Data
public class ProductCheckHandlerConfig {
/**
* 處理器Bean名稱
*/
private String handler;
/**
* 下一個處理器
*/
private ProductCheckHandlerConfig next;
/**
* 是否降級
*/
private Boolean down = Boolean.FALSE;
}子類處理器:處理特有的校驗邏輯
AbstractCheckHandler抽象類處理器有3個子類分別是:
- NullValueCheckHandler:空值校驗處理器
- PriceCheckHandler:價格校驗處理
- StockCheckHandler:庫存校驗處理器
各個處理器繼承AbstractCheckHandler抽象類處理器,并重寫其handle()處理方法以實現特有的校驗邏輯。
NullValueCheckHandler:空值校驗處理器。針對性校驗創建商品中必填的參數。如果校驗未通過,則返回錯誤碼ErrorCode,責任鏈在此截斷(停止),創建商品返回被校驗住的錯誤信息。注意代碼中的降級配置!
super.getConfig().getDown()是獲取AbstractCheckHandler處理器對象中保存的配置信息,如果處理器配置了降級,則跳過該處理器,調用super.next()執行下一個處理器邏輯。
同樣,使用@Component注冊為由Spring管理的Bean對象,
/**
* 空值校驗處理器
*/
@Component
publicclass NullValueCheckHandler extends AbstractCheckHandler{
@Override
public Result handle(ProductVO param) {
System.out.println("空值校驗 Handler 開始...");
//降級:如果配置了降級,則跳過此處理器,執行下一個處理器
if (super.getConfig().getDown()) {
System.out.println("空值校驗 Handler 已降級,跳過空值校驗 Handler...");
returnsuper.next(param);
}
//參數必填校驗
if (Objects.isNull(param)) {
return Result.failure(ErrorCode.PARAM_NULL_ERROR);
}
//SkuId商品主鍵參數必填校驗
if (Objects.isNull(param.getSkuId())) {
return Result.failure(ErrorCode.PARAM_SKU_NULL_ERROR);
}
//Price價格參數必填校驗
if (Objects.isNull(param.getPrice())) {
return Result.failure(ErrorCode.PARAM_PRICE_NULL_ERROR);
}
//Stock庫存參數必填校驗
if (Objects.isNull(param.getStock())) {
return Result.failure(ErrorCode.PARAM_STOCK_NULL_ERROR);
}
System.out.println("空值校驗 Handler 通過...");
//執行下一個處理器
returnsuper.next(param);
}
}PriceCheckHandler:價格校驗處理。針對創建商品的價格參數進行校驗。這里只是做了簡單的判斷價格>0的校驗,實際業務中比較復雜,比如“價格門”這些防范措施等。
/**
* 價格校驗處理器
*/
@Component
publicclass PriceCheckHandler extends AbstractCheckHandler{
@Override
public Result handle(ProductVO param) {
System.out.println("價格校驗 Handler 開始...");
//非法價格校驗
boolean illegalPrice = param.getPrice().compareTo(BigDecimal.ZERO) <= 0;
if (illegalPrice) {
return Result.failure(ErrorCode.PARAM_PRICE_ILLEGAL_ERROR);
}
//其他校驗邏輯...
System.out.println("價格校驗 Handler 通過...");
//執行下一個處理器
returnsuper.next(param);
}
}StockCheckHandler:庫存校驗處理器。針對創建商品的庫存參數進行校驗。
/**
* 庫存校驗處理器
*/
@Component
publicclass StockCheckHandler extends AbstractCheckHandler{
@Override
public Result handle(ProductVO param) {
System.out.println("庫存校驗 Handler 開始...");
//非法庫存校驗
boolean illegalStock = param.getStock() < 0;
if (illegalStock) {
return Result.failure(ErrorCode.PARAM_STOCK_ILLEGAL_ERROR);
}
//其他校驗邏輯..
System.out.println("庫存校驗 Handler 通過...");
//執行下一個處理器
returnsuper.next(param);
}
}客戶端:執行處理器鏈路
HandlerClient客戶端類負責發起整個處理器鏈路的執行,通過executeChain()方法。如果處理器鏈路返回錯誤信息,即校驗未通過,則整個鏈路截斷(停止),返回相應的錯誤信息。
public class HandlerClient {
public static Result executeChain(AbstractCheckHandler handler, ProductVO param) {
//執行處理器
Result handlerResult = handler.handle(param);
if (!handlerResult.isSuccess()) {
System.out.println("HandlerClient 責任鏈執行失敗返回:" + handlerResult.toString());
return handlerResult;
}
return Result.success();
}
}以上,責任鏈模式相關的類已經創建好了。接下來就可以創建商品了。
創建商品:抽象步驟,化繁為簡
createProduct()創建商品方法抽象為2個步驟:①參數校驗、②創建商品。參數校驗使用責任鏈模式進行校驗,包含:空值校驗、價格校驗、庫存校驗等等,只有鏈上的所有處理器均校驗通過,才調用saveProduct()創建商品方法;否則返回校驗錯誤信息。
在createProduct()創建商品方法中,通過責任鏈模式,我們將校驗邏輯進行解耦。createProduct()創建商品方法中不需要關注都要經過哪些校驗處理器,以及校驗處理器的細節。
/**
* 創建商品
* @return
*/
@Test
public Result createProduct(ProductVO param) {
//參數校驗,使用責任鏈模式
Result paramCheckResult = this.paramCheck(param);
if (!paramCheckResult.isSuccess()) {
return paramCheckResult;
}
//創建商品
return this.saveProduct(param);
}參數校驗:責任鏈模式
參數校驗paramCheck()方法使用責任鏈模式進行參數校驗,方法內沒有聲明具體都有哪些校驗,具體有哪些參數校驗邏輯是通過多個處理器鏈傳遞的。如下:
/**
* 參數校驗:責任鏈模式
* @param param
* @return
*/
private Result paramCheck(ProductVO param) {
//獲取處理器配置:通常配置使用統一配置中心存儲,支持動態變更
ProductCheckHandlerConfig handlerConfig = this.getHandlerConfigFile();
//獲取處理器
AbstractCheckHandler handler = this.getHandler(handlerConfig);
//責任鏈:執行處理器鏈路
Result executeChainResult = HandlerClient.executeChain(handler, param);
if (!executeChainResult.isSuccess()) {
System.out.println("創建商品 失敗...");
return executeChainResult;
}
//處理器鏈路全部成功
return Result.success();
}paramCheck()方法步驟說明如下:
?? 步驟1:獲取處理器配置。
通過getHandlerConfigFile()方法獲取處理器配置類對象,配置類保存了鏈上各個處理器的上下級節點配置,支持流程編排、動態擴展。通常配置是通過Ducc(京東自研的配置中心)、Nacos(阿里開源的配置中心)等配置中心存儲的,支持動態變更、實時生效。
基于此,我們便可以實現校驗處理器的編排、以及動態擴展了。我這里沒有使用配置中心存儲處理器鏈路的配置,而是使用JSON串的形式去模擬配置,大家感興趣的可以自行實現。
/**
* 獲取處理器配置:通常配置使用統一配置中心存儲,支持動態變更
* @return
*/
private ProductCheckHandlerConfig getHandlerConfigFile() {
//配置中心存儲的配置
String configJson = "{\"handler\":\"nullValueCheckHandler\",\"down\":true,\"next\":{\"handler\":\"priceCheckHandler\",\"next\":{\"handler\":\"stockCheckHandler\",\"next\":null}}}";
//轉成Config對象
ProductCheckHandlerConfig handlerConfig = JSON.parseObject(configJson, ProductCheckHandlerConfig.class);
return handlerConfig;
}ConfigJson存儲的處理器鏈路配置JSON串,在代碼中可能不便于觀看,我們可以使用json.cn等格式化看一下,如下,配置的整個調用鏈路規則特別清晰。
圖片
getHandlerConfigFile()類獲到配置類的結構如下,可以看到,就是把在配置中心儲存的配置規則,轉換成配置類ProductCheckHandlerConfig對象,用于程序處理。
注意,此時配置類中存儲的僅僅是處理器Spring Bean的name而已,并非實際處理器對象。
圖片
接下來,通過配置類獲取實際要執行的處理器。
?? 步驟2:根據配置獲取處理器。
上面步驟1通過getHandlerConfigFile()方法獲取到處理器鏈路配置規則后,再調用getHandler()獲取處理器。
getHandler()參數是如上ConfigJson配置的規則,即步驟1轉換成的ProductCheckHandlerConfig對象;根據ProductCheckHandlerConfig配置規則轉換成處理器鏈路對象。代碼如下:
* 使用Spring注入:所有繼承了AbstractCheckHandler抽象類的Spring Bean都會注入進來。Map的Key對應Bean的name,Value是name對應相應的Bean
*/
@Resource
private Map<String, AbstractCheckHandler> handlerMap;
/**
* 獲取處理器
* @param config
* @return
*/
private AbstractCheckHandler getHandler (ProductCheckHandlerConfig config) {
//配置檢查:沒有配置處理器鏈路,則不執行校驗邏輯
if (Objects.isNull(config)) {
returnnull;
}
//配置錯誤
String handler = config.getHandler();
if (StringUtils.isBlank(handler)) {
returnnull;
}
//配置了不存在的處理器
AbstractCheckHandler abstractCheckHandler = handlerMap.get(config.getHandler());
if (Objects.isNull(abstractCheckHandler)) {
returnnull;
}
//處理器設置配置Config
abstractCheckHandler.setConfig(config);
//遞歸設置鏈路處理器
abstractCheckHandler.setNextHandler(this.getHandler(config.getNext()));
return abstractCheckHandler;
}?? ?? 步驟2-1:配置檢查。
代碼14~27行,進行了配置的一些檢查操作。如果配置錯誤,則獲取不到對應的處理器。代碼23行handlerMap.get(config.getHandler())是從所有處理器映射Map中獲取到對應的處理器Spring Bean。
注意第5行代碼,handlerMap存儲了所有的處理器映射,是通過Spring @Resource注解注入進來的。注入的規則是:所有繼承了AbstractCheckHandler抽象類(它是Spring管理的Bean)的子類(子類也是Spring管理的Bean)都會注入進來。
注入進來的handlerMap中 Map的Key對應Bean的name,Value是name對應的Bean實例,也就是實際的處理器,這里指空值校驗處理器、價格校驗處理器、庫存校驗處理器。如下:
圖片
這樣根據配置ConfigJson(?? 步驟1:獲取處理器配置)中handler:"priceCheckHandler"的配置,使用handlerMap.get(config.getHandler())便可以獲取到對應的處理器Spring Bean對象了。
?? ?? 步驟2-2:保存處理器規則。
代碼29行,將配置規則保存到對應的處理器中abstractCheckHandler.setConfig(config),子類處理器就持有了配置的規則。
?? ?? 步驟2-3:遞歸設置處理器鏈路。
代碼32行,遞歸設置鏈路上的處理器。
//遞歸設置鏈路處理器 abstractCheckHandler.setNextHandler(this.getHandler(config.getNext()));這一步可能不太好理解,結合ConfigJson配置的規則來看,似乎就很很容易理解了。
圖片
由上而下,NullValueCheckHandler 空值校驗處理器通過setNextHandler()方法設置自己持有的下一節點的處理器,也就是價格處理器PriceCheckHandler。
接著,PriceCheckHandler價格處理器,同樣需要經過步驟2-1配置檢查、步驟2-2保存配置規則,并且最重要的是,它也需要設置下一節點的處理器StockCheckHandler庫存校驗處理器。
StockCheckHandler庫存校驗處理器也一樣,同樣需要經過步驟2-1配置檢查、步驟2-2保存配置規則,但請注意StockCheckHandler的配置,它的next規則配置了null,這表示它下面沒有任何處理器要執行了,它就是整個鏈路上的最后一個處理節點。
通過遞歸調用getHandler()獲取處理器方法,就將整個處理器鏈路對象串聯起來了。如下:
圖片
友情提示:遞歸雖香,但使用遞歸一定要注意截斷遞歸的條件處理,否則可能造成死循環哦!
實際上,getHandler()獲取處理器對象的代碼就是把在配置中心配置的規則ConfigJson,轉換成配置類ProductCheckHandlerConfig對象,再根據配置類對象,轉換成實際的處理器對象,這個處理器對象持有整個鏈路的調用順序。
?? 步驟3:客戶端執行調用鏈路。
public class HandlerClient {
public static Result executeChain(AbstractCheckHandler handler, ProductVO param) {
//執行處理器
Result handlerResult = handler.handle(param);
if (!handlerResult.isSuccess()) {
System.out.println("HandlerClient 責任鏈執行失敗返回:" + handlerResult.toString());
return handlerResult;
}
return Result.success();
}
}getHandler()獲取完處理器后,整個調用鏈路的執行順序也就確定了,此時,客戶端該干活了!
HandlerClient.executeChain(handler, param)方法是HandlerClient客戶端類執行處理器整個調用鏈路的,并接收處理器鏈路的返回值。
executeChain()通過AbstractCheckHandler.handle()觸發整個鏈路處理器順序執行,如果某個處理器校驗沒有通過!handlerResult.isSuccess(),則返回錯誤信息;所有處理器都校驗通過,則返回正確信息Result.success()。
總結:串聯方法調用流程
基于以上,再通過流程圖來回顧一下整個調用流程。
圖片
測試:代碼執行結果
場景1:創建商品參數中有空值(如下skuId參數為null),鏈路被空值處理器截斷,返回錯誤信息
//創建商品參數
ProductVO param = ProductVO.builder()
.skuId(null).skuName("華為手機").Path("http://...")
.price(new BigDecimal(1))
.stock(1)
.build();測試結果
圖片
場景2:創建商品價格參數異常(如下price參數),被價格處理器截斷,返回錯誤信息
ProductVO param = ProductVO.builder()
.skuId(1L).skuName("華為手機").Path("http://...")
.price(new BigDecimal(-999))
.stock(1)
.build();測試結果
圖片
場景 3:創建商品庫存參數異常(如下stock參數),被庫存處理器截斷,返回錯誤信息。
//創建商品參數,模擬用戶傳入
ProductVO param = ProductVO.builder()
.skuId(1L).skuName("華為手機").Path("http://...")
.price(new BigDecimal(1))
.stock(-999)
.build();測試結果
圖片
場景4:創建商品所有處理器校驗通過,保存商品。
//創建商品參數,模擬用戶傳入
ProductVO param = ProductVO.builder()
.skuId(1L).skuName("華為手機").Path("http://...")
.price(new BigDecimal(999))
.stock(1).build();測試結果

案例二:工作流,費用報銷審核流程
同事小賈最近剛出差回來,她迫不及待的就提交了費用報銷的流程。根據金額不同,分為以下幾種審核流程。報銷金額低于1000元,三級部門管理者審批即可,1000到5000元除了三級部門管理者審批,還需要二級部門管理者審批,而5000到10000元還需要一級部門管理者審批。即有以下幾種情況:
- 小賈需報銷500元,三級部門管理者審批即可。
- 小賈需報銷2500元,三級部門管理者審批通過后,還需要二級部門管理者審批,二級部門管理者審批通過后,才完成報銷審批流程。
- 小賈需報銷7500元,三級管理者審批通過后,并且二級管理者審批通過后,流程流轉到一級部門管理者進行審批,一級管理者審批通過后,即完成了報銷流程。
UML圖
AbstractFlowHandler作為處理器抽象類,抽象了approve()審核方法,一級、二級、三級部門管理者處理器繼承了抽象類,并重寫其approve()審核方法,從而實現特有的審核邏輯。
圖片
配置類如下所示,每層的處理器都要配置審核人、價格審核規則(審核的最大、最小金額)、下一級處理人。配置規則是可以動態變更的,如果三級部門管理者可以審核的金額增加到2000元,修改一下配置即可動態生效。
圖片
代碼實現與案例一相似,感興趣的自己動動小手吧~
責任鏈的優缺點
圖片































