戲說領域驅動設計(十九)——外驗

来源:https://www.cnblogs.com/skevin/archive/2022/03/31/16035785.html
-Advertisement-
Play Games

觀察者模式又叫做發佈-訂閱模式,屬於行為型模式;觀察者模式通過定義一種一對多得依賴關係,讓多個觀察者對象同時監聽某一個主題對象,這個主題對象在狀態上發生變化時,會通知所有觀察者對象,使他們能夠自動更新自己。 觀察者模式的UML類圖如下: 如上圖所示,觀察者模式主要涉及到抽象主題角色、具體主題角色、抽 ...


  內驗是針對領域模型自身的驗證,其驗證規則也是由領域模型自已來完成,只是觸發的時機可能在工廠中也可能在構造函數中。與內驗對應的當然就是外驗了,這是用於對用戶的輸入和業務流程的前提或得更專業一點叫“前置條件”的檢驗。如果細化一點,可以將外驗分成兩個情況:用戶輸入和業務流程的前置條件。情況不同驗證的方式也不一樣,下麵讓我們展開了細聊。對了,額外多說一句,此處的“內驗”和“外驗”是我為了說明問題所起的名稱,其實叫什麼您只要能和團隊成員說明白就行,名字並不是很重要。

一、基於外部輸入的驗證

  對外部的輸入進行驗證其實很簡單,有多種現成的手段可用比如SpringBoot里的類庫“hibernate-validator”,引入後直接使用即可。這種驗證方式僅限於視圖模型或簡單類型,不建議在領域模型中也進行使用,會造成BO與基礎設施的強綁定,看過前面內容的您應該知道,減少對基礎設施的依賴是六邊型架構的典型特征。回到正題,我個人在面對外部輸入的時候,如果是視圖模型,便在模型中直接嵌入驗證代碼;如果是簡單類型,則將驗證的邏輯交付地一個驗證工具進行。這樣做的好處是業務邏輯中的代碼量比較少,看起來乾凈;另外就是由於工具是可以復用的,所以減少的代碼量總的算起來還是不少的,畢竟驗證是一個剛需。熟悉本系列文章的老朋友應該發現我提到了很多次的“代碼乾凈、整潔”,這個並非是可有可無的要求,而是應當在開發過程中隨時要註意的。在滿足需求的同時有效代碼越少系統可維護性越高;涉及到工作交接或增加人手等相關工作,這些在IT團隊中非常常見的情況本來成本是不低的,但如果能在代碼書寫度方面給予重視,成本是可以降下來的。

  基於視圖模型的驗證,通過為所有的模型增加一個支持驗證的基類來實現,具體類可通過對用於驗證的方法進行覆蓋來實現自定義的驗證規則。這種實現簡單明瞭,也不用做太多的額外的工作。雖然說Spring有現成的框架,但我不太喜歡在代碼上加入各種註解,顯得亂。現實中您可以使用Spring框架所提供的能力,而我在這裡寫出來是為了展示驗證實現的思想。下麵的類圖展示了這種設計的類結構。

 

   “Validatable”介面在上一章已經進行了介紹,這裡需要重點說明的是“VOBase”。所有的視圖模型都從它繼承,由於介面“Validatable”的存在使得視圖模型具備了可驗證性。方法“validate()”用於提供具體的驗證邏輯,不過我們在VOBase只是對其做了簡單的實現,畢竟抽象類也沒什麼可進行驗證的。“ApprovalInfo”是一個視圖模型具體類,對“validate()”方法進行了覆蓋並加入了實現邏輯。其實我一度在想,在這裡貼這類簡單的代碼是不是對您的技術水平有一定的侮辱,不過既然都看到這兒了您就索性多看兩眼,畢竟我們想要強調的驗證思想和意識,看看別人怎麼做的再考慮自身如何提高。

public abstract class VOBase implements Validatable {

    @Override
    public ParameterValidationResult validate() {
        return ParameterValidationResult.success();
    }
}

public class ApprovalInfo extends VOBase {
    @ApiModelProperty(value = "審批人ID", required = true)
    private String approverId;    
    @ApiModelProperty(value = "審批建議", required = true)
    private String comment;    

