規則驗證是業務穩定性的重要保障手段,通過規則驗證,可以驗證和確保系統或業務邏輯的正確性和合規性,避免潛在的錯誤和問題。而規則的遺漏往往會伴隨著線上bug的出現。
相信每個開發人員都曾面對過以下情況:
可見,驗證對流程極為重要,不合理的輸入會導致嚴重的業務問題。同時錯誤數據的影響也比想象中的大得多:
如何避免上述情況的發生,答案就在 防御式編程。
防御式編程(Defensive Programming)是一種軟件開發方法,目的是在代碼中預測可能出現的異常和錯誤情況,并用適當的措施對其進行處理,從而提高軟件的健壯性和穩定性。通過防御式編程,軟件開發人員可以在軟件功能相對復雜的情況下,避免和減少由于程序錯誤所導致的不可預測的行為和不良影響,并保障軟件在部署和運行時的正確性和穩定性,提高軟件可靠性和安全性。
防御式編程的核心思想是在代碼中盡量考慮一切可能出現的異常和錯誤情況,并在代碼中針對這些異常和錯誤情況做出相應的處理。例如,可以使用異常捕獲機制處理可能出現的異常,充分利用代碼注釋和約束條件來規范輸入數據,使用斷言(assert)來檢查代碼中的前置條件和后置條件等。
概念過于繁瑣,簡單理解:防御式編程就是:
對輸入參數保持懷疑,對業務執行的前提條件保存懷疑,對業務執行結果保持懷疑,將極大的提升系統的準確性!
在規則校驗場景,優先使用異常進行流程中斷。
在沒有提供異常的編程語言中,我們只能使用特殊返回值來表示異常,這樣的設計會將正常流程和異常處理流程混在一起,讓語言失去了可讀性。比如在 C 中,通用會使用 -1 或 NULL 來表示異常情況,所以在調用函數第一件事便是判斷 result 是 NULL 或 -1,比如以下代碼:
void readFileAndPrintContent(const char* filename) { FILE* file = fopen(filename, "r"); if (file == NULL) { // 文件無法打開,返回異常狀態 fprintf(stderr, "Failed to open the file./n"); return; // 直接返回,表示發生異常 } char line[256]; while (fgets(line, sizeof(line), file) != NULL) { printf("%s", line); } fclose(file);}
在 Java 語言中,引入了完整的異常機制,以更好的處理異常情況,該機制有如下特點:
在 Java 中異常處理變得簡單且嚴謹:
import java.io.BufferedReader;import java.io.FileReader;import java.io.IOException;public class FileReadExample { public static void readFileAndPrintContent(String filename) throws IOException { try (BufferedReader reader = new BufferedReader(new FileReader(filename))) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } } public static void main(String[] args) { try { readFileAndPrintContent("example.txt"); } catch (IOException e) { System.err.println("Exception occurred: " + e.getMessage()); System.exit(-1); // 返回錯誤代碼 -1 表示發生異常 } }}
在日常業務開發中,當出現不符合業務預期時,直接通過異常對流程進行中斷即可。
當出現不符合預期情況時,是直接拋出異常,還是完成整個階段在拋出異常?這個需要看業務場景!
參數驗證場景,需要對所有不合法信息進行收集,然后統一拋出異常,從而能夠讓用戶一目了然的看到所有問題信息,以方便進行統一修改。
而在業務場景,不符合規則時,需要直接進行異常中斷,避免對后續流程造成破壞。
使用 DDD 進行開發時,一個標準的寫流程包括:
圖片
其中,涉及5大類規則驗證,如:
這是最基礎的校驗,沒有太多的業務概念,只有簡單的參數。其目的是 對數據格式進行驗證。
針對這種通用能力,優先借助框架來完成,常用框架主要有:
對于單屬性的驗證,可以使用 hibernate validation 框架來實現。Hibernate Validation 是一個基于 Java Bean 驗證規范(Java Bean Validation)的驗證框架,它提供了一系列的特性來實現對數據模型的驗證和約束,其特性主要包括:
@NotNull
用于驗證字段不能為空,@Email
用于驗證郵箱格式,@Size
用于驗證字符串長度等;@NotBlank
用于驗證字符串非空且必須包含至少一個非空白字符,@Pattern
用于驗證字符串匹配指定的正則表達式,@Min
和 @Max
用于驗證數字的最小值和最大值等;特性非常多,我們最常用的就是在模型字段、方法參數、返回值增加相應功能的注解,比如在 CreateOrderCmd 中增加相關驗證注解,從而避免手寫代碼:
@Datapublic class CreateOrderCmd { @NotNull private Long userId; @NotNull private Long productId; @NotNull @Min(1) private int count;}
有些參數驗證可能會比較復雜,需要對多個屬性進行判斷,此時 Validation 框架會顯得無能為力。
當然,可以制定相應規范,在參數封裝的類上統一提供一個 validate 方法,并在進入方法后使用參數前調用該方法。但,規范由人執行難免發生遺留。所以,最佳方案是將其內化到框架。如下圖所示:
圖片
image
當需要對多個參數進行校驗時,只需要實現 Verifiable 接口的 validate 方法即可,無需手工對 validate 進行調用。
業務校驗是業務邏輯執行的前置條件驗證,包括外部校驗和控制條件校驗。
通常情況下,業務校驗比較復雜,變化頻次也比較高,所以對擴展性要求很高。但,業務規則本身比較獨立,相互之間沒有太多的依賴關系。為了更好的應對邏輯擴展,可以使用策略模型進行設計。如下圖所示:
圖片
業務驗證器就是策略模型中的策略接口。
核心代碼如下:
public interface BaseValidator<A> extends SmartComponent<A> { void validate(A a, ValidateErrorHandler validateErrorHandler); default void validate(A a){ validate(a, ((name, code, msg) -> { throw new ValidateException(name, code, msg); })); }}
該接口非常簡單:
有了統一的策略接口后,需要使用 Context 模式對入參進行管理。Context 可以是簡單的數據容器,也可以是一個具有 LazyLoad 能力的加強容器,其核心功能就是在多個策略間實現數據的共享。
比如,在生單流程中的 CreateOrderContext 定義如下:
@Datapublic class CreateOrderContext implements CreateOrderContext{ private CreateOrderCmd cmd; @LazyLoadBy("#{@userRepository.getById(cmd.userId)}") private User user; @LazyLoadBy("#{@productRepository.getById(cmd.productId)}") private Product product; @LazyLoadBy("#{@addressRepository.getDefaultAddressByUserId(user.id)}") private Address defAddress; @LazyLoadBy("#{@stockRepository.getByProductId(product.id)}") private Stock stock; @LazyLoadBy("#{@priceService.getByUserAndProduct(user.id, product.id)}") private Price price;}
其中 @LazyLoadBy 是一個功能加強注解,在第一次訪問屬性的 getter 方法時,將自動觸發數據加載,并將加載的數據設置到屬性上,再第二次訪問時,直接從屬性上獲取所需數據。
【注】對該部分感興趣,可以學習 《Command/&Query Object 與 Context 模式》
在有了策略接口 和 共享數據 Context 后,接下來便是按照業務需求實現高內聚低耦合的各種實現類。如下圖所示:
圖片
這些組件如何進行管理,詳見下圖:
圖片
這樣做最大的好處便是,在驗證組件中徹底實現“開閉原則”:
認真思考后,可能會發現:這其實是責任鏈模式的一種變形。但,由于實現非常簡單,在 Spring 框架中多次使用。
狀態校驗又成前置狀態驗證,是業務規則中最重要的一部分。
核心實體通常會有一個狀態屬性,狀態屬性的這些值共同組成一個標準的狀態機。如下圖所示:
圖片
這是一個訂單實體的狀態機,定義了各狀態間的轉換關系,這是領域設計中最為重要的一部分。當發生業務動作時,第一件事不是修改業務狀態,而是判斷當前狀態下是否可以進行該操作。
比如,支付成功的核心業務:
public void paySuccess(PayByIdSuccessCommand paySuccessCommand){ if (getStatus() != OrderStatus.CREATED){ throw new OrderStatusNotMatch(); } this.setStatus(OrderStatus.PAID); PayRecord payRecord = PayRecord.create(paySuccessCommand.getChanel(), paySuccessCommand.getPrice()); this.payRecords.add(payRecord); OrderPaySuccessEvent event = new OrderPaySuccessEvent(this); this.events.add(event);}
在進入邏輯處理前,先對狀態進行判斷,只有 “已創建” 才能接收 支付成功操作,并將狀態轉換為 “已支付”。
固定規則校驗使用場景不多,但其威力巨大,可以從根源上解決邏輯錯誤。
在訂單實體上存在大量的金額操作,比如:
訂單金額發生變化后,更新字段很多,但無論如何變化都需要滿足一個公式:支付金額 = 售賣金額總和 - 優惠金額總和。
我們可以基于這個公式,在業務操作之后、數據庫更新之前對規則進行校驗,一旦規則不滿足則說明處理邏輯出現問題,直接拋出異常中斷處理流程。
JPA 支持在數據保存或更新前對業務方法進行回調。
我們可以使用 回調注解 或 實體監聽器 完成業務回調。
@PreUpdate@PrePersistpublic void checkBizRule(){ // 進行業務校驗}
checkBizRule 方法上增加 @PreUpdate 和 @PrePersist,在保存數據庫或更新數據庫之前,框架自動對 chekBizRule 方法進行回調,當方法拋出異常,處理流程被強制中斷。
也可以使用 實體監聽器 進行處理,如下例所示:
// 首先,定義一個 OrderListenrerpublic class OrderListener { @PrePersist public void preCreate(Order order) { order.checkBiz(); } @PostPersist public void postCreate(Order order) { order.checkBiz(); }}// 在 Order 實體上添加相關配置@Data@Entity@Table@Setter(AccessLevel.PRIVATE)// 配置 OrderListener@EntityListeners(OrderListener.class)public class Order implements AggRoot<Long> { // 省略部分非關鍵代碼 public void checkBizRule(){ // 進行業務校驗 }}
MyBatis 對實體的生命周期支持并沒有 JPA 中那么強大,但通過 Intercepter 仍舊能實現該功能。具體操作如下:
首先,自定義 Intercepter,判斷參數并調用規則校驗方法:
@Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})public class EntityInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object[] args = invocation.getArgs(); MappedStatement statement = (MappedStatement) args[0]; Object parameter = args[1]; // 在這里可以對參數進行判斷,并執行相應的操作 if (parameter instanceof Order) { Order order = (Order) parameter; order.checkBizRule(); } return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // 可以在這里設置一些配置參數 }}
然后,在 mybatis-config.xml 配置文件中增加 Intercepter 的配置,具體如下:
<configuration> <!-- 其他配置 --> <plugins> <plugin interceptor="com.example.EntityInterceptor"/> </plugins></configuration>
Lego 框架對標準 Command 處理流程進行封裝,流程中對固定規則校驗進行了支持。如下圖所示:
圖片
image
在標準寫流程中的固定規則校驗階段會自動調用 ValidateService 中的 validateRule,整體結構和 業務校驗基本一致,在這里就不在贅述。其中:
存儲引擎提供了非常豐富的數據校驗,比如 Not Null,Length、Unique 等;
一般情況下,在流程達到存儲引擎前,所有的驗證規則必須全部通過,盡量不要使用存儲引擎作為兜底方案。但有一種情況極為特殊,也就只有存儲引擎能夠優雅的完成,那就是唯一鍵保護。
比如,在需要冪等保護時,我們通常將冪等鍵設置為唯一索引,從而保證不會出現重復提交的情況。
為了保證臟數據(不符合業務預期的數據)不會進入到系統,我們將“防御式編程”思想用到了極致,在一個標準的寫流程中共設立了5項關卡,從多維度多視角對數據進行保障:
5大關卡共同發力才能真正保障業務數據的有效性。
本文鏈接:http://www.www897cc.com/showinfo-26-12314-0.htmlDDD架構下的防御式編程:五大關卡共同保障業務數據的有效性
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com