在同事的代碼中學習-責任鏈模式

来源:https://www.cnblogs.com/jtea/archive/2023/08/23/17650353.html
-Advertisement-
Play Games

# 前言 不知道大家有沒有發現,設計模式學習起來其實不容易,並不是說它難,主要是它表達的是思想層面或者說抽象層面的東西,如果你沒有實踐經歷過,感覺就是看了就懂,過了就忘。 所以本人現在也不多花費時間去專門學習設計模式,而是平時在看一些框架源碼時,多留意,多學習別人的設計方法和實現思路,在平時工作中, ...


前言

不知道大家有沒有發現,設計模式學習起來其實不容易,並不是說它難,主要是它表達的是思想層面或者說抽象層面的東西,如果你沒有實踐經歷過,感覺就是看了就懂,過了就忘。
所以本人現在也不多花費時間去專門學習設計模式,而是平時在看一些框架源碼時,多留意,多學習別人的設計方法和實現思路,在平時工作中,遇到比較複雜的場景,不好看的代碼,或者想要更優雅的寫法時,再反過來去翻設計模式,這樣學習起來印象更加深刻,出去面試時,有解決場景也比背書要更容易說服別人。

這不最近在review同學代碼時就發現如下代碼,學習的機會不就來了嗎~~~

我簡單說一下這段代碼的邏輯,非常簡單,就是要處理客戶端上傳的一批數據,處理前要校驗一下,失敗就記錄,退出。
從方法命名大概可以看出要校驗日期、用戶、號碼、備註等等,這些校驗規則可能會隨著業務變化而增減,且它們之前有順序要求,也就是圖中的if不能隨意顛倒順序。

這段代碼的缺點很明顯,首先它不符合“開閉原則”,每次增減校驗都需要來修改主業務流程的代碼,沒有做到動態擴展。
且在主業務流程里看到如此長的if,真的非常影響閱讀體驗,if里方法的代碼也高度重覆,如下:

同時它還會形成“破窗效應”,你說它不好吧,一排if還挺有規則的,以後新加,大概率大家都是繼續加if,那這段代碼就越來越難看了。
接下來我們就看如何用設計模式中的責任鏈模式來優化它。

責任鏈模式

來自百度百科的解釋:責任鏈模式是一種設計模式,在責任鏈模式里,很多對象由每一個對象對其下家的引用而連接起來形成一條鏈。請求在這個鏈上傳遞,直到鏈上的某一個對象決定處理此請求。發出這個請求的客戶端並不知道鏈上的哪一個對象最終處理這個請求,這使得系統可以在不影響客戶端的情況下動態地重新組織和分配責任。

我們轉換成圖如下:

在客戶端請求與真實邏輯處理之前,請求需要經過一條請求處理鏈條,其中每個handler可以對請求進行加工、處理、過濾,這與我們上面的業務場景是完全一樣的。
網上的uml圖都把介面和實現對象定義為XXHandler,但這不是強制,你可以結合實際業務場景來,例如XXInterceotor,XXValidator都可以。

使用責任鏈模式的優點:(來自chatgpt的回答)
降低耦合度:請求發送者不需要知道哪個對象會處理請求,處理器之間也不需要知道彼此的詳細信息,從而降低了系統的耦合度。
動態添加/移除處理器:可以在運行時動態地添加、移除處理器,而不會影響其他部分的代碼。
增強靈活性:可以根據具體情況定製處理器的鏈條,以適應不同的請求處理流程。
遵循開閉原則:當需要新增一種處理方式時,只需創建一個新的具體處理器,並將其添加到鏈條中,而無需修改現有代碼。

案例

本人在看到上面代碼,開始想優化思路的時候,實際並不是馬上想到責任鏈模式,這也是開頭說的,死記硬背並不牢靠。
我先想到的是一些使用過的工具或組件也有類似的場景,如spring中的攔截器,sentinel中的插槽。

spring攔截器

spring攔截器要實現HandlerInterceptor,它的作用是可以在請求處理前後做一些處理邏輯,如下定義了兩個攔截器。

上面只是定義,還是註冊到spring中,如下

@Configuration
public class MyInterceptorConfig implements WebMvcConfigurer {

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(new MyInterceptor1());
		registry.addInterceptor(new MyInterceptor2());
	}
}