    @Override
    public ParameterValidationResult validate() {
        if (StringUtils.isEmpty(approverId)) {
            return ParameterValidationResult.failed(OperationMessages.INVALID_APPROVER_INFO);
        }
        if (StringUtils.isEmpty(comment)) {
            return ParameterValidationResult.failed(OperationMessages.INVALID_APPROVAL_COMMENT);
        }
        return ParameterValidationResult.success();
    }    
}

  我們再進一步思考一下,其實無論驗證是針對VO還是基本類型的,本質上都是對參數的驗證,那就完全可以將參數的驗證規則抽象成規則對象,原理同內驗是一樣,只是內驗把規則封裝在了領域對象的內部。而且,上面我只是寫了VO對象驗證的邏輯並沒有進行觸發,也的確需要一個調用驗證方法的點。對此,我們設計一個工具類“ParameterValidators”,類圖如下所示。把驗證規則如“VORule”封裝在“ParameterValidators”中,用戶在觸發驗證的時候會迴圈所有內嵌的規則並調用規則本身的驗證邏輯。

 

  又有一個我們熟悉的介面“Validatable”,截止到目前已經用到了三次了,感覺這是我的職業生涯中設計最成功的介面,復用度極高。多說一句,其實很多程式員在使用介面的時候只是為了用而用,實際上並沒有考慮為什麼、要在什麼場景用。這裡有一個小小的提示:介面的作用是“賦能”,您在設計的時候要從對象能力這個角度去考慮,千萬別用岔了,否則容易出現設計過度的情況。回歸正文,通過上面的類圖我們可以知道,有多個具體的類實現了“Validatable”介面,比如“StringNotNullRule ”、“VORule”等,“ParameterValidators”則匯聚了這些規則。代碼片段請看示例。

final public class ParameterValidators {
    /**
     * 驗證
     * @throws IllegalArgumentException 參數異常
     */
    public void validate() throws IllegalArgumentException {
        if (this.parameters.isEmpty()) {
            return;
        }
        for (Validatable parameter : this.parameters) {
            ParameterValidationResult validationResult = parameter.validate();
            if (!validationResult.isSuccess()) {
                throw new IllegalArgumentException(validationResult.getMessage());
            }
        }
    }


    /**
     * 增加待驗證的視圖模型
     * @param vo 視圖模型
     * @param messageIfVoIsNull 當視圖模型為空時的提示信息
     */
    public ParameterValidators addVoRule(VOBase vo, String messageIfVoIsNull) {
        this.parameters.add(new VORule(vo, messageIfVoIsNull));
        return this;
    }


    /**
     * 增加業務模型ID驗證
     * @param targetValue 待驗證參數的值
     * @param errorMessage 錯誤提示
     * @return 參數驗證器
     */
    public ParameterValidators addStringNotNullRule(String targetValue, String errorMessage) {
        this.parameters.add(new StringNotNullRule(targetValue, errorMessage));
        return this;
    }
}

class VORule implements Validatable {
    private VOBase vo;
    private String messageIfVoIsNull;


    @Override
    public ParameterValidationResult validate() {
        if (vo == null) {
            if (StringUtils.isEmpty(messageIfVoIsNull)) {
                return ParameterValidationResult.failed(OperationMessages.INVALID_BUSINESS_INFO);
            }
            return ParameterValidationResult.failed(messageIfVoIsNull);
        }
        ParameterValidationResult validationResult = vo.validate();
        if (!validationResult.isSuccess()) {
            return ParameterValidationResult.failed(validationResult.getMessage());
        }
        return ParameterValidationResult.success();
    }
}

  針對視圖模型的驗證實際上是調用了VO對象的驗證邏輯;針對簡單類型的驗證則是設計了一些驗證規則如“StringNotNullRule”。“ParameterValidators”包含了一些“add*”方法,通過調用這些方法把待驗證的目標加到本對象中,“validate”會迴圈其內部包含的規則並觸發驗證,一旦有不合法的情況出現則直接拋出異常。您也可以通過將異常信息進行匯聚和包裝來統一給出驗證結果。有了這些基礎設施的支撐,我們在業務代碼中進行參數驗證時會節省很多精力,寫出的代碼看起來很乾凈、整潔,如下片段所示。

    public CommandHandlingResult terminate(DeploymentResultVO resultVO, Long approvalFormId, OperatorInfo operatorInfo) {
        try {
            ParameterValidators.build()
                    .addVoRule(resultVO, OperationMessages.INVALID_DEPLOYMENT_RESULT)
                    .addVoRule(operatorInfo, OperationMessages.INVALID_OPERATOR_INFO)
                    .addObjectNotNullRule(approvalFormId, OperationMessages.INVALID_APPROVAL_FROM_INFO)
                    .validate();
            ……                
        } catch (IllegalArgumentException | ApprovalFormOperationException e) {
            logger.error(e.getMessage(), e);
            return new CommandHandlingResult(false, e.getMessage(), null);
        }
    }

