天天看點

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

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

一、基于外部輸入的驗證

  對外部的輸入進行驗證其實很簡單,有多種現成的手段可用比如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中已經進行了保障。

繼續閱讀