接著請求介面就會發現請求依次經過MyInterceptor1,MyInterceptor2,順序就是我們註冊時寫的順序。我們可以猜測spring肯定在請求過程會有一個迴圈,把所有的攔截器都拿出來依次執行,答案就是HandlerExecutionChain這個類中,從名字就可以看出它是一個Handler執行鏈,它內部有一個集合保存了本次請求要經過的攔截器,可以看到我們的攔截器也在集合當中。

比較類似的servlet Filter也是類似原理,有興趣的可以對比一下。

sentinel solt

sentinel是阿裡的一個流量管理中間件,它的架構圖如下:

請求會經過一系列稱為“功能插槽”的對象,這些對象會對請求進行判斷,統計,限流,降級等。
這些對象在sentinel中是實現ProcessorSlot介面的對象,預設為我們提供了8種slot,使用SPI的機制載入,具體配置在sentinel包的META-INF目錄下。

從上面架構圖可以看出,sentinel的solt會組成一個鏈表,在它們的基類AbstractLinkedProcessorSlot中的next屬性就指向下一個節點,這些solt的順序就是配置的順序,也定義在Constants中,可以看到它是以1000為步長,以後想在中間新增一個就比較方便。

    /**
     * Order of default processor slots
     */
    public static final int ORDER_NODE_SELECTOR_SLOT = -10000;
    public static final int ORDER_CLUSTER_BUILDER_SLOT = -9000;
    public static final int ORDER_LOG_SLOT = -8000;
    public static final int ORDER_STATISTIC_SLOT = -7000;
    public static final int ORDER_AUTHORITY_SLOT = -6000;
    public static final int ORDER_SYSTEM_SLOT = -5000;
    public static final int ORDER_FLOW_SLOT = -2000;
    public static final int ORDER_DEGRADE_SLOT = -1000;

代碼改造

從上面的案例可以看出,設計模式的實現並沒有固定的套路,只要設計思想一致就行了,實現方式可以有很多種,spring攔截器使用集合保存,sentinel使用鏈表,適合才是最好的。
有了上面的知識儲備,現在我們可以開始改造代碼了,以下代碼都經過簡寫。

首先定義一個校驗介面,有一個校驗方法,由於原來代碼的參數比較多,所以我們定義一個context來包裝。

public interface MyValidator {

	/**
	 * 校驗
	 *
	 * @param context 上下文
	 * @return 校驗失敗時的錯誤碼,成功返回null
	 */
	FeedbackUploadCode valid(ValidateContext context);
}

接著我們定義一個抽象類作為基類,來實現一些代碼的復用,其中getNext用於指示下一個校驗器,也是我們構建順序的方式。

public abstract class AbstractMyValidator implements MyValidator {

	public abstract AbstractMyValidator getNext();
}

由於校驗比較多,我們就拿前兩個校驗作為兩個例子,其中添加一個頭節點,作為起始節點,後面每一個校驗器只需要實現valid校驗邏輯,和說明它的下一個校驗器是誰即可,最後一個的next就是null。

class HeadValidator extends AbstractMyValidator {

	@Override
	public AbstractMyValidator getNext() {
		return new TaskIdValidator();
	}

	@Override
	public FeedbackUploadCode valid(ValidateContext context) {
		return null;
	}
}

class TaskIdValidator extends AbstractMyValidator {

	@Override
	public AbstractMyValidator getNext() {
		return new DateFormatValidator();
	}

	@Override
	public FeedbackUploadCode valid(ValidateContext context) {
		try {
			Long.parseLong(context.getFileBo().getTaskId());
			return null;
		} catch (NumberFormatException e) {
			return FeedbackUploadCode.ERROR_TASK_ID_FORMAT;
		}
	}
}

class DateFormatValidator extends AbstractMyValidator {

	private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
	private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN);

	@Override
	public AbstractMyValidator getNext() {
		return null;
	}

	@Override
	public FeedbackUploadCode valid(ValidateContext context) {
		try {
			LocalDateTime.parse(context.getFileBo().getCollectionTime(), DATE_TIME_FORMATTER);
			return null;
		} catch (DateTimeParseException e) {
			return FeedbackUploadCode.ERROR_DATE_FORMAT;
		}
	}
}

