DDD领域模型学习笔记

前提、DDD的基本概念

领域驱动设计(Domain-Driven Design,简称DDD)是由Eric Evans提出的一种软件开发方法论,旨在应对复杂业务系统的设计和实现。它的核心思想是将软件的设计与业务领域紧密结合,通过深入理解业务需求,构建一个反映真实业务逻辑的模型,并用代码清晰地表达出来。

在传统的开发模式中,我们常常以技术为中心,先设计数据库表结构或API 接口,再围绕这些技术组件填充业务逻辑。而DDD则反其道而行之,它强调**领域(Domain)**是软件的核心,技术只是实现领域的工具。换句话说,DDD的目标是通过代码和架构让业务逻辑成为系统的"主角"。

1、DDD的核心原则

聚焦领域 :软件的核心是解决业务问题,而不是技术本身。

统一语言(Ubiquitous Language) :开发团队和领域专家使用一致的术语,确保沟通无歧义。

模型驱动设计 :通过领域模型将业务概念抽象为对象、关系和行为,并映射到代码中。

分层架构 :将系统划分为多个层次,隔离关注点,提升可维护性。

限界上下文(Bounded Context):为不同的业务子域定义清晰的边界,避免概念混淆。

2、DDD的两个阶段

战略设计 :关注宏观层面,比如划分限界上下文、定义子域、建立上下文映射。

战术设计:关注微观层面,比如如何设计实体(Entity)、值对象(Value Object)、聚合(Aggregate)、领域服务(Domain Service)等。

3、为什么需要DDD

假设你正在开发一个电商系统,里面涉及商品、订单、库存、支付等功能。如果没有清晰的领域划分,可能出现以下问题:

  • 商品的"价格"在不同模块中有不同定义,导致逻辑混乱。
  • 订单和库存的耦合过于紧密,改动一处牵动全身。
  • 随着业务扩展,代码变得难以维护,新增功能需要改动大量现有代码。

DDD通过领域模型和限界上下文解决了这些问题。它鼓励你先理解业务的全貌,再通过分层和模块化设计,让每个部分的职责清晰,降低耦合性。

一、代码分层结构

1.1、DDD圈子中经常讨论的几种代码分层结构

  1. MVC架构
  2. 四层架构(Eric书中的架构模式)
  3. 六边形架构(洋葱架构)
  4. 整洁架构
MVC 四层架构
六边形或洋葱架构 整洁架构

1.2、实际项目上

  • ORM选择Mybatis Plus 用的比较多,兼顾方便和灵活性,Repository 这一层自己实现。
  • ApplicationService 和 DomainService 分开,ApplicationService 处理场景用例,DomainService 处理通用业务逻辑(应用(上下文有关的继承)和服务(解偶上下文无关的独立能力)分离)。

1.3、总结

  1. 提前设计一个打样工程非常有必要,不然很多人同时写代码会不一致。
  2. 避免千层饼架构,每一层尽可能要有意义,避免千层饼架构。
  3. 注意微服务之间的宏观架构和微服务内的微观架构的职责区分。

1.4、经验

  1. 一个聚合根一般对应一个控制器,控制器中包含场景用例的方法。
  2. 一个聚合根一般对应一个服务,服务中包含业务逻辑方法。
  3. 一个聚合根一般对应一个仓储,仓储中包含CRUD方法。

二、充血模型和贫血模型

为什么感受不到以DDD风格写代码?因为大部分人都基本上以面向对象的方式在写代码,只是不能完全理解面向对象,特别是java中的面向对象。

-所以DDD中会有一个贫血模型、充血模型、涨血模型的概念。

2.1、贫血模型

一个对象只需要包含数据,而不需要包含处理数据的逻辑,比如订单对象只需要包含订单号、金额、状态等数据。

2.2、充血模型

一个对象需要包含一些处理自身状态的逻辑,比如订单对象需要包含计算金额的逻辑

2.3、涨血模型

对象的行为超出了自己的范围,比如订单对象需要包含支付、取消等行为,这些行为超出了订单对象的范围,应该放到订单服务中处理。

三、聚合,以及如何对大聚合进行拆分

