DDD——领域模型设计要素:实体、值对象和聚合
ccwgpt 2024-10-13 01:28 30 浏览 0 评论
1 领域模型设计要素
1.1 领域设计模型
1.1.1 理想的对象设计模型
理想的对象:不会考虑数据的存储/性能的瓶颈以及依赖的千丝万缕,认为对象在计算机的内存中自给自足。但实际上会有以下问题:
问题一 领域模型对象如何实现数据的持久化
问题二 领域模型对象的加载以及对象间的关系该如何处理
问题三 领域模型对象在身份上是否存在明确的差别
问题四 领域模型对象彼此之间如何做到弱依赖地完成状态的变更通知
1.2 战术设计元模型
战术模型如下图所示:
设计元模型规定:只能由实体,值对象,领域服务和领域事件表示模型。聚合用于封装实体和值对象,并维持自己边界内所有对象的完整性。要访问聚合,只能通过聚合根的资源库,这就隐式地划定了边界和入口,有效控制了聚合内所有类型的领域对象。若聚合的创建逻辑比较复杂或存在可变性,可引入工厂来创建聚合内的领域对象。若牵涉到实体的状态变更,领域元模型建议使用领域事件来推动。
1.3 模型元素的哲学依据
理论依据:“述说某物于某物”。其实就是 主语-谓语 的结构。在亚里士多德的哲学观中,实体 是描述事物的主体,其他范畴必须“内居”于一主体。也就是说实体是真实世界的形而上学基础,而其他范畴则成为实体的属性,需要有某种实体作为属性的基础。所以模型元素如下
a 实体
b 值对象
c 领域事件
d 领域服务
1.4 实体
定义:就是能够以主题类型形式表达领域逻辑中具有个性特征的概念,而这个主体的状态在相当长一段时间内会持续地变化,因此需要一个身份标识来标记。
实际情况,实体往往具有几十乃至上百个属性,如果缺乏封装,就会因为暴露太多的信息让实体变得臃肿。当实体的属性被封装为不同层次的实体和值对象时,与之相关的行为也需要随之转移。这样才满足信息专家模式,既能避免贫血模型与事务脚本的实现,又能形成对象之间良好的行为 协作。
一个实体应该具备以下3个要素
1.身份标识
2.属性
3.领域行为
1.4.1 身份标识
身份标识是实体对象的必要标志,在领域设计中,没有身份标识的领域对象就不是实体。
实体的身份标识的类型分为两种:通用类型 与 领域类型
通用类型ID:没有业务含义,采用一些常用的技术手段就可以实现,例如:随机数的标识,数据库自增长的标识,根据机器MAC地址和时间戳生成。如果在分布式系统中保持全局的唯一性,需要特殊的算法(例如SnowFlake算法)
领域类型ID:通常与各个限界上下文的实体对象有关,例如为Employee定义EmployeeId类型。代表了业务含义。
1.4.2 属性
实体的属性用来说明主体的静态特征,并持有数据与状态。一般分为原子属性和组合属性。开发语言内建类型的属性就是原子属性(Integer ,String,long等),只定义的类型为组合属性。两种属性的划分标准就是:该属性是否存在约束规则,组合因子或属于自己的领域行为。
组合属性可以是实体,也可以是值对象,取决于该属性是否需要身份标识。
1.4.3 领域行为
根据不同的行为特征,实体的领域行为分为:
a 变更状态的领域i行为
b 自给自足的领域行为
c 互为协作的领域行为
1.4.4 变更状态的领域行为
实体对象的状态由属性持有。与值对象不同,实体对象允许调用者更改状态。领域驱动设计认为,由业务代码组成的实现模型是领域模型的一部分,业务代码中的类名,方法名应从业务角度表达领域逻辑。领域专家最好也要参与到编程元素的命名讨论上,使得业务代码遵循统一语言。例如,修改产品价格的领域行为应该定义为 changePriceTo(newPrice)方法,而不是setPrice(newPrice)。方法名也传递了业务知识,成了实体类拥有的领域行为。
1.4.5 自给自足的领域行为
意味着实体对象只操作了自己的属性,不外求于别的对象。主要对实体已有的属性包括调用该实体组合属性定义的方法返回值进行计算,返回调用者希望获得的结果。
1.4.6 互为协作的领域行为
实体不可能都做到自给自足,有时也需要调用者提供必要的信息。这些信息往往通过方法参数传入,这就形成了领域对象之间互为协作的领域行为。
有一种特殊的领域行为,就是针对实体包括值对象进行“增删改查”。领域驱动设计将这些行为分配给了专门的资源库对象,实体无须承担“增删改查”的职责。实体拥有变更状态的领域行为,修改的只是对象的内存状态,与持久化无关。除了“增删改查”,创建行为也是对象生命周期管理的一部分,代表了对象在内存中从无到有的实例化。
1.5 值对象
定义:通常作为实体的属性。如果只关心一个模型元素的属性时,应把它归类为值对象。值对象应该是不可变的,不要为它分配任何标识,而且不要把它设计成实体那么复杂。
1.5.1 值对象与实体的本质区别
实体与值对象的本质区别在于是否拥有唯一的身份标识。值对象缺乏身份标识,在领域设计模型中,表达的是实体的属性。
1.5.2 不变性
领域驱动设计建议尽量将值对象设计为不变类。因为一个不变类是线程安全的,可以减少并发控制成本。
不变类需要满足如下条件:
a 对象创建以后其状态就不能修改
b 对象的所有字段都是final类型
c 对象是正确创建的(创建期间没有this引用溢出)
1.5.3 领域行为
实际上无论实体对象还是值对象,都要遵循面向对象设计的基本原则,如信息专家模式,将操作自身数据的行为分配给它。
通常值对象提供如下能力
a 自我验证
b 自我组合
c 自我运算
1.5.4 自我验证
当一个值对象拥有自我验证的能力时,拥有操作值对象的实体类就会变得轻松许多。否则实体类中就会有大量的验证代码,干扰读者对主要逻辑的理解。
1.5.5 自我组合
值对象往往牵涉到对数据值的运算。为了更好的表达其运算能力,可定义相同类型值对象的组合运算方法,使得值对象具备自我组合能力。
1.5.6 自我运算
就是根据业务规则对属性值进行运算的行为。根据需要,参与运算的值也可以通过参数传入。
一个拥有合理领域行为的值对象可以分摊在实体身上的重任,让实体的职责变的更单一。
1.6 值对象的优势
在进行领域设计建模时,要善于运用值对象而非内建类型去表达那些细粒度的领域概念。值对象优势如下:
a 内建类型无法展现领域概念,值对象则不然
b 内建类型无法封装显而易见的领域逻辑,值对象则不然
c 内建类型缺乏验证能力,值对象则不然
1.7 聚合
1.7.1 类的关系
类的关系如下:
a 范化
b 关联
c 依赖
1.7.1.1 范化关系
泛化关系体现了通用的父类与特定的子类之间的关系。泛化关系会导致子类与父类之间的强耦合,父类发生的任何变更都会传递给子类。
1.7.1.2 关联关系
关联关系代表了类之间的一种结构关系,用以指定一个类的对象与另一个类的对象之间存在连接关系。包括一对一,一对多和多对多的关系。
还存在一种特殊的关联关系:关联双方分别体现整体与部分的特征:合成关系和聚合关系。
合成关系:不仅代表了整体与部分的关系,还体现了强烈的“所有权”特征。这种所有权使得二者的生命周期存在一种啮合关系,即组成合成关系的两个对象同属于同一个生命周期。当代表整体概念的主对象被销毁时,代表部分概念的从对象也随之销毁。例如:School和Classroom的关系。
聚合关系:同样代表了整体与部分的关系,但没有所有权,不会约束它们的生命周期。例如:Classroom和Student存在聚合关系。
1.7.1.3 依赖关系
依赖关系代表一个类使用了另一个类的信息或服务。一般依赖关系产生于:
a 类的方法接收了另一个类的参数
b 类的方法返回了另一个类的对象
c 类的方法内部创建了另一个类的实例
d 类的方法内部使用了另一个类的成员
1.7.2 模型的设计约束
如果不对类的关系进行控制,耦合就会蔓延。一旦需要考虑数据持久化,一致性,对象之间的通信机制以及加载数据的性能等设计约束,网状的耦合关系就会成为致命的毒药。
1.7.2.1 控制类的关系
从以下3点下手
a 去除不必要的关系
b 降低耦合的强度
c 避免双向耦合
1.7.2.2 引入边界
领域设计模型并非真实世界的直接映射。如果真实世界缺乏清晰的边界,在设计时,我们就应该给它清晰地划定边界。划定边界时,同样依据“高内聚松耦合”原则。
1.7.3 聚合的定义与特征
定义:将实体和值对象划分为聚合并围绕着聚合定义的边界。选择一个实体作为每个聚合的根,并允许外部对象仅能持有聚合根的引用。作为一个整体来定义聚合的属性和不变量,并将执行职责赋予聚合根或指定的框架机制。
特征:
a 聚合是包含了实体和值对象的一个边界
b 聚合内包含的实体和值对象形成一颗树,只有实体才能作为这颗树的根。这个根称为聚合根,这个实体称为根实体
c 外部对象只允许持有聚合根的引用,以起到边界的控制作用。
d 聚合作为一个完整的领域概念整体,其内部会维护这个领域概念的完整性,体现业务上的不变量约束
e 由聚合根统一对外提供履行该领域概念职责的行为方法,实现内部各个对象之间行为协作。
聚合是一个边界,不是对象。DDD聚合是边界,它的边界内可以只有一个实体对象,也可以包含一些具有关联关系,泛化关系和依赖关系的实体与值对象。
1.7.4 聚合的设计原则
聚合的目的是通过合理的对象边界控制对象之间的关系,在关系内保证对象的一致性与完整性,在边界外作为一个整体参与业务行为的协作。
1.7.4.1 完整性
聚合作为一个受到边界控制的领域共同体,对外由聚合根体现一个统一的概念,对内则管理和维护着高内聚的对象关系。
1.7.4.2 独立性
追求概念的完整性固然重要,但是保证概念的独立性同样重要。
a 既然一个概念是独立的,为何还要依附于别的概念呢?例如,发动机需要被独立跟踪,还需要纳入汽车这个整体概念?
b 一旦这个独立的领域概念被分离出去,原有的聚合是否还具备领域概念的完整性?例如,离开发动机的汽车 概念是否完整?
当聚合边界存在模糊之处时,小聚合显然要优于大聚合。换言之独立性对聚合边界的影响要高于完整性
1.7.4.3 不变量
定义:”在数据变化时必须保持的一致性规则,涉及聚合成员之间的内部关系“。有3个重要概念
a 数据变化
b 内部关系
c 一致
1.7.4.4 一致性
聚合需要保证聚合边界内的所有对象满足不变量约束,其中一个最重要的不变量就是一致性约束,因此一致性也是一种特殊的不变量。
一致性约束可以理解为事务的一致性,即在事务开始前和开始后,数据库的完整性约束没有被破坏。
在单个事务中,只允许一个聚合实例进行修改,由此产生的其他改变必须在单独的事务中完成。如果发现一个事务对聚合实例的修改违背了该原则,需酌情修改
a 合并两个聚合:例如在执行分配问题的操作时,需要修改问题(Issue)状态的同时,生成一条分配记录(Assignment);若Issue和Assignment被设计为两个聚合,根据本原则,可以考虑合并
b 实现最终一致性:例如在执行取款操作时,需要扣除账号(Account)和余额(Balance),并创建一条新的交易记录(Transaction);若Account和Transtraction被设计为两个聚合,而业务操作又要保证两者事务的一致性,可考虑引入事务,实现事务的最终一致。
聚合代表领域逻辑,事务代表技术实现,在确定聚合一致性原则时,可以结合事务特征辅助我们做出判断,但事务对于一致性的实现却不能作为确定聚合边界的绝对标准。
一个聚合必须满足事务的一致性,反之则不然。
1.7.4.5 最高原则
最高原则是:只有聚合根才是访问聚合边界的唯一入口。只有聚合的根才能直接通过数据库查询获取。所有其他内部对象必须通过遍历关联来发现。
1.7.5 聚合的协作
在领域设计模型,聚合才是最小的设计单元。论及聚合的协作,分为两种:
a 聚合根的对象引用
b 聚合根身份标识的引用
关联关系
对象引用往往极具诱惑力,因为它可以使得一个聚合遍历到另一个聚合非常方便,仿佛这才是面向对象设计的正确方式。既然不允许通过对象引用,唯一的方法就是通过身份标识建立关联。
在建立领域设计模型时,我们不能照搬面向对象设计得来的经验,直接通过对象引用建立关联,必须让聚合边界的约束力产生价值。
1.7.5.1 依赖关系
依赖关系产生的耦合要弱于关联关系,也不要求管理被依赖对象的生命周期。只要存在依赖关系的聚合位于同一个限界上下文,就应该允许一个聚合的根实体直接引用另一个聚合的根实体,以形成良高的行为协作。
聚合之间的依赖关系通常分为两种:
a职责的委派
b聚合的创建
1.8 聚合生命周期的管理
所谓生命周期,就是聚合对象从创建开始,在成长过程中经历各种状态的变化,直至最终消亡的过程。在软件系统中,生命周期按存储介质分为两种:内存和硬盘,分别对应对象的实例化和数据的持久化。
1.8.1 工厂
工厂是创建产品对象的一种隐喻。领域驱动设计的工厂并不限于使用哪一种设计模式。一个类或者方法只要封装了聚合对象的创建逻辑,都可以被认为是工厂。主要表现为一些形式:
1. 由被依赖聚合担任工厂
2. 引入专门的聚合工厂
3. 聚合自身担任工厂
4. 消息契约模型或装配器担任工厂
5. 使用构建者组装聚合
1.8.1.1 由被依赖聚合担任工厂
领域驱动设计虽然建议引入工厂创建聚合,但并不要求必须引入专门的工厂类,而是可由一个聚合担任另一个“聚合的工厂”
1.8.1.2 引入专门的聚合工厂
即引入工厂方法模式或抽象工厂模式。
1.8.1.3 聚合自身担任工厂
聚合产品自身也可以承担工厂角色。
1.8.1.4 消息契约模型或装配器担任工厂
设计服务契约时,如果远程服务或应用服务接收到的消息是用于创建的命令请求,则消息契约与领域模型之间的转换操作,实则是聚合的工厂方法。
1.8.1.5 使用构建者组装聚合
构建者模式有两种实现风格:一种是单独定义Builder类,由它对外提供组合构建聚合对象的API。 另一种是由被构建的聚合对象担任近乎Builder的角色,然后将可选的构造参数定义到每个单独的构建方法中,并返回聚合对象自身以形成流畅接口。
1.9 资源库
资源库(resposity)是对数据访问的一种业务抽象。在菱形对称架构中,它是南向网关的端口,可以缓解领域层与外部环境,使领域层变得更为纯粹。
1.9.1 一个聚合一个资源库
管理领域模型对象生命周期的基本单元就是聚合,DDD规定:一个聚合对应一个资源库。如果要访问聚合内的非根实体,也只能通过资源库获得整个聚合后,将根实体作为入口,在内存中访问封装在聚合边界内的非根实体对象。
1.9.2 资源库端口的定义
资源库作为端口,可以视为存取聚合资源的容器。在添加和删除相应类型的对象是,资源库的后台机制负责将对象添加到数据库中,或从数据库中删除。
1.10 领域服务
既然已经有了聚合这一自治的设计单元,并且遵循信息专家模式,其内部的实体与值对象皆承担了与其数据相关的领域行为逻辑,构成了富领域模型,为何还需引入领域服务?
1.10.1 聚合问题
问题一:虽然一些领域行为需要访问聚合封装的信息,它的实现却不稳定,常随着需求的变化而变化。为了满足领域行为的可扩展性,应该将它分配给哪个对象?
问题二:两个聚合之间的协作该有谁负责发起?
问题三:如果聚合不知道端口的存在,那么业务行为与南向网关端口的协作,该由谁来负责?
1.10.2 领域服务的特征
针对领域行为建模时,需要优先考虑使用值对象和实体来封装领域行为,只有确定无法找到合适的对象来承担时,才将该行为建模为领域服务的方法。领域服务时领域设计建模的最后选择。
为了避免领域服务中的方法扩大化,需要控制领域服务的粒度,保证它履行的职责是单一职责。因此在领域服务的命名中必须包含一个动词,这样可以体现领域服务的行为本质。并且领域服务必须是一个无状态的。
1.10.3 领域服务的运用场景
1.10.3.1 第一个问题
虽然一些领域行为需要访问聚合封装的信息,它的实现却不稳定,常随着需求的变化发生变化,为了满足领域行为的可扩展性,需要抽出领域服务。但是信息专家模式仍是DDD建模时需要遵循的首要原则。如果领域行为的变化方向没有与拥有数据的类保持一致,就应该将这一变化的领域行为从所属的聚合中剥离出来,形成领域服务。
1.10.3.2 第二个问题
两个聚合之间的协作该由谁负责发起? 多数时候,一个自治的聚合无法完成一个完整的业务服务,聚合之间需要协作。通常采用职责委派,即一个聚合的根实体作为参数传递给另一个聚合根实体的方法中。那么可以使用领域服务来完成职责委派。
1.10.3.3 第三个问题
聚合应设计为一个稳定的不依赖于任何外部环境的设计单元,那么如何与南向网关端口协作?
领域服务、端口和聚合非常默契履行各自的职责:聚合操作它以及他边界内的数据,履行自治的领域行为;端口通过适配器封装与外部环境交互的行为,又通过抽象隔离对具体技术实现的依赖;领域服务对外提供完整的业务功能,对内负责聚合和端口之间的协调。
总结一下:满足下面情况可以定义领域服务:
1. 与状态无关的领域行为
2. 变化方向与聚合不一致的领域行为
3. 聚合之间协作的领域行为
4. 聚合和端口之间协作的领域行为
1.11 领域事件
1.11.1 建模思想的转变
DDD将对象的状态提升为“一等公民”,赋予它领域事件的省份。其特征如下:
1. 领域事件代表了领域概念
2. 领域事件是已经发生的事实
3. 领域事件是不可变得领域对象
4. 领域事件会基于某个条件而触发
1.11.2 领域事件的定义
领域事件的命名必须清晰的传递领域概念。作为已经发生的事实,事件的命名应采用动词的过去时态,例如订单完成的事件命名为OrderCompleted。
1.11.3 对象建模范式的领域事件
引入领域事件的首要目的就是更好的跟踪实体状态的变更,并在状态发生变更时,通过事件消息的通知完成领域模型对象之间的协作。在接收到状态变更的事件时,参与协作的对象需要依据当前实体的状态变更决定该做出什么样的响应。符合观察者模式的设计思路。
相关推荐
- 迈向群体智能 | 智源发布首个跨本体具身大小脑协作框架
-
允中发自凹非寺量子位|公众号QbitAI3月29日,智源研究院在2025中关村论坛“未来人工智能先锋论坛”上发布首个跨本体具身大小脑协作框架RoboOS与开源具身大脑RoboBrain,可实...
- 大模型对接微信个人号,极空间部署AstrBot机器人,万事不求百度
-
「亲爱的粉丝朋友们好啊!今天熊猫又来介绍好玩有趣的Docker项目了,喜欢的记得点个关注哦!」引言前两天熊猫发过一篇关于如何在极空间部署AstrBot并对接QQ消息平台的文章,不过其实QQ现在已经很少...
- Seata,让分布式事务不再是难题!实战分享带你领略Seata的魅力!
-
终身学习、乐于分享、共同成长!前言Seata是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata将为用户提供了AT、TCC、SAGA和XA事务模式,为用户打造一站式的...
- 常见分布式事务解决方案(分布式事务解决的问题)
-
1.两阶段提交(2PC)原理:分为准备阶段(协调者询问参与者是否可提交)和提交阶段(协调者根据参与者反馈决定提交或回滚)。优点:强一致性,适用于数据库层(如XA协议)。缺点:同步阻塞:所有参与者阻塞...
- 分布式事务:从崩溃到高可用,程序员必须掌握的实战方案!
-
“支付成功,但订单状态未更新!”、“库存扣减后,交易却回滚了!”——如果你在分布式系统中踩过这些“天坑”,这篇文章就是你的救命稻草!本文将手把手拆解分布式事务的核心痛点和6大主流解决方案,用代码实战+...
- 谈谈对分布式事务的一点理解和解决方案
-
分布式事务首先,做系统拆分的时候几乎都会遇到分布式事务的问题,一个仿真的案例如下:项目初期,由于用户体量不大,订单模块和钱包模块共库共应用(大war包时代),模块调用可以简化为本地事务操作,这样做只要...
- 一篇教你通过Seata解决分布式事务问题
-
1 Seata介绍Seata是由阿里中间件团队发起的开源分布式事务框架项目,依赖支持本地ACID事务的关系型数据库,可以高效并且对业务0侵入的方式解决微服务场景下面临的分布式事务问题,目前提供AT...
- Seata分布式事务详解(原理流程及4种模式)
-
Seata分布式事务是SpringCloudAlibaba的核心组件,也是构建分布式的基石,下面我就全面来详解Seata@mikechen本篇已收于mikechen原创超30万字《阿里架构师进阶专题合...
- 分布式事务最终一致性解决方案有哪些?MQ、TCC、saga如何实现?
-
JTA方案适用于单体架构多数据源时实现分布式事务,但对于微服务间的分布式事务就无能为力了,我们需要使用其他的方案实现分布式事务。1、本地消息表本地消息表的核心思想是将分布式事务拆分成本地事务进行处理...
- 彻底掌握分布式事务2PC、3PC模型(分布式事务视频教程)
-
原文:https://mp.weixin.qq.com/s/_zhntxv07GEz9ktAKuj70Q作者:马龙台工作中使用最多的是本地事务,但是在对单一项目拆分为SOA、微服务之后,就会牵扯出分...
- Seata分布式事务框架关于Annotation的SAGA模式分析
-
SAGAAnnotation是ApacheSeata版本2.3.0中引入的功能,它提供了一种使用Java注解而不是传统的JSON配置或编程API来实现SAGA事务模式的声明...
- 分布式事务,原理简单,写起来全是坑
-
今天我们就一起来看下另一种模式,XA模式!其实我觉得seata中的四种不同的分布式事务模式,学完AT、TCC以及XA就够了,Saga不好玩,而且长事务本身就有很多问题,也不推荐使用。S...
- 内存空间节约利器redis的bitmap(位图)应用场景有哪些你知道吗
-
在前面我们分享过一次Redis常用数据结构和使用场景,文章对Redis基本使用做了一个简单的API说明,但是对于其中String类型中的bitmap(位图)我们需要重点说明一下,因为他的作用真的不容忽...
- 分布式事务原理详解(图文全面总结)
-
分布式事务是非常核心的分布式系统,也是大厂经常考察对象,下面我就重点详解分布式事务及原理实现@mikechen本文作者:陈睿|mikechen文章来源:mikechen.cc分布式事务分布式事务指的是...
- 大家平时天天说的分布式系统到底是什么东西?
-
目录从单块系统说起团队越来越大,业务越来越复杂分布式出现:庞大系统分而治之分布式系统所带来的技术问题一句话总结:什么是分布式系统设计和开发经验补充说明:中间件系统及大数据系统前言现在有很多Java技术...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- MVC框架 (46)
- spring框架 (46)
- 框架图 (58)
- bootstrap框架 (43)
- flask框架 (53)
- quartz框架 (51)
- abp框架 (47)
- jpa框架 (47)
- laravel框架 (46)
- express框架 (43)
- springmvc框架 (49)
- 分布式事务框架 (65)
- scrapy框架 (52)
- java框架spring (43)
- grpc框架 (55)
- orm框架有哪些 (43)
- ppt框架 (48)
- 内联框架 (52)
- winform框架 (46)
- gui框架 (44)
- cad怎么画框架 (58)
- ps怎么画框架 (47)
- ssm框架实现登录注册 (49)
- oracle字符串长度 (48)
- oracle提交事务 (47)