二、業務流程前置條件驗證

  業務流程前置條件的驗證相對要比參數驗證複雜得多,比如這樣的需求“用戶下訂單前,需要判斷庫存是否大於0且賬戶不能是凍結狀態”,這裡的兩個約束是下單業務的前置條件。如果您仔細分析一下會發現前置驗證的條件驗證不同於參數和對象的驗證:前者一般需要使用其它服務提供或從資料庫中查詢出的數據作為判斷依據;而後者一般是對自身屬性的判斷,不需要使用外部數據,您還真別小看這種不同,它限制了後續驗證的實現方式,後面我們會詳解。上述作為假想的案例,乍一看感覺實現起來應該非常簡單,在用戶創建訂單對象前把庫存信息和賬戶信息分別查詢出來,並根據需求進行條件的驗證,代碼可能是下麵這樣的。

@Service
public
class OrderService { public void placeOrder(OrderDetail orderDetail, string accountId) { AccountVO account = this.accountService.find(accountId); if (account.getStatus == AccountStatus.FREEZEN) { throw new IllegalOperationException(); } StockVO stock = this.stockService.find(orderDetail.getProductId()); if (stock.getAmount() < 0) { throw new IllegalOperationException(); } Order order = OrderFactory.create(orderDetail); …… } }

  我相信大多數開發都會按上述代碼的方式進行開發。實際上這種方式有點四不像,“Order”使用了面向對象編程而兩個驗證條件是典型的面向過程思維。這裡有三個顯示的問題:1)當前的驗證條件有兩個,如果再加上新的條件呢?比如“下單前,賬戶信用額度要大於0;賬戶餘額要大於0;用戶必須實名認證的;必須是首單用戶等”,我可以一口氣說出幾十種條件,按上述的寫法肯定要包含大量的“if……”,代碼基本就沒法看了;2)這些前置條件其實是一種業務規則,您把業務規則放到應用服務中是不合理的。因為我們一直強調,應用服務中只做業務流程式控制制,不應該包含業務邏輯,面向過程的代碼才會這麼乾;3)這些業務的前置條件沒有復用的可能性。比如“首單用戶”規則,在秒殺訂購場景需要使用;在購買具備優惠活動的產品時也會有需要,所以你不得不在使用的時候把代碼全複製過來。這種代碼上線的時候容易,一旦涉及到規則變更,改起來就是個噩夢,你能說得清楚有多少個地方使用了重覆的代碼嗎?

  問題我們已經列舉了出來,那麼如何解決這些問題?我們可以簡單的根據上面所說的三點問題一一解決掉。針對問題一,可以把前置條件的驗證全提到一個服務中或另一個方法中即可解決;針對問題二,可以把這些業務規則獨立出去作為一個個的領域模型,只是我們需要註意前文中說過的這些規則所用的數據來源於外部系統或資料庫,而領域模型是不能使用這些基礎設施的,所以就需要你在構造的時候把這些信息先從應用服務中提出來;針對問題三,既然能把每個規則封裝成獨立的領域模型,那這些規則就具備了復用性,所以針對問題二的解決方案是一箭雙調的。

  有瞭解決思路我們就需要考慮一下如何設計實現,既然訂單服務中有這麼多的限制條件,我們可以做一個驗證的的框架,這種框架不僅能用於訂單服務的驗證,如果設計得當也可以在其它服務內部復用,畢竟前置條件驗證是一個剛性需求。另外,框架需要提供驗證所需要的信息比如進行資料庫查詢,需要組織驗證規則,所以其實現一定是個應用服務。據此,我們的類圖所下所示。

  這個類圖相對複雜一點,讓我們來解釋一下具體的含義。這裡面有一個似曾相識的老朋友“Validatable”介面,不過這個和前面的不太一樣(其實可以一樣的,只是案例代碼實現有先後,如果您打算使用本文的設計思想,請儘量實現統一),驗證的方法中多了一個參數“ValidationContext”,這是一個抽象類,需要在具體實現的時候包含用於獲取驗證數據的信息。以上面的下單場景為例,當然就是賬戶ID“accountId”和訂單詳情“orderDetail”。所以您需要新建一個繼承自“ValidationContext”的具體類並把賬戶ID作為屬性,用於驗證的應用服務使用賬號ID調用賬戶服務來獲取賬號信息。下麵代碼片段為“Validatable”介面的定義以及驗證應用服務的示例。

 

