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

来源:https://www.cnblogs.com/skevin/archive/2022/03/23/16018010.html
-Advertisement-
Play Games

外觀模式又叫門面模式,屬於結構型模式;是一種通過為多個複雜的子系統提供一個一致的介面,而使這些子系統更加容易被訪問的模式。該模式對外有一個統一介面,外部應用程式不用關心內部子系統的具體細節,這樣會大大降低應用程式的複雜度,提高了程式的可維護性。 現在微服務和模塊化越來越流行,我們都會把一個複雜的系統 ...


  上一节中讲了实体的一些概念,作为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系列后就将其屏蔽掉了,没头没尾的不太好。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、Nginx介紹 1.nginx是一個高性能HTTP伺服器,反向代理伺服器,郵件代理伺服器,TCP/UDP反向代理伺服器. 2.nginx處理請求是非同步非阻塞的,在高併發下nginx 能保持低資源低消耗高性能,主要用在集群系統中用於支持負載均衡. 3.nginx對靜態文件的處理速度也相當快,也可以 ...
  • 本文嘗試通過解釋 api 介面底層做了什麼來闡釋 linux 文件系統在設計層面的一些考慮,配合通俗易懂的日常命令和簡單程式來進行驗證,踐行“紙上得來終覺淺,絕知此事要躬行”的理念,目的是做一個 linux 文件系統的引入… ...
  • Centos7下載及安裝 1.下載虛擬機 虛擬機下載地址: https://www.vmware.com 或者 360一鍵安裝(推薦) 2.在虛擬機上安裝Centos7 2.1.通過鏡像進行安裝 這裡是阿裡雲Centos7的鏡像http://mirrors.aliyun.com/centos/7/i ...
  • 開發者通過華為分析服務下載所需的事件數據,這些數據可以導入到開發者自有的分析系統中,用於構建自定義報告或生成受眾群體的個性化分析等,從而幫助制定切實有效的營銷活動。數據導出支持按照用戶屬性和導出事件作為過濾條件,同時展示“預計可導出事件數”。開發者選擇不同的時間段和過濾條件,預估事件數就會隨之改變。 ...
  • 關於HarmonyOS 自定義View我們可以學習HarmonyOS自定義組件 這篇文檔,今天描述自定義折線圖的功能,我們從“準備工作”、“初始化畫筆”、“繪畫折線圖”、“運行效果圖”,這四個方面進行描述 1. 準備工作 想要實現折線圖我們瞭解Paint,獲取屏幕的寬高,這幾個功能的實現 獲取屏幕的 ...
  • 一、新增的語義化佈局標簽: 1. header和footer標簽 頁面中一個內容區塊的頭部和尾部佈局 2. nav 導航區域 3. article標簽 頁面中獨立的內容部分佈局 4. aside標簽 在獨立內容之外,但是又與article有關聯的部分佈局 二、新增媒體標簽 1. audio(音頻) ...
  • 前言 在 《一篇帶你用 VuePress + Github Pages 搭建博客》中,我們使用 VuePress 搭建了一個博客,最終的效果查看:TypeScript 中文文檔。 在搭建這樣一個博客後,其實還有很多的優化工作需要做,本篇我們來盤點一下那些完成基礎搭建後必做的 10 個優化。 1. 開 ...
  • 前言 大部分的面試者在IT行業面試中,提及設計模式,可以列舉一大堆,但是面試官要求細說的時候,往往部分基礎不夠牢固的同學只能提及簡單工廠。今天我們來對面試過程中最常見的簡單工廠、工廠方法和抽象工廠進行一個剖析,喜歡的朋友可以點個關註哦。 正文 在面向對象的編程中,一般通過繼承和虛函數來提供抽象能力, ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...