新增一個Service,對外提供校驗方法,核心就是持有校驗器的頭節點,外部調用只需要組裝好上下文,校驗方法會通過頭節點遍歷所有的校驗器完成校驗。

@Service
public class ValidateService {

	@Autowired
	private FileDbService fileDbService;

	private AbstractMyValidator feedbackValidator = new HeadValidator();

	public Boolean valid(ValidateContext context) {
		AbstractMyValidator currentValidator = feedbackValidator.getNext();
		while (currentValidator != null) {
			if (currentValidator.valid(context) != null) {
				FeedbackFile file = buildOutsourceFeedbackFile(context.getFileBo(), context.getBatchId(), context.getUser(), context.getFileName());
				fileDbService.insert(file);
				return false;
			}
			currentValidator = currentValidator.getNext();
		}
		return true;
	}

	private FeedbackFile buildOutsourceFeedbackFile(FileBo fileBo, long batchId, LoginUser user, String fileName) {
		FeedbackFile file = new FeedbackFile();
		// set value
		return file;
	}
}

由於校驗的規則都比較簡單,我們可以把所有的校驗器都寫到同一個類中,並且代碼順序就是校驗的順序,當然也可以像sentinel一樣維護一個順序值,或者像spring攔截器一樣把它們按照順序添加到集合中。
這樣以後新增一個校驗規則,就只需要新增一個校驗器,並且把它放到鏈表合適的位置即可,真正做到對擴展開放,對修改封閉。

更多分享,歡迎關註我的github:https://github.com/jmilktea/jtea


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • # Java將MySQL建表語句轉換為SQLite的建表語句 **源代碼**: ```java package com.fxsen.platform.core.util; import java.util.HashMap; import java.util.Map; import java.util ...
  • 本文主要講述通過MyBatis、JDBC等做大數據量數據插入的案例和結果。 ## 30萬條數據插入插入資料庫驗證 - 實體類、mapper和配置文件定義 - - User實體 - mapper介面 - mapper.xml文件 - jdbc.properties - sqlMapConfig.xml ...
  • 本文已收錄至GitHub,推薦閱讀 👉 [Java隨想錄](https://github.com/ZhengShuHai/JavaRecord) 微信公眾號:Java隨想錄 > 原創不易,註重版權。轉載請註明原作者和原文鏈接 [TOC] 某天,爪哇星球上,一個普通的房間,正在舉行一場秘密的面試: ...
  • ITGeeker技術奇客發佈的開源Word文字替換小工具更新到v1.0.1.0版本啦,現已支持Office Word文檔頁眉和頁腳的替換。 同時ITGeeker技術奇客修複了v1.0.0.0版本因替換數字引起的in ‘ requires string as left operand, not int ...
  • 本文已收錄至GitHub,推薦閱讀 👉 [Java隨想錄](https://github.com/ZhengShuHai/JavaRecord) 微信公眾號:Java隨想錄 > 原創不易,註重版權。轉載請註明原作者和原文鏈接 [TOC] 前面我們講了可達性分析和根節點枚舉,介紹完了GC的前置工作, ...
  • LibCurl是一個開源的免費的多協議數據傳輸開源庫,該框架具備跨平臺性,開源免費,並提供了包括`HTTP`、`FTP`、`SMTP`、`POP3`等協議的功能,使用`libcurl`可以方便地進行網路數據傳輸操作,如發送`HTTP`請求、下載文件、發送電子郵件等。它被廣泛應用於各種網路應用開發中,... ...
  • 我們在`jupyter notebook`中使用`pandas`顯示`DataFrame`的數據時,由於屏幕大小,或者數據量大小的原因,常常會覺得顯示出來的表格不是特別符合預期。 這時,就需要調整`pandas`顯示`DataFrame`的方式。`pandas`為我們提供了很多調整顯示方式的參數,具 ...
  • 事務管理,一個被說爛的也被看爛的話題,還是八股文中的基礎股之一。​本文會從設計角度,一步步的剖析 Spring 事務管理的設計思路(都會設計事務管理器了,還能玩不轉?) ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...