上一節中講了實體的一些概念,作為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系列后就將其屏蔽掉了,沒頭沒尾的不太好。