戲說領域驅動設計(十六)——實體概念

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

裝飾器模式又叫包裝模式,數據結構型模式;是指在不改變現有對象結構的情況下,動態的給改對象增加一些職責(即增加其額外功能)的模式。 在星巴克咖啡店,有美式咖啡(LongBlack)、無因咖啡(Decaf)、義大利農咖啡(Espresso)等不同的咖啡種類,也可以添加牛奶(Milk)、豆漿(Soy)、巧 ...


  现在开始正式的进入战术部分,我看前面发的一些文章,只要有代码的阅读量就高,没代码的就差太多了,难道是因为平台只要看到代码才会加强推荐吗?真要是这样那我是真醉了,其实学习DDD光看代码还真不行,需要很多理论支持的。如果您是新的读者我建议先把前面的内容都翻看一下,至少得有一些理论依据作支撑后面学习起来才会更有效率。本章主要讲解实体,属于战术部分最为核心的内容。有人说聚合重要,但聚合也是实体,重要度都高,所以要先讲基础的。

一、两类模型

  实体包含两类。如果只有属性及“getter/setter”方法,这叫“贫血模型”,DDD不推荐使用这种模型。再说了,数据模型本身就是贫血的,再多引个贫血的领域模型除了各种赋值操作外根本就没个卵用。另外一种模型叫“充血模型”,“充血模型”不仅要包含属性,还要包含业务方法,下面两张图展示了两类模型在设计时的区别。把充血模型比喻成“人”最合适,有属性还有行为。本章及后续文章所涉及实体和值对象都属于充血模型。

 

  您可别简简单单的认为这两种模型只是在包含方法上有区别,这里面的学问大着呢,你理解透了才能在用的时候不至于抓瞎。

  首先咱们先说其意义,贫血模型是一种数据传输对象,它用于表现数据;充血模型由于其包含了对象属性和业务能力,可以有效的表达真实世界中各类活灵活现的事务,比较适合作为领域模型来用。还有一点,您就没法通过贫血模型来进行业务推测,所以一般称之为反模式。是不是会惊呆了?“啥玩意儿,什么叫通过模型推导业务?”。这东西明摆着嘛,您通过对业务进行分析来设计领域模型,当然也可以通过领域模型反向推导出业务能力了。别抬杠说不行,那只能说明设计不到位,不是业务上没讲明白就是模型上少东西。比如您看上面“贫血模型”那张图,你能知道这个实体的业务能力是什么吗?第二张的“充血模型”就可以看出来:1)支持修改订单价格,且修改价格时订单不能是已完成的状态;2)不支持变更下单日期。为什么人常说业领域模型反映了业务规则,就是指这种可相互推导的能力。那么好了,领域模型这么重要,从何而来?答:根据业务需求进行推断和设计出来的,实体的识别会占用您的大部分工作,只要这东西搞定,编写代码就分分钟的事情了。我见过有些架构师只管设计然后让开发去实现,这种行为基本上是来搞笑的。您只要敢这么干,开发就敢违背你的设计而放飞自我。所以,好的架构师一定也是个优秀的研发,也要实际的参与一线的研发任务。

  第二,您别以为使用了Java或C#这种面向对象的语言就能写出面向对象的程序,无数的程序员用着面向对象的语言写着面向过程的代码。没办法,下了班就想玩儿王者荣耀,一点学习的心思都没有。再说了,咱可是大学生,天之骄子,只有破大专才需要恶补上学时的不足。

  第三,贫血模型一般出现在事务脚本式开发中,学习曲线较低,代码几乎无复用性;充血模型自治力高,可也不是属样都特别牛掰。这东西拆分出的组件(子对象、嵌套对象等)特别多,代码编写复杂也不易理解。我自己的代码三个月不看都会蒙圈。

  最后,假如您在设计或开发时发现存在着大量的无业务方法的贫血模型,在排除设计方式不正确的原因之外,也说明了此时使用事务脚本的方式实现代码会更好。您也别抬杠,OOP就是特别麻烦,需要多写业务模型、资源仓库等相关的代码。实现方式不对除了费力不讨好外只能用于拿出去装开发高手了。

二、实体定义

  实体的简单定义为:一种领域模型,此模型的定义并非来自于属性,而是一连串的连续事件和标识。说它是领域模型这个很好理解,“一连串的连续事件”是什么意思?“标识”是什么意思?让我们分别解释一下。“一连串的连续事件”就是说实体这个东西会由于某些事件而引发变化,可不管怎么变化其本质是不变的。比如说“人”这个实体,体重属性会随着减肥事件而产生变化,可再怎么变,这个人本质上仍然还是他。类似的还有性别,以当前的科技也不是不可变的,比如去趟泰国……就算是这人挂了,他还是他。不过话又说回来了,“人”的属性比如外形的变化可以让熟悉他的人都认不出来,那要怎么去唯一定位这个人呢?这其实就是“标识”的作用了。现实中,“人”一般会有身份证号,这个就可作为标识来用。标识一定是唯一且不变的,极端情况下实体可能没有属性,但也得有ID。当然了,咱不能抬扛说没有方法和属性只有ID能否还算为实体,设计出这么一个东西除了作为超类用,没其它太大的价值。

  实体并不是独立存在的,它还会同其它的对象产生关联。一个“人”有各种属性,会干各类事情,这是“人”的特征与能力,可人并不是独立的。在与其它的人产生了关系关联后就出现了各类角色比如父亲、母亲、上下级等;在与其它的事务产生了行为关联后就出现了需要其它事务配合才能完成能的活动比如结婚、离婚。可以这么说,正是因为有了关联,实体才能变得有血有肉有活力。所以在设计实体时就会出现继承、嵌套对象、外部对象依赖等各类关联,也让实体变得复杂。

