驗證在我們現實的生活中非常常見,比如您找作業得先整個面試驗證你的能力是否靠譜;找物件得先驗證下對方的顏值和升值空間,有些工程師寫代碼從不驗證,我覺得是有三個原因,一是意識不夠,過于相信前端或外部服務;二是個人缺少主動思考的能力;三是團隊負責人的問題,您都當了領導了為什么不制定一些基本開發規則給團隊樹規矩,實際上,驗證這個事情說簡單也的確不難,不就是個值判斷嗎?可如果想把這個事情做好還真是一個需要值得思考的作業,就和例外的處理一樣,我告訴你就算干了10年的開發都未必知道怎么有效的使用例外,代碼里中充滿了土味,一看就特Low,所以我們把驗證這個事情單獨的提出來,越是越是簡單的東西想寫好才越難,
您應該不知道“物件不變性”這個名字吧?領域模型包括物體與值物件都需要遵循這個規則,就是說不論你對一個領域對像做什么操作,不論怎么盤它,其本質應該保持不變,不都說“江沒易改,本性難變”嗎?上述的操作不僅是呼叫物件上的方法,還包括構造物件的程序,有一個例子說“一個沒有角的獨角獸還能稱得上是獨角獸嗎?”,簡單來說就是你需要始終保持領域物件處于合法的狀態或者說是屬性的值不能超出業務規則限制,比如訂單物件:客戶資訊不能為空、價格資訊不能為負數等、訂單項數量要大于0小于100等,不論你在訂單物件上做什么操作,這些屬性值都不可以超出約束,
想要保證物件的“不變性”,不能依賴于前端的輸入和資料庫本身的約束,那些基本都不靠譜,最好的方式還是首推“驗證”,針對物件本身是否合法的驗證我稱之為“內驗”,相對的,驗證某個業務先決條件的驗證稱之為“外驗”,因為此時的驗證已經超出了物件本身的規則范圍,既然需要對所有的物件都進行驗證,就應該將其做為一種通用的能力放到OOP編程框架中,其實我個人特別不喜歡稱之為“框架”,感覺概念太大了,所以我們就稱呼為基礎類別庫吧,這個類別庫可以提供一些用于檢驗領域物件是否合法的工具隨用隨取,不用重復的造輪子,
領域物件內驗的實作思想很簡單:為每個領域物件中加入用于驗證的方法和驗證規則,在物件創建后或持久化前通過呼叫每個物件的驗證方法實作驗證邏輯,您一定要注意前面這句話中所說的觸發驗證方法的時機,別回頭不管什么場景就呼叫驗證,這叫過度設計,那代碼會讓人吐的,另外,既然是通用的能力而且用于驗證領域物件,就最好將其放到領域模型的基類中按需在具體類中進行重寫,所以就讓我們從這些基類作為起點開搞,
一、驗證服務基類
看過前面的文章您應該已經知道了我們在實作驗證的時候使用了一種類“規約模式”,也就是將驗證規則嵌入到領域物件中,并在合適的時機進行驗證方法的呼叫,為此,在設計領域模型基類的時候我們讓其繼承的了一個用于驗證的父類“ValidatableBase”,這個類里面包含了兩個方法,具體代碼如下所示,“ValidatableBase”是一個抽象類,實作了介面“Validatable”,這個介面很重要,但凡需要驗證的物件都會實作這個介面, 您可以看一下下面的類圖,說得挺繞其實就三個組件,