public interface Validatable {
    /**
     * 驗證方法
     * @param validationContext  驗證上下文
     * @throws ValidationException 驗證異常
     */
    void validate(final ValidationContext validationContext) throws ValidationException;
}

public abstract class ValidationServiceBase implements Validatable {

    private ThreadLocal<Validator> validatorThreadLocal = new ThreadLocal<Validator>();

    /**
     * 驗證服務
     *
     * @param validationContext 驗證信息上下文
     * @throws OrderValidationException 驗證異常
     */
    @Override
    public void validate(final ValidationContext validationContext) throws ValidationException {
        this.validatorThreadLocal.set(new Validator());
        this.buildValidator(this.validatorThreadLocal.get());
        this.validatorThreadLocal.get().validate(validationContext);
    }

    /**
     * 構建驗證器
     * @param validator 驗證器
     */
    protected abstract void buildValidator(Validator validator);
}

  我們前面說過了,用於驗證的服務是一個應用服務,所以我們為這個服務設計了一個基類,也就是上面的“ValidationServiceBase”,方法“validate”用於觸發驗證邏輯;方法“buildValidator”用於在其中加入待驗證的規則,註意:這些規則是領域模型。這裡引入了一個新的對象“Validator”,作為驗證規則的容器裡面包含了“ValidationSpecificationBase”類型對象的列表。在觸發ValidationServiceBase.validate()方法時,會調用Validator.validate(),後者會遍歷Validator中的驗證規則“ValidationSpecificationBase”再調用每個規則的validate()方法。不論是“ValidationServiceBase”、“Validator”還是“ValidationSpecificationBase”,由於實現了“Validatable”介面,所以都會包含方法“validate()”,具體代碼如下所示。

public class Validator implements Validatable {
    //訂單驗證規則列表
    private List<ValidationSpecificationBase> specifications = new ArrayList<ValidationSpecificationBase>();

    /**
     * 驗證方法
     * @param validationContext  驗證上下文
     * @throws ValidationException 驗證異常
     */
    @Override
    public synchronized void validate(final ValidationContext validationContext) throws ValidationException {
        Iterator<ValidationSpecificationBase> iterator  = this.specifications.iterator();
        while (iterator.hasNext()) {
            ValidationSpecificationBase validationSpecification = iterator.next();
            validationSpecification.validate(validationContext);
        }
        clearSpecifications();
    }
}
public abstract class ValidationSpecificationBase implements Validatable {

}

public class AccountBalanceSpec extends ValidationSpecificationBase {
    private Customer customer;
    
    public AccountBalanceSpec(Customer customer) {
        this.customer = customer;
    }
    
    @Override
    protected void validate(ValidationContext validationContext) throws OrderValidationException {        
        if (this.customer.getBalance == 0) {
            throw new OrderValidationException();
        }
    }    
}

  有了上述的基本類型作支撐,我們就可以在業務代碼中加入用於驗證的領域模型和用於驗證的應用服務,案例中的“判斷賬戶餘額”驗證規則可參看上面代碼“AccountBalanceSpec”的實現(再提示一次:這是一個領域模型)。那麼餘下的就是看如何設計用於驗證的應用服務了,代碼如下片段。

@Service
public class OrderValidationService extends ValidationServiceBase {
    /**
     * 構建驗證器
     *
     * @param validator 驗證器
     */
    @Override
    protected void buildValidator(Validator validator) {
        Customer customer = this.constructAccount(validator);
        //賬號狀態驗證
        validator.addSpecification(new AccountBalanceSpec(customer));
     //可加入其它驗證規則 }
private Customer constructAccount(Validator validator) { String accountId = (OrderValidationContext)validator.getContext(); //通過調用遠程服務查詢賬戶信息
    AccountVO = ……
     //構建客戶信息
     Customer customer = ……
     return customer;
} }