聚合这个概念在DDD和UML的术语体系中是不匹配的,聚合根应该是对象的组合关系

-从关系模型,到对象树模型,再到对象森林,我们就能知道聚合的意义了。先知道三个概念,如下:

  1. 实体 Entity
  2. 聚合根 Aggregate Root
  3. 值对象 Value Object

3.1、了解聚合这个概念的好处

  1. 用聚合可以保持业务逻辑的一致性
  2. 用聚合可以让对象解偶
  3. 用聚合可以降低人的心智负担,把大量无关的对象分门别类
  4. 用聚合可以指导进一步设计,比如指导控制器、仓储、服务的创建,都可以和聚合一一对应,聚合是程序的骨架。

3.2、如何合理的聚合和拆分

聚合的几个问题

  1. 聚合的大小
  2. 实体在聚合中的归属问题:一个实体只能属于一个聚合根,不能跨聚合根。比如订单行和物流单行,中的字段看似非常相似,其实绝对不能搞到一起。
  3. 批量操作的导致的问题*------也就是可以不用聚合*
    • 如果是业务逻辑,需要用聚合一个一个处理
    • 如果本身就是搬运数据,其实模型是不可见的,直接批量操作数据
  4. 聚合过大时,如何拆分
    -对聚合根进行拆分
梳理关系网 关系紧密的做聚合
大型聚合拆解示例

总结

  1. 聚合是设计出来的,在很多时候需要取舍;分清楚发现和设计的关系。
  2. 一般来说是整存整取,但只要保证语义也可以对聚合根进行批量操作;聚合是业务一致性单元,不是存储单元。
  3. 经验:对象树的深度不要超过2层,宽度不要超过4-5个对象

四、什么是聚合根

聚合根(Aggregate Root)是DDD中的一个核心概念,用于组织和管理一组相关的领域对象,确保它们的整体一致性和完整性。聚合根是领域模型中的关键组件,它不仅封装了领域内的复杂业务逻辑,还提供了控制访问和维护数据一致性的机制,是构建可维护、可扩展的软件系统的重要基石。

4.1、设计原则

4.1.1、主要特点

  • 聚合的概念:聚合是一组相关对象的集合,这些对象形成一个完整的业务概念,并作为一个单元进行操作。在聚合内部,对象之间有严格的关联和依赖关系。
  • 根实体:聚合根是聚合中的一个特殊实体,它是外部世界访问聚合内部其他成员的唯一入口点。这意味着外部对象不能直接持有聚合内非根实体的引用,必须通过根实体来访问和操作它们。
  • 边界定义:每个聚合都有一个清晰的边界,这个边界定义了聚合的范围,以及哪些操作可以在聚合内部执行,哪些操作需要通过根实体来协调。这有助于维护数据的一致性和完整性。
  • 一致性保证:聚合根负责维护其内部的所有业务规则和一致性约束。当外部尝试修改聚合状态时,所有必要的验证和业务逻辑都在聚合根内部执行,确保操作的原子性和一致性。
  • 唯一标识 :聚合根拥有一个全局唯一的标识符(ID),外部系统通过这个 ID 来识别和访问聚合。这个 ID也是与其他聚合建立关联的基础。
  • 生命周期管理:聚合根还负责管理器内部对象的生命周期,包括创建、更新和删除聚合内的实体和值对象。

4.1.2、明确的边界

  • 聚合根定义了一个清晰的边界,限定那些对象属于聚合内部,那些属于外部。边界内的对象作为一个整体处理,对外部隐藏内部细节

4.1.3、单一入口点

  • 聚合根是外界访问聚合内部其他对象的唯一合法途径。外部对象不能直接持有或操作聚合内的非根实体,所有交互都需通过根实体的方法进行

4.1.4、内聚性

  • 聚合内的所有对象应紧密相关,共同完成一个业务功能。这意味着聚合内的对象和操作应该围绕着一个核心业务概念组织

4.1.5、一致性规则封装

  • 聚合根负责维护其内部的一致性,包括业务规则、验证逻辑等。所有改变聚合状态的操作都应该在根实体中实现,确保操作的原子性和业务规则的遵守

