戲說領域驅動設計(十七)——實體實戰

SKevin發表於2022-03-23

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

相關文章