  有了驗證服務,我們就可以按如下代碼的方式實現下單場景的驗證。對比一下前面的那種四不像的方式,您覺得這種方式是不是要好得多。

@Service
public class OrderService {
    @Resource
    private OrderValidationService orderValidationService;
    
    public void placeOrder(OrderDetail orderDetail, string accountId) {
        OrderValidationContext context = new OrderValidationContext(orderDetail, accountId);
        this.orderValidationService.validate(context);
        Order order = OrderFactory.create(orderDetail);
        ……
    }    
}

 總結

  本章代碼有點多,如果您一遍沒整明白,可以多看幾次。為了減少代碼的量,我閹割了部分內容,所以如果出現對應不上的情況是正常的。最重要的是您得學會一種面向對象編程的思想和解決問題的思路。我在前面的文章中提過二級驗證,此處的兩級就是指外驗與內驗。另外多提一句,兩能驗證只適用於命令類的方法。查詢直接通過參數驗證即可,不需要這麼複雜的判斷。這裡其實暗含一個思想:在設計命令類方法的時候務必要保持謹慎的態度,做到足夠的驗證是對自己的一種保護。截止到本章結束,我們已經總結了驗證相關的知識。如果您在回顧一下內容就會發現通過這兩種驗證,您在寫業務代碼也就是編寫業務模型中的代碼的時候,根本不用判斷這個欄位是否為空,那個欄位是否數據不對;下沉到比如DAO層也不用再寫驗證相關的代碼,因為DAO的上層是BO,數據是否正確在BO中已經進行了保障。


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

-Advertisement-
Play Games
更多相關文章
  • 最近Jetpack Compose發佈了Beta版本,抽時間瞭解了一下Compose帶來的改變和其中的一些原理。本文不會講解具體API,只是比較隨意的分享自己的一些疑問以及在探尋答案過程中的一些收穫。 ...
  • 基於大家都期望的更美好的未來世界,數字管家必定會為實現更便捷、更具幸福感的生活體驗而不斷努力,期待作為開發者的你加入併為之貢獻一臂之力。 ...
  • 在涉及團購、外賣、快遞、家政、物流、搬家等生活服務類的App、小程式中,填寫收貨地址是用戶高頻使用的功能。這一功能通常採取讓用戶手動填寫的解決方案,例如上下拉動選擇浙江省-->杭州市-->西湖區-->西溪街道,再切換到姓名輸入框輸入姓名-->電話輸入框輸入電話等一系列的操作。從中我們不難發現手動輸入 ...
  • 平時想記錄的東西太多了,關於日常感知,莫明其妙的小想法、隨筆,這些發到公眾號又不太合適。 索性擼起袖子加油乾。埋頭苦幹了一周多 ...
  • 前言 元宇宙正在如火如荼地發展,大有引領未來潮流之勢。對於我們這麼專業的(web 前端)團隊來說,元宇宙是一個大 (wan) 顯 (quan) 身 (bu) 手 (dong) 的領域,因此團隊在這方面投入了很多人力進行預研和總結,請隨本文一起踏入元宇宙的神秘世界。 元宇宙與 3D 元宇宙,或稱為後設 ...
  • 一、冒泡排序 原理:相鄰兩元素之間兩兩比較,比較出大值進行賦值互換,再依次與相鄰的元素比較,層層遞進。#互換元素位置,相互賦值。 時間複雜度:最好O(n),最差O(n^2) 1、比較相鄰的兩個元素,如果前一個比後一個大,則交換位置。2、比較完第一輪的時候,最後一個元素是最大的元素。3、這時候最後一個 ...
  • 橋接模式是什麼 橋接模式:橋接是一種結構型設計模式, 可將業務邏輯或一個大類拆分為不同的層次結構, 從而能獨立地進行開發。 為什麼用橋接模式 對於兩個獨立變化的維度,使用橋接模式再適合不過了. 橋接模式怎麼實現 這裡是將computer和printer分成兩層,用介面的方式把強耦合轉化為弱耦合。這兩 ...
  • 我在之前一段時間做過網路通信的系列文章,但是文章還是偏散,沒有一個整體脈絡,本篇就以知識地圖的形式來進行梳理。 知識地圖是一種知識導航系統,並顯示不同的知識存儲之間重要的動態聯繫。本篇主要就是從更高的視角將之前的文章的結構思路展現出來。文章結構的思路實際上也是達到架構師程度要掌握的網路通信知識學習路 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...