4.1.6、标识唯一性

  • 每个聚合根都有一个全局唯一的标识符(ID), 用以区分不同的聚合实例。这个 ID 也用于外部对聚合的引用

4.1.7、生命周期管理

  • 聚合根控制其内部成员(实体和值对象)的生命周期,包括它们的创建、更新和删除

4.1.8、有限的大小

  • 为了保持聚合的可管理和易于理解,通常建议聚合的大小不要过大。过大的聚合可能导致性能问题和复杂度增加

4.1.9、聚合内部引用

  • 聚合内部的对象可以通常引用相互协作,但这些引用应限制在聚合边界之内。对合聚合间的关联,通常适用ID进行间接引用

4.1.10事务边界

  • 在事务处理中,聚合根常常作为事务的边界,确保事务内的所有操作要么全部成功,要么全部失败,以此来维护数据的完整性

4.2、聚合根和实体对象有什么区别?

4.2.1、身份标识(Identity)

  • 实体(Entity):实体具有唯一标识(ID),这个 ID用来区分领域中的每一个单独的实体实例。实体的ID在整个系统范围内具有唯一性。
  • 聚合根 (Aggregate Root):聚合根也是一个实体,但它在一个聚合中扮演领导角色。它的ID不仅在系统范围内是唯一的,而且是外部世界访问聚合内部其他实体的入口点

4.2.1、聚合边界(Aggregate Boundary)

  • 实体:实体可以是聚合内部的一部分,也可以是独立存在的。如果实体是聚合内部的一部分,则其ID在聚合内部唯一即可,外部访问该实体必须通过聚合根
  • 聚合根:定义了聚合的边界,决定了那些对象属于聚合内部。外部对象不能直接访问聚合内的非根实体,只能通过聚合根暴露的接口进行操作

4.2.2、职责与控制

  • 实体:实体主要负责维护自身的状态和行为,可能包含一些简单的业务逻辑
  • 聚合根:负责维护整个聚合的一致性,包括内部实体的状态更改和业务规则的执行。它控制着对聚合内部元素的所有修改,确保在任何时刻聚合都处于有效状态

4.2.3、生命周期关联

  • 实体:实体的生命周期通常由其所在聚合根或外部服务(如 Repository)管理
  • 聚合根:除了管理自己的生命周期外,还间接管理其内部实体的生命周期,决定它们的创建、更新和删除

4.2.4、访问控制

  • 实体:如果不是聚合根,实体通常不直接暴露给外部,外部组件不应直接持有实体的引用
  • 聚合根:是外部访问聚合内部的唯一合法通道,提供对外接口,隐藏内部实现细节和复杂性

综上所述,聚合根是一种特殊的实体,它不仅代表一个独立的业务概念,还负责协调和保护其内部的其他实体和值对象,确保整体的业务规则得到执行,维持数据的一致性和完整性。实体则是领域模型中的基本构建块,标识具有唯一标识的领域对象,而聚合根在实体之上提供了一层额外的结构和控制。

五、值对象

5.1、理解值对象

  1. 实体有ID值对象没有ID
  2. 值对象的属性是不可变的,一旦创建就不能改变
  3. 值对象的相等性是基于属性值的,而不是引用
  4. 通俗来说值对象就是属性包

5.2、为什么需要用值对象

复制代码
减少重复字段的定义

5.3、项目中真实的例子

  1. 订单中的地址信息,包括国家、省份、城市、区县、街道、详细地址、邮政编码等
  2. 金额:包括金额值和货币单位
  3. 身份证号:号码本体即全部含义,无需主键,可跨表复用
  4. 坐标点:经度+纬度,可整体替换,不可"部分修改"
  5. 时间段:开始时间+结束时间,常用于排班、价格策略等场景

5.4、在数据库中如何存储值对象,这两种方法按情况使用

  • 可以将值对象的属性作为独立的字段存储在数据库表中(使用映射工具,自动映射展开)
  • 也可以将值对象作为JSON或xML格式的字符串存储在数据库表中(序列化成JSON)

六、限界上下文------逻辑上下界