public interface Validatable { /** * 驗證 * @return 驗證結果 */ ParameterValidationResult validate(); }
public abstract class ValidatableBase implements Validatable { /** * 驗證當前領域模型 * @return 驗證的結果 */ final public ParameterValidationResult validate() { RuleManager ruleManager = new RuleManager(this); this.addRule(ruleManager); return ruleManager.validate(); } /** * 增加驗證規則 * @param ruleManager 驗證規則管理器 */ protected void addRule(RuleManager ruleManager) { } }
public abstract class DomainModel extends ValidatableBase {
protected void addRule(RuleManager ruleManager) { }
}
“ValidatableBase”類中的方法“validate”用于觸發模型的驗證,方法“addRule”用于將驗證規則加入到一個包含了驗證規則串列的物件“RuleManager”中,所以你可根據需要決定是否在具體類中進行方法的重寫,比如上面的“DomainModel”中我就對它進行了覆寫,當觸發驗證的時候,只需要遍歷這個“RuleManager”物件中的每個規約并將驗證結果合并即可實作統一驗證的目的,RuleManager代碼可參看如下片段,
public class RuleManager implements Validatable { //規則擁有者 private DomainModel owner; //規則串列 private List<Rule> rules = new ArrayList<Rule>(); /** * 增加規則 * @param rule 規則物件 */ public void addRule(Rule rule){ if(rule != null){ rules.add(rule); } } public RuleManager(DomainModel owner){ this.owner = owner; } /** * 執行驗證,呼叫規則的驗證方法來執行具體的驗證, * @return 驗證結果 */ public ParameterValidationResult validate(){ CompositeParameterValidateResult result = new CompositeParameterValidateResult(); for(Rule rule : this.rules){ //針對嵌入式物件的驗證 if (rule instanceof EmbeddedObjectRule){ EmbeddedObjectRule embeddedObjectRule = (EmbeddedObjectRule) rule; ParameterValidationResult validationResult = embeddedObjectRule.getTarget().validate(); if(!validationResult.isSuccess()){ result.addValidationResult(new ParameterValidationResult(false, validateHandlingResult.getMessage())); } continue; } ParameterValidationResult ruleVerifyResult = rule.validate(); if(!ruleVerifyResult.isSuccess()){ result.fail(); result.addValidationResult(new ParameterValidationResult(false, errorMessage)); } } return result; } }
這里面其實最有意思也最值得一說的是“EmbeddedObjectRule”這一段,其用于對內嵌物件進行驗證,所謂的內嵌物件是指包含于其它物件內部的領域物件,比如下面代碼片段中的“contact”就是一個嵌套物件,我們驗證領域物件的時候不僅要驗證每個簡單型別的屬性,還需要驗證其中嵌入的其它物件,通過這種方式,就可以實作一層層的驗證,使得每個屬性都能被檢驗到,在上面代碼中另外一個有意思的地方是這段“ParameterValidationResult ruleVerifyResult = rule.validate();”,您會發現真正執行驗證操作的其實是“Rule”物件,這些是我們預定好的一組規則,當然您也可以通過實作“Rule”介面自行加入新的規則,使用預定義規則的方式能加速開發的速度,讓我們拎包即可入住,由“Rule”做驗證其實是OOP中使用較為頻繁的方式,把責任分配的非常明確,十分有利用擴展,
public class Order extends EntityModel<Long> { private String name; private Contact contact; protected Order(Long id, String name, Contact contact) throws OrderCreationException { super(id); this.name = name; this.contact = contact; } @Override protected void addRule(RuleManager ruleManager) { super.addRule(ruleManager); ruleManager.addRule(new EmbeddedObjectRule("contact", this.contact)); } public String getName() { return name; } public Contact getContact() { return contact; } }
驗證規則定義了待驗證目標需要滿足什么樣的規范,由于規則間有一些通用的屬性,所以我們在設計的時候首先會引入一個“RuleBase”基類,所有的規則都會從他繼承,“RuleBase”實作了“Rule”介面,而“Rule”也對前面我們說過的“Validatable”進行了擴展,類圖與代碼如下所示,其實也是三個組件,

public interface Rule extends Validatable { /** * 與操作 * @param rule 目標規則 * @return 與后的規則 */ Rule and(Rule rule); /** * 或操作 * @param rule 目標規則 * @return 或后的規則 */ Rule or(Rule rule); }
public abstract class RuleBase<TTarget extends DomainModel> implements Rule { //驗證的目標 private TTarget target; //驗證目標的名稱 private String nameOfTarget; //當規驗證失敗時的錯誤提示資訊 private String customErrorMessage = GlobalConstants.EMPTY_STRING; /** * 規則基類 * @param nameOfTarget 驗證目標的名稱 * @param target 驗證的目標 */ protected RuleBase(String nameOfTarget, TTarget target){ this(nameOfTarget, target, new String()); } /** * 與操作 * @param rule 目標規則 * @return 與后的規則 */ @Override public Rule and(Rule rule) { return new AndRule(this, (RuleBase)rule); } /** * 或操作 * * @param rule 目標規則 * @return 或后的規則 */ @Override public Rule or(Rule rule) { return new OrRule(this, (RuleBase)rule); } }
“RuleBase”類里除了包含了共用屬性外,還實作了兩個邏輯操作“與”和“或”,也就是說您可以實作規則的組合,比如我們要求:用戶名稱不能為空且長度小于等于30,就可以使用下面代碼表示,這樣寫比較優雅,
new ObjectNotNullRule("name", this.name).and(new LE("name", this.name.length(), 30))
通過上面提到的驗證規則框架,我們就可以開始著手建立一些具體的規則 ,下面展示了“物件不為空”規則的代碼片段,這里面需要特別關注的是方法“validate”,用于執行實際的驗證邏輯,類似“大于”規則,可以通過使用“compareTo”方法實作,
public class ObjectNotNullRule extends RuleBase<DomainModel> { /** * 獲取驗證失敗時預設的錯誤提示資訊 */ @Override protected String getDefaultErrorMessage() { return String.format("%s為空物件", this.getNameOfTarget()); } /** * 物件非空規則 * @param nameOfTarget 驗證目標的名稱 * @param target 驗證的目標 */ public ObjectNotNullRule(String nameOfTarget, DomainModel target) { this(nameOfTarget, target, GlobalConstants.EMPTY_STRING); } /** * 執行驗證 * @return 驗證是否成功 */ @Override public ParameterValidationResult validate() { if(this.getTarget() == null){ return ParameterValidationResult.failed(null); } return ParameterValidationResult.success(); } }
到目前為止我們已經展示了內驗所具備的一切條件,現在我們就可以在領域模型中加入各類驗證規則了,下面的代碼片段以上面的“ObjectNotNullRule”規則為例展示了如何在業務代碼中設定驗證規則,這樣的代碼是不是看起來非常的漂亮?至少不用寫一堆的“if……else”,
public class Order extends EntityModel<Long> { private String name; private Contact contact; @Override protected void addRule(RuleManager ruleManager) { super.addRule(ruleManager); ruleManager.addRule(new EmbeddedObjectRule("contact", this.contact)); ruleManager.addRule(new ObjectNotNullRule("name", this.name)); } }
二、驗證觸發的時機
驗證觸發的時機是需要重點說明和解釋的內容,通過上面的代碼您應該可以看出來每個領域模型無論是物體還是值物件都會包含一個叫作“validate”的公有方法,既然是公有就代表您可以隨意的使用,所以如果不加以限制代碼就會變得特別臟……像我這種有代碼潔癖的人是無論如何不能忍受的,所以我們需要確定觸發驗證的時機,這里給的答案很簡單:物件構造完成時,物件構造包括使用建構式和物件工廠兩種方式,一旦不合法就直接拋出例外,因為不合法的物件是一個畸形兒不能該被創造出來,一般情況下也不允許創造出來后做二次加工使其合法,直白一點就是說你只能使用一行代碼構造物件比如“new BusinessEntity()”或“BusinessEntityFactory.create(),比較建議使用工廠的方式創建物件以避免在建構式中拋例外”,如果成功就回傳目標物件失敗則直接報錯,第十七章中我展示過一個“OrderFactory”的案例,您可以翻看一下,
領域物件的創建其實也只會出現在兩個時機中:新建及反序列化時,針對新建做驗證是因為引數來源于用戶或其它服務的輸入,這些是不可信任的;而反序列化時進行驗證的原因也很簡單,我們在將物件序列化時它其實是合法的,不過一旦存盤到比如資料庫中就不可控了,您知道誰手賤把資料給改了或由于錯誤執行了某些腳本造成資料變質了,您不能或也不應該只依賴于資料庫本身的驗證規則來保障資料的正確性,使用關系型資料庫還好一點,使用如MongoDB這種的,那只能看運氣了,再說了,業務物件的驗證屬于業務代碼要處理的,您把這個責任推給資料庫就不合適了,
被成功創建后的物件,您就可以為所欲為的進行操作了,包括最后的持久化階段也不需要進行二次驗證(如果我在前面的文章中提及到物件在持久化時進行驗證的話,請務必注意這種后驗的方式很不友好,比如訂單中的客戶資訊由于意外被置成了“null”,如果不進行構造時的檢測,您在使用這個資訊的時候就可能拋NPE),這種說法應該沒讓您驚呆了吧?也許您可能認為這種說法非常的荒唐,我給您解釋一下為什么,
首先,我們的前提是物件創建后是合法的,這個在前面已經說過,使用建構式或工廠進行保障;第二,由于有了聚合及聚合根的概念,您不可能繞過聚合根而直接修改其聚合內部的物件,比如用戶物體包含了一個值物件“實名資訊”,我們在修改這個資訊的時候不應該繞過用戶物件而直接對其參考或修改,假如此時的用戶是被凍結的狀態,修改實名資訊是沒有意義的,違反了“客戶凍結”時的業務操作限制;而通過讓客戶物件提供修改的方法,就可以在修改前加一些驗證對操作進行限制,也就是說“只能通過聚合根修改聚合”的原則進一步保障了物件的合法性,當然了,您也可以在修改前先把客戶資訊查詢出來判斷一下狀態再做變更邏輯,但這種方式會造成業務規則不夠內聚,而且這也是典型的面向程序的編程思維,第三點,我假設您在呼叫領域物件的公有方法時已經進行了引數的驗證,如果出現違反業務規則的情況則可直接拋出一個業務例外,比如“凍結的用戶不能修改實名資訊”這個規則,您的代碼可能會按如下方式寫,其實第三條的假設就不應該存在,誰寫公有方法的時候不驗證啊?
public class Account extends EntityModel<Long> { public void changeRealName(string name, string idCard) throws RealNameModificationException { if (this.status == AccountStatus.FREEZEN) { throw new RealNameModificationException(); } …… } }
綜上三條所述,已經覆寫了您使用領域物件時涉及修改的所有場景,每一步都對物件的不變性進行了保障,那創建好的領域物件不就是您手中的小白羊嗎?盤它的時候根本而不用擔心它不服,
總結
物件的內驗是一種驗證物件合法性的手段,條條大路通羅馬,在實踐中其實有多種驗證的方式可采用,您所關注的其實應該是它的思想,還是要多提醒一句,你應該知道在DDD中要以聚合為存盤單元、事務單元,其實應該還需要多加一條:驗證單元,上述所說的驗證是以聚合為單位的而非某一個物體或值物件,在實踐中您需要多去思考物件的合法性,雖然說不太可能一下子都想全了,但要有一個驗證意識,這樣的代碼安全性才高,其實不論是做什么樣的系統,應該對安全抱有敬畏的態度,今天多想一點,明天您就少吃點虧,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/452065.html
標籤:其他
上一篇:戲說領域驅動設計(十八)——內驗