三、实体的特征

  实体的特征主要有三点,不过最值得一说的就是ID这块。另外,实体设计起来非常容易和我们后面讲的“值对象”弄混了,也就是初学者常常遇到的不知道一个对象到底应该设计为实体还是值对象。这东西别说小白了,好多有经验的人做的时候也经常搞乱呢,所以需要使用迭代式设计来解决。回到ID这个问题上面,您在实际使用的时候尽量别用UUID,这东西做个Token什么的还凑合,作为业务ID就差了点意思,本身没什么规律也慢,毕竟我们通常会将实体的ID也同时作为数据库表的ID来用,而UUID的无序性造成插入和检索效率都不怎么高。想简单一点整个雪花算法就差不多了,绝对够用,毕竟并不是所有的单位都和大厂一样能资源和能力建立分布式ID生成系统。

  有些工程师喜欢使用数据库的自增长ID作为实体ID,这种方式我在项目中用过,效果一般,有些情况下还特别麻烦。比如在进行实体的批量新建时,引入了“工作单元”后,需要把待执行久的对象放到一个Map中,但这个时候实体没有ID,多实体的情况下绝对扯犊子。另外的场景比如序列化实体后再发布一个领域事件,事件中需要有一个实体的ID,所以你就需要使用一些手段来保证实体序列化后事件能获取到这个ID。所以以我个人的经验,最好使用预生成ID也就是在创建实体的时候进行ID的创建,延迟ID的方式写代码比较难受。

  实体的ID通常会跟随实体的一生,因为是不变的,在设计实体的时候不应该有类似“setId()”这种方法,唯一给ID赋值的方式就是通过构造函数。另外还需要注意的就是有些人喜欢使用一些框架比如“Entity Framework”,使用后就不用再考虑领域模型持久化的事情。我本人写了10年左右的C#后转行Java,所以不太清楚EF框架这几年的变化。但就我个人而言,不是很喜欢使用这种编程方式,过于依赖框架不说,涉及一些关系特别复杂的聚合时简直麻烦死了。但是,EF可以让工程师聚焦于业务模型的设计,由框架自动生成业务模型的子类并隐蔽的完成序列化工作的这种方式是推荐的。所以如果BC设计的合理,使用EF框架也挺好。

四、实体设计经验

  实体的设计需要研发人员投入很多的精力,也是最容易出现设计不当的情况。如果细节上的失误其实问题不大,因为有BC进行隔离出现问题后一般并不会出现大面积蔓延。就怕是在设计的时候让实体脱离了BC的约束而出现了超级类,这种情况下,且不说BC间的交互来往变多,代码的维护也比较恶心。所以在设计的时候,要确保你的实体不论是在属性方面还是责任方面都不要脱离BC的责任约束。比如在“鉴权”的BC中,关注的就是用户的角色和权限,您就不要把用户的交易流水信息做为用户的属性,那是“账务”BC所关心的。我们设计BC的目的就是为了实现业务责任单一化,那其内部的实体也得遵从这个原则,不能做超出所在BC责任范围外的事儿。

  另外一条需要额外注意的原则就是实体的构造。构造实体通常包含两种方式,一是构造函数,二是使用工厂。使用构造函数时你需要保证其参数应能够使得当前实体在构造后是合法的,该有值的有值,该不为“null”不能为“null”。比如“用户”对象包含了ID和用户名两个属性,那么你在使用构造函数时需要为这两个属性赋值,而且要保证其值是合法的比如用户名是空字符串,不能超过某个长度。那位可能会问,要是不合法要怎么办?抛个业务异常呗,看一下如下代码。

public class Order extends EntityModel<Long> {
    private String name;

    public Order(Long id, String name) throws OrderCreationException {
        super(id);
        this.setName(name);
    }

    public String getName() {
        return name;
    }

    public void setName(String name) throws OrderCreationException {
        if (StringUtils.isEmpty(name)) {
            throw new OrderCreationException();
        }
        this.name = name;
    }
}

  以我的经验来看,每个实体都应该有一个构造函数用于构造实体,不论其有多少个属性。属性多了您可以将部分属性包装成值对象,如果不想值对象对外暴露可以将构造函数设置为“protected”,然后建立一个继承于本实体的工厂对象,将构造实体时所需要的数据以视图模型的方式传进去,然后在工厂内部进行实体的构造,请看如下代码。

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;
    }

    public String getName() {
        return name;
    }

    public Contact getContact() {
        return contact;
    }
}

