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

来源: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
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...