觀察者模式又叫做發佈-訂閱模式,屬於行為型模式;觀察者模式通過定義一種一對多得依賴關係,讓多個觀察者對象同時監聽某一個主題對象,這個主題對象在狀態上發生變化時,會通知所有觀察者對象,使他們能夠自動更新自己。 觀察者模式的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中已經進行了保障。