public class OrderFactory extends Order {

    private OrderFactory(Long id, String name, Contact contact) throws OrderCreationException {
        super(id, name, contact);
    }


    public static Order create(OrderVO orderInfo) throws OrderCreationException {
        if (orderInfo == null) {
            throw new OrderCreationException();
        }
        Contact contact = new Contact(orderInfo.getEmail(), orderInfo.getName());

        return new Order(0L, orderInfo.getName(), contact);
    }
}

  上面的案例中,如果想直接通过构造函数创建“Order”类型的对象,就需要创建“Contact”类型的对象也就是您还需要了解“Contact”对象的构造方式。这还只是一个嵌套对象,如果多了那简直就是构造噩梦。通过使用工厂的方式,您不仅把对象的创建细节放在了工厂中进行封装,而且还能避免如“Contact”类型的泄露,这是一举几得来着?上述代码请注意标红的部分,虽然是细节但很重要。对象的创建方式总结一下,属性少时直接使用构造函数否则建立一个对象工厂。

  还有一条值得分享的实体设计经验就是我们在确定实体的属性后只提供getter方法,根据需要再提供对应的用于修正属性的方法,这种方式可以最大化保障实体信息的安全以及防止属性被意外篡改。实际上,对象的封装性越好,后续出现问题的可能性就会越少。尤其是团队协作时,您哪知道谁手欠意外改了某个属性而引发程序BUG。

总结

  其实涉及实体的内容挺多的,比如实体的存储、验证、编写方法时的注意事项等,能想得上来的原则就好多。比如在验证方面,我个人一般会使用二级验证+内、外验证的方式来实现,这方面内容值得开一个新的章节做细讲。下一章,我展示一下个人在实际的项目中是如何使用实体的,敬请关注。


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

-Advertisement-
Play Games
更多相關文章
  • 華為 AppGallery Connect 提供了一個應用內消息的服務。應用內消息服務可以在用戶使用應用時,基於用戶使用情景向用戶發送有針對性的消息,鼓勵用戶使用應用的某些關鍵功能,也可以藉助應用內消息發送更具吸引力的營銷內容,增強用戶粘性。今天就來教大家如何接入 AppGallery Connec ...
  • 在競爭日趨白熱化的渠道推廣環境中,對於企業而言,如何控制投放成本、如何提升投入產出比,是做好產品運營時首先要考慮的問題。 華為分析服務的渠道分析能力基於對不同渠道效果對比分析、渠道質量深度分析、渠道用戶特征分析等,全方位助力企業量化渠道投放成本與轉化效果,及時調整投放策略。 使用場景 場景一:不同渠 ...
  • 本文分享於華為開發者論壇《如何使用DTM將App事件發送到Google Analytics》,可觀看視頻具體集成指導。 作為一名開發者或App運營人員,實時獲取用戶在App中的行為數據是日常且重要的工作之一。例如,各個渠道用戶在App各個關鍵節點的轉化數據以及最終付費情況該如何快速獲取? 通過動態標 ...
  • 面對五花八門的開發板不知道該怎麼選取?晶元、模組、開發板傻傻分不清?如何使用代碼控制開發板?本期,我們將一一為你解答。 ...
  • 什麼是動態標簽管理? 動態標簽管理(Dynamic Tag Manager,簡稱“DTM”),可讓開發者快速配置更新測量代碼及相關代碼片段,可以基於Web界面輕鬆地進行分析、測量代碼的配置,完成特定事件動態跟蹤並將數據傳送給第三方分析平臺,實現營銷數據隨需跟蹤。本期我們向大家介紹:如何從零開始集成D ...
  • 配置好 renren-fast 腳手架,學習完 Spring MVC 架構後,我需要具體調試 renren-fast 的介面,比如要新增某個介面。 什麼是前後端分離 運行 renren-fast 項目時,我們訪問 http://localhost:8080/renren-fast/ 的結果: 可以看 ...
  • 現代軟體開發和以前的軟體開發有很大的不同,以前軟體一般都會根據業務流程,設計程式的入口和程式的出口,即軟體耦合性很強。隨著軟體技術的不斷發展和DDD領域設計模型的不斷深入研究,在微服務化開發框架的大力推廣下,Docker技術和K8s 技術的普及,新一代的企業應用架構再次革新了軟體行業。 無論是軟體開 ...
  • 適配器模式是什麼 適配器模式(Adapter Design Pattern)適配器是一種結構型設計模式, 用來將不相容的介面轉換為相容的介面。適配器可擔任兩個對象間的轉換器, 它會接收對於一個對象的調用, 並將其轉換為另一個對象可識別的格式和介面。 為什麼用適配器模式 兩個對象直接由於格式或者介面不 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...