上一節中講了物體的一些概念,作為DDD中最為復雜的組件,想用好了還需要在實踐中慢慢去摸索,都是摸爬滾打過來的,本章著重演示一些物體相關的代碼,通過建立一個基類和通用方法,能讓您在開發程序中少寫一些重復的代碼同時也減少在使用第三方開源框架時的學習成本,此外,是從0寫代碼,不需要付出太多的精力便可以加深自身對理論的理解,友情提示一下,您在看的同時也需要回憶一下前面文章中所說的各類規則、限制,理論與實踐相互印證才能更高效,其實在業務系統開發程序中很少會直接從零寫物體的,多多少少也得有一些基類供使用,畢竟有很多東西是通用的,建一個物體就重寫一次您不累嗎?本章我會從一些基礎的內容開始展示在不用任何架構的情況下如果實踐DDD,代碼僅供參考,每個人的實作方式都會不一樣,了解思路即可,
一、領域模型基類
領域模型基類是物體和值物件共同的父類,雖然物體和值物件作用不一樣但都屬于領域模型,這個基類無任何屬性,只是起到了占位符的作用,后面有些功能比如“領域模型驗證工具”要求待驗證的目標應該是領域模型,具體代碼如下,
/** * 領域模型基類 */ public abstract class DomainModel extends ValidatableBase { /** * 初始化當前狀態 */ public void initializeForNewCreation() { } }
方法“initializeForNewCreation”用于初始化新建的物件,比如在new物件后進行一些屬性的默認值設定,現實中有些場景可能還需要特殊的初始化方式,一般會放到領域物件工廠中完成,您可能會注意到,領域模型從類“ValidatableBase”繼承,這樣做的目的表示領域模型是可被驗證的,比如領域模型持久化前或從反序列后需要進行物件合法性的驗證,而我又不想在每次對屬性賦值后都判斷值的合法性,好的方式是進行統一的驗證并將不合法的內容統一拋出去,物件內部提供驗證方法我稱之為“內驗”,內驗的目標是物件的屬性或屬性組合,也就是只驗證模型本身是否合法,不驗證外部條件,
有人認為把物件驗證的方法放到領域模型中會造成模型的責任變重,所以會建立專門用于驗證的類或服務,我個人覺得一個物件屬性是否合法是一種業務規則,應由物件自已責任,由其自己驗證可產生較好的內聚性,就和人生病一樣,自己最了解哪里最不爽,此外,我所用的“內驗”并未讓物件自己執行驗證(雖然你也可以進行手動的呼叫)而是在其中設定驗證規則并由專門的驗證服務負責執行驗證邏輯,下面代碼演示了如何在領域模型中嵌入驗證規則,需要注意的是本章重點并不在驗證上面,這方面內容會啟動一個新的章節做專門講解,
/** * 可驗證物件的基類 */ public abstract class ValidatableBase implements Validatable { …… protected void addRule(RuleManager ruleManager){ }
final public ParameterValidationResult validate() {
……
}
…… } public class DeploymentApprover extends ApproverBase {
private PhaseType targetPhase; …… @Override protected void addRule(RuleManager ruleManager) { super.addRule(ruleManager); ruleManager.addRule(new ObjectNotNullRule("targetPhase", this.targetPhase, OperationMessages.INVALID_ROLE_TYPE)); ruleManager.addRule(new NotEqualsRule("targetPhase", this.targetPhase, PhaseType.UNKNOWN, OperationMessages.INVALID_ROLE_TYPE)); } …… }
上述代碼中的“addRule”方法定義于父類“ValidatableBase”中,用于為領域模型增加驗證規則,比如屬性“status”的值不能是“null”和“PhaseType.UNKNOWN”,這種只加規則不驗證的方式實際上有點規約模式(Specification )的味道,算是一個簡化版,
二、物體型別基類
物體型別也算是一種領域模型,所以我們就可以在既有的領域模型的基礎上設計物體型別的基類,所有的業務物體都從這個基類繼承,請參看如下代碼,
public abstract class EntityModel<TID extends Comparable> extends DomainModel implements Versionable { //ID private TID id; //版本資訊,用于控制并發 private int version; //創建日期 private LocalDateTime createdDate = LocalDateTime.now(); //變更日期 private LocalDateTime updatedDate = LocalDateTime.now(); //狀態 private Status status = Status.ACTIVE; protected EntityModel(TID id) { this(id, null, null); } protected EntityModel(TID id, LocalDateTime createdDate, LocalDateTime updatedDate) { this(id, Status.ACTIVE, 0, createdDate, updatedDate); } protected EntityModel(TID id, Status status, LocalDateTime createdDate, LocalDateTime updatedDate) { this(id, status, 0, createdDate, updatedDate); } protected EntityModel(TID id, Status status, int version, LocalDateTime createdDate, LocalDateTime updatedDate) { this.id = id; this.version = version; this.initializeForNewCreation(); if (status != null && status != Status.UNKNOWN) { this.status = status; } if (createdDate != null) { this.createdDate = createdDate; } if (updatedDate != null) { this.updatedDate = updatedDate; } } /** * 當前物件置無效 */ public void disable() throws InvalidOperationException { this.status = Status.INACTIVE; this.updatedDate = LocalDateTime.now(); } @Override protected void addRule(RuleManager ruleManager) { super.addRule(ruleManager); ruleManager.addRule(new ObjectNotNullRule("id", this.id, OperationMessages.INVALID_ID)); } @Override public boolean equals(Object object){ if(object == null){ return false; } if(!(object instanceof EntityModel)){ return false; } if(object == this){ return true; } return this.id.compareTo(((EntityModel)object).getId()) == 0; } @Override public int hashCode(){ return this.id.hashCode(); } /** * 獲取版本資訊, * @return 版本資訊 */ @Override public int getVersion() { return this.version; } }
上面的代碼作為演示用沒有把所有的方法列出來,您需要了解和關注其中一些重要的概念,案例中引入了一個新的介面“Versionable”,這個介面用于為物體模型增加樂觀鎖支撐,通過在類中引入屬性“version”,每次對物體進行變更時此欄位加1,涉及樂觀鎖的概念及使用方式可參看網路上其它文章,在DDD中,使用樂觀鎖可以說是一種最起碼的要求且并不需要付出太多的精力,還是十分推薦的,
第二個重點內容是標識屬性“id”,每一個物體必須有一個標識屬性用于對其生命周期進行跟蹤,案例中使用了泛型表示物體的ID型別,不過仍然要求ID是可以比較的(Comparable),您可以通過重寫方法“equals”及“hashCode”來實作物體間的比較,這兩個方法一般都是成對出現,具體原因可自行參考相關文章,需要注意的是“equals”的實作,兩個物件相等不看屬性只看ID,所以代碼實作的時候只對ID做比較,針對ID的設計其實還有一個方式,就是設計一個專門表示ID的類,將ID的操作如等價判斷直接放在類中,這樣可以讓ID的設計更加優雅也能減少物體物件的責任,請看如下代碼,
public class Identity<TID extends Comparable> extends ValueModel { private TID id; @Override public boolean equals(Object obj) { if(obj == null){ return false; } if(!(obj instanceof Identity)){ return false; } if(obj == this){ return true; } return this.id.compareTo(((Identity)obj).getId()) == 0; } }
public abstract class EntityModel { private Identity<? extends Comparable> id; protected EntityModel(Identity<? extends Comparable> id) { this.id = id; } @Override public boolean equals(Object obj) { if(obj == null || !(obj instanceof EntityModel)){ return false; } return this.getId().equals(((EntityModel) obj).getId()); } }
上述的案例在ID設計方面要比第一個版本漂亮得多,也顯得更加專業,實際在做面向物件編程的時候,將責任細化到各個小一點的物件中是一種非常常見的情況,這也是為什么我在前面說使用OOP的時候成本比較高,單一責任的目的倒是達到了,不過出現一堆稀碎的物件,組裝起來也挺費勁的,
我們再回到物體模型的設計上,您會發現我嚴格遵循了一些原則:1)使用建構式的方式來實作對所有物件屬性的賦值,雖然沒有在賦值的時候對屬性的是否合法進行保障,但由于使用了前面所說的“內驗”的方式對物件進行驗證,也就是物件工廠在創建物體后呼叫其驗證方法“validate()”,也可以保障物體的合法性,實際上,在我寫文本篇文章時進行了代碼的走查,才發現基類“EntityModel”中未對ID的正確性進行驗證又沒有限定物件必須使用工廠創建,而是把驗證放到了持久化前的階段,這樣還是有一定的風險的,那么文章結束后我肯定需要對代碼進行調整的,創建物體的原則您需要格外注意:不論使用工廠還是建構式,一旦業務物件被成功創建就應該是合法的,不需要也不應該再呼叫其它方法進行補償(比如物件創建后手動呼叫某個初始化方法);2)物體中引入了一些通用屬性比如“status”,表示物件是活越的還是已經被廢了,在資料的角度看就是資料是否被邏輯洗掉,一般來說,我們不會對物件做物理洗掉,物體物件只要被創建且進行了持久化,就表示其曾經來到過這個世界上,只是因一些事件他已經不活越了,所以不應該將其直接干掉,
三、業務物體的設計
上面通過代碼展示了物體基類的設計方式,在此基礎上就可以進行業務物體模型的設計,下面展示了作業流業務模型的代碼片段,其設計方式仍然遵循我們談及的規范,如果您是一個有強迫癥的設計師,可能會對“forward”方法比較糾結,通常情況下,跨領域模型的操作應當由“領域服務”來完成,不過我們這里并沒有采用這種模式,因為此段代碼是作業流的基類,往大了說算是作業流框架的一部分,在專案中引入“由領域服務完成跨領域模型的業務操作”是一個很好的規范,值得遵守,
public abstract class WorkFlowInstanceBase extends EntityModel<Long> { public static final long EMPTY_WORK_NODE = -1; private Long templateId;//作業流模板 private Creator creator;//創建人 private String request;//請求資訊 private String title;//標題 private Long currentWorkNodeId = EMPTY_WORK_NODE;//當前處理節點 protected WorkFlowInstanceBase(Long id, String title, DataStatus dataStatus, LocalDateTime createdDate, LocalDateTime updatedDate, Long templateId, Creator creator, String request, Long currentWorkNodeId) { super(id, dataStatus, createdDate, updatedDate); this.title = title; this.templateId = templateId; this.creator = creator; this.request = request; this.currentWorkNodeId = currentWorkNodeId; } /** * 轉向下一個處理節點 * @param comment 備注 * @param template 模板 * @return 處理記錄 */ protected ProcessRecord forward(String comment, WorkFlowTemplateBase template) throws InvalidOperationException { if (StringUtils.isEmpty(comment)) { throw new InvalidOperationException(OperationMessages.INVALID_COMMENT); } return this.forwardCore(comment, template, currentWorkNode); } @Override protected void addRule(RuleManager ruleManager) { super.addRule(ruleManager); ruleManager.addRule(new ObjectNotNullRule("currentWorkNodeId", this.currentWorkNodeId, OperationMessages.INVALID_CURRENT_WORK_NODE)); ruleManager.addRule(new ObjectNotNullRule("templateId", this.templateId, OperationMessages.INVALID_TEMPLATE)); ruleManager.addRule(new ObjectNotNullRule("creator", this.creator, OperationMessages.INVALID_CREATOR_INFO)); ruleManager.addRule(new EmbeddedObjectRule("creator", this.creator)); ruleManager.addRule(new StringNotNullOrEmptyRule("request", this.request, OperationMessages.INVALID_REQUEST)); ruleManager.addRule(new StringNotNullOrEmptyRule("title", this.title, OperationMessages.INVALID_TITLE)); } }
總結
本章中所示的代碼相對簡單明了,沒有那么多的花里胡哨,別看東西少但足夠在真實的專案中使用,類似于事件溯源這種,個覺得真正需要的場景并不是很多,所以也沒有加到基類中來,我見過一些個人開發的框架,把代碼設計的特別復雜,可以說是包羅萬象,但其價值有幾何,估計也是仁者見仁、智者見智罷了,另外呢,個人建議在實踐DDD的時候,從這種簡單的途徑開始即可,自己寫一點東西能幫助您在實戰中多積累一些經驗,類似AXON這種大型框架,您別看他東西多,其實并沒有脫離DDD戰術中所說的那點事情,
下一章我們討論內驗,較早之前我寫過驗證相關的文章,不過在決定開啟DDD系列后就將其屏蔽掉了,沒頭沒尾的不太好,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/448170.html
標籤:領域驅動設計
下一篇:JDK動態代理