6.1、为什么需要限界上下文

  1. 通俗来说就是根据模型划分出逻辑边界,上下文中装的是代表系统状态的实体、对象。
  2. 限界上下文是DDD中非常重要的一个概念,它是一个边界,用来划分不同的业务领域。
  3. 在单体中,划分模块。
  4. 在微服务中,限界上下文往往对应一个服务。
  5. 上下文之间最终一致性事务,上下文内本地事务。

6.2、划分限界上下文的参考是什么

识别上下文可以从概念的二义性着手,比如商品的概念在物流、交易、支付含义完全不一样,但具有不同内涵和外延,实际上他们处在不同上下文。

6.3、战略设计和战术设计

  1. 在DDD中把上下文这个层次叫做战略设计。
  2. 把上下文内的开发过程叫做战术设计。

6.4、总结

  • 限界上下文实际上是"设计"而不是"分析",要注意这一点,上下文是一个解决方案,而不是需求问题。
  • 上下文划分与大小关系不大,与是否能相对独立提供服务比较大。
  • 上下文划分注意相互依赖的问题。
  • 在实际操作的时候,可以用聚合来划分,找核心模型,然后来划分,不然项目太大模型太多不好操作。

七、DDD建模前准备的术语

7.1、程序员可能不太喜欢,但管理者非常关心的概念

  • 问题空间、解决方案空间

  • 战略设计、战术设计

    • 战略设计要求把技术规划做好,在开始写代码之前就需要把上下文和微服务划分合理,还有核心模型,这些改动起来比较麻烦。
    • 战术设计是指把上下文内的开发过程叫做战术设计。
    • 建模原则
      • 通用语言: 语言决定了认知边界,对模型正确的术语定义就已经成功了一半。
      • 聚焦核心域: 只做最重要的事情。
      • 协作共创:必须从业务中提炼技术方案,共识大于正确,否则技术毫无意义。
  • 为什么我还要讲这部分?

    • 你不能永远站在程序员视角思考问题,需要从市场、技术管理的角度。
    • 康威定律是马尔文康威1967提出的:"设计十系统的架构受制于产生这些设计的组织的沟通结构。"
  • 总结

    • 更重要的是大型系统团队之间如何协作-领导的关注点在于比起架构是否干净合理。
    • 这是架构师在很多公司需要面对最严峻的可题。
    问题空间------解决方案空间 问题空间------解决方案空间
示例 模型梳理前 模型梳理后
圈子下的用户还是用户吗?
订单下面放入的还是商品吗?

7.2、核心域、支撑域、通用域梳理

7.2.1、核心概念解析

‌ 7.2.1.1、 核心域 (Core Domain)

这是决定业务成功和公司‌核心竞争力‌的子域,是整个系统中最关键的部分。它直接对业务产生价值,是企业必须自主研发、全力投入的领域。

  • 识别方法‌:如果该功能无法直接购买、无法外包,且是企业存在的根本理由,则属于核心域。
  • 示例‌:电商平台的销售交易系统、智慧课堂的订阅域。
7.2.1.2、 支撑域 (Supporting Subdomain)

这是具备‌企业特性‌但非核心竞争力的功能,市场上找不到现成的通用方案,却又不得不做以支撑业务运转。

  • 策略‌:重要性次于核心域,需自主开发但无需作为战略重点,资源投入适中。
  • 示例‌:特定业务的数据字典、定制化的排行榜系统、专栏管理或评论系统。
7.2.1.3、 通用域 (Generic Subdomain)

这是没有太多个性化需求、同时被多个子域使用的‌通用功能‌。这类功能通常无企业特点限制,行业内有成熟解决方案。

  • 策略‌:无需投入大量研发资源,可直接购买第三方服务或使用开源方案。
  • 示例‌:用户认证、权限管理、消息通知、日志系统。

7.2.2、划分意义

通过这种划分,企业可以明确不同领域的属性:

  • 核心域‌:全力投入,打造差异化竞争优势。
  • 支撑域‌:按需建设,确保业务闭环。
  • 通用域‌:低成本获取,避免重复造轮子。

这种策略有助于将有限的研发资源集中在最能创造价值的环节,实现效率与竞争力的最大化。‌‌

核心域、支撑域、通用域的识别 规划阶段->迭代实施阶段

八、DDD建模过程

8.1、第一种:系统词汇法(OOA)

复制代码
寻找系统中的客体,表达状态的被操作者。

8.2、第二种:用例分析方法

  1. 通过原型图整理,用例(UseCase)是对一个活动者使用系统的一项功能时所进行的交互过程的一个
    文字描述。
  2. 在用户故事中寻找名词(这块现在可以通过AI实现)。
  3. 分析参与者、用例关系、名词(模型)之间的关系,辨析模型是否正确。

8.3、第三种:彩色建模(四色建模)

四色建模法其实是以数据驱动,通过挑选一些关键数据(类似于办事过程中的存根),来还原整个业务流程。然后基于这个线索,找出时标性对象(moment-interval)、实体(party/place/thing)、角色(Role)、描述对象 (description) 。

用四种颜色标注这些对象,参考:

  • 红色:时标性对象(moment-interval)
  • 绿色:实体(party/plac/thing)
  • 蓝色:角色 (Role)
  • 黄色:描述对象(description)

"任何业务事件都会以某种数据的形式留下足迹"。

8.4、第四种、协作建模(事件风暴)

8.5、总结

本质上是通过不同的线索,寻找对象。参考:建模方法元模型https://mp.weixin.gg.com/s/B3tJhSh Az5g8s0aMVZtUA

没有一种严格推理输出上下文的方法,更多的还是经验和取舍权衡。

九、DDD在敏捷团队中如何落地

9.1 核心域、支撑域、通用域的划分

9.2 规划阶段

9.3 落地后指标规划

9.4 技术选型

9.5 领域模型设计

9.6 模块(服务)设计

9.6.1、服务系统设计

9.6.2 服务系统域周边系统管理梳理

9.6.3 系统内部功能模块设计

9.7、DDD领域分层参考示例

9.8 部署架构图

9.9 代码开发的质量保障措施

十、每个人都认为自己对DDD理解是正确的

DDD建模的没有最正确的,只有最合适的。说门槛高也不高,说门槛低也不低。每个人只要去投入时间研究都能形成自己的一套规则,这是DDD最大的挑战。所以在实践DDD的时候,一定要记住"共识大于对错"你只能根据经验整理一些建模的原则。

  1. 即使你觉得不合理的模型,别人也能写出代码来,这是人与人之间心智的差异只不过不合理的模型在业务变化时,不是很方便,会产生一些空字段之类的。-

  2. 设计不合理,如果业务发生变化就不太好拓展。

    比如把支付信息放到订单上,当出现拆单的时候就不好弄了。

    使用关系表处理多对多关系,造成更多状态字段错误.

  3. "all models are wrong some are useful"-没有正确的错误的模型,即使是物理学

十一、DDD落地参考文章地址

DDD落地参考文章地址

相关推荐
鱼子星_2 小时前
【C++】stack和queue的应用及其模拟实现:适配器与容器适配器
数据结构·c++·笔记·stl
LccKyI2 小时前
C#学习day05(开发福彩双色球系统附思维导图)
开发语言·学习·c#
湘美书院--湘美谈教育3 小时前
湘美书院谈探索式教育,中医药AI研究络病理论
大数据·人工智能·深度学习·学习·生活
PC2005-cloud3 小时前
Redis学习笔记:Cluster 集群读写分离实战,Lettuce + Spring Boot 从配置到验证
redis·笔记·学习
math_hongfan3 小时前
鸿蒙存储异常高级排查:文件损坏检测/数据恢复/读写失败重试/磁盘空间预警系统性根治方案
学习·华为·harmonyos·鸿蒙
文艺理科生3 小时前
3 年,8 种方案,1 次重构:LangChain 记忆方案如何从混乱走向清晰
前端·后端·架构
那个松鼠很眼熟w4 小时前
5.GBK字符集,字符编码
笔记
小小工匠4 小时前
当 Agent 遇上逆向:拆解 reverse-skill 的技能路由架构
架构·逆向·reverse-skill
阿米亚波4 小时前
【C++ 异常处理】try-catch
开发语言·c++·笔记·try-catch