文章目录
Pre
概述
基于 DDD 的程序设计,就是将前面设计的领域模型,映射成数据架构中的程序设计,从而通过领域驱动提高软件设计质量。那么,应当怎样进行映射,让领域模型指导程序设计呢?
要将领域模型映射到程序设计,最终都会落实到 3 种类型的对象设计:服务、实体和值对象。
服务、实体与值对象
建领域模型的第一步就是需要区分出服务、实体与值对象。
服务(Service)
服务,标识的是那些在领域对象之外的操作与行为。 在 DDD 中,“服务”通常承担了两种类型的职责:接收用户的请求和执行某些操作。当用户在系统界面中进行一些操作时,就会向系统发送请求。这时,是由“服务”首先去接收用户的这些请求,然后再根据需求去执行相应的方法。在执行这些方法的过程中,“服务”会去操作相应的实体与值对象。最后,当所有操作都完成以后,再将实体或值对象中的数据持久化到数据库中。
譬如,当用户需要下单的时候,就会从前端发起一个“下单”请求。该请求被“订单”Service 接收到,并执行下单的相应操作。在执行过程中,“订单”Service 会对“订单”实体中的数据进行校验,完成各种数据操作,最后将其保存到数据库中。
实体(Entity)
在 DDD 中有这样一类对象,它们拥有唯一标识符,且标识符在历经各种状态变更后仍能保持一致。对这些对象而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至超出软件的生命周期。我们把这样的对象称为实体。
实体,就是那些通过一个唯一标识字段来区分真实世界中的每一个个体的领域对象。例如,在学籍管理系统中的“学员”对象就是一个实体,它通过标识字段“学员编号”将每一个学员进行了区分,通过某个学员编号就能唯一地标识某个学员;并且,这个学员有许多属性,如姓名、性别、年龄等,这些属性也是随着时间不断变化。这样的设计就叫作“实体”。
1. 实体的业务形态
在 DDD 不同的设计过程中,实体的形态是不同的。在战略设计时,实体是领域模型的一个重要对象。领域模型中的实体是多个属性、操作或行为的载体。在事件风暴中,我们可以根据命令、操作或者事件,找出产生这些行为的业务实体对象,进而按照一定的业务规则将依存度高和业务关联紧密的多个实体对象和值对象进行聚类,形成聚合。可以这么理解,实体和值对象是组成领域模型的基础单元。
2. 实体的代码形态
在代码模型中,实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。在 DDD 里,这些实体类通常采用充血模型,与这个实体相关的所有业务逻辑都在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现。
3. 实体的运行形态
实体以 DO(领域对象)的形式存在,每个实体对象都有唯一的 ID。我们可以对一个实体对象进行多次修改,修改后的数据和原来的数据可能会大不相同。但是,由于它们拥有相同的 ID,它们依然是同一个实体。比如商品是商品上下文的一个实体,通过唯一的商品 ID 来标识,不管这个商品的数据如何变化,商品的 ID 一直保持不变,它始终是同一个商品。
4. 实体的数据库形态
与传统数据模型设计优先不同,DDD 是先构建领域模型,针对实际业务场景构建实体对象和行为,再将实体对象映射到数据持久化对象。
在领域模型映射到数据模型时,一个实体可能对应 0 个、1 个或者多个数据库持久化对象。大多数情况下实体与持久化对象是一对一。在某些场景中,有些实体只是暂驻静态内存的一个运行态实体,它不需要持久化。比如,基于多个价格配置数据计算后生成的折扣实体。
而在有些复杂场景下,实体与持久化对象则可能是一对多或者多对一的关系。比如,用户 user 与角色 role 两个持久化对象可生成权限实体,一个实体对应两个持久化对象,这是一对多的场景。再比如,有些场景为了避免数据库的联表查询,提升系统性能,会将客户信息 customer 和账户信息 account 两类数据保存到同一张数据库表中,客户和账户两个实体可根据需要从一个持久化对象中生成,这就是多对一的场景。
值对象
《实现领域驱动设计》一书中对值对象的定义:通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。在 DDD 中用来描述领域的特定方面,并且是一个没有标识符的对象,叫作值对象
值对象,代表的是真实世界中那些一成不变的、本质性的事物,这样的领域对象叫作 “值对象”,如地理位置、行政区划、币种、行业、职位等。
也就说,值对象描述了领域中的一件东西,这个东西是不可变的,它将不同的相关属性组合成了一个概念整体。当度量和描述改变时,可以用另外一个值对象予以替换。它可以和其它值对象进行相等性比较,且不会对协作对象造成副作用。
简单来说,值对象本质上就是一个集。那这个集合里面有什么呢?若干个用于描述目的、具有整体概念和不可修改的属性。那这个集合存在的意义又是什么?在领域建模的过程中,值对象可以保证属性归类的清晰和概念的完整性,避免属性零碎.
举个例子
人员实体原本包括:姓名、年龄、性别以及人员所在的省、市、县和街道等属性。这样显示地址相关的属性就很零碎了对不对?现在,我们可以将“省、市、县和街道等属性”拿出来构成一个“地址属性集合”,这个集合就是值对象了
1. 值对象的业务形态
值对象是 DDD 领域模型中的一个基础对象,它跟实体一样都来源于事件风暴所构建的领域模型,都包含了若干个属性,它与实体一起构成聚合。
我们不妨对照实体,来看值对象的业务形态,这样更好理解。本质上,实体是看得到、摸得着的实实在在的业务对象,实体具有业务属性、业务行为和业务逻辑。而值对象只是若干个属性的集合,只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑。值对象的属性集虽然在物理上独立出来了,但在逻辑上它仍然是实体属性的一部分,用于描述实体的特征。
在值对象中也有部分共享的标准类型的值对象,它们有自己的限界上下文,有自己的持久化对象,可以建立共享的数据类微服务,比如数据字典。
2. 值对象的代码形态
值对象在代码中有这样两种形态。如果值对象是单一属性,则直接定义为实体类的属性;如果值对象是属性集合,则把它设计为 Class 类,Class 将具有整体概念的多个属性归集到属性集合,这样的值对象没有 ID,会被实体整体引用。
我们看一下下面这段代码,person 这个实体有若干个单一属性的值对象,比如 Id、name 等属性;同时它也包含多个属性的值对象,比如地址 address。
3. 值对象的运行形态
实体实例化后的 DO 对象的业务属性和业务行为非常丰富,但值对象实例化的对象则相对简单和乏味。除了值对象数据初始化和整体替换的行为外,其它业务行为就很少了。
值对象嵌入到实体的话,有这样两种不同的数据格式,也可以说是两种方式,分别是属性嵌入的方式和序列化大对象的方式。
引用单一属性的值对象或只有一条记录的多属性值对象的实体,可以采用属性嵌入的方式嵌入。引用一条或多条记录的多属性值对象的实体,可以采用序列化大对象的方式嵌入。比如,人员实体可以有多个通讯地址,多个地址序列化后可以嵌入人员的地址属性。值对象创建后就不允许修改了,只能用另外一个值对象来整体替换。
如果对这两种方式不够了解,可以看看下面的例子。
案例 1:以属性嵌入的方式形成的人员实体对象,地址值对象直接以属性值嵌入人员实体中。
案例 2:以序列化大对象的方式形成的人员实体对象,地址值对象被序列化成大对象 Json 串后,嵌入人员实体中。
4. 值对象的数据库形态
DDD 引入值对象是希望实现从“数据建模为中心”向“领域建模为中心”转变,减少数据库表的数量和表与表之间复杂的依赖关系,尽可能地简化数据库设计,提升数据库性能。
如何理解用值对象来简化数据库设计呢?
传统的数据建模大多是根据数据库范式设计的,每一个数据库表对应一个实体,每一个实体的属性值用单独的一列来存储,一个实体主表会对应 N 个实体从表。而值对象在数据库持久化方面简化了设计,它的数据库设计大多采用非数据库范式,值对象的属性值和实体对象的属性值保存在同一个数据库实体表中。
举个例子,还是基于上述人员和地址那个场景,实体和数据模型设计通常有两种解决方案:第一是把地址值对象的所有属性都放到人员实体表中,创建人员实体,创建人员数据表;第二是创建人员和地址两个实体,同时创建人员和地址两张表。
第一个方案会破坏地址的业务涵义和概念完整性,第二个方案增加了不必要的实体和表,需要处理多个实体和表的关系,从而增加了数据库设计的复杂性。
那到底应该怎样设计,才能让业务含义清楚,同时又不让数据库变得复杂呢?
我们可以综合这两个方案的优势,扬长避短。在领域建模时,我们可以把地址作为值对象,人员作为实体,这样就可以保留地址的业务涵义和概念完整性。而在数据建模时,我们可以将地址的属性值嵌入人员实体数据库表中,只创建人员数据库表。这样既可以兼顾业务含义和表达,又不增加数据库的复杂度。
值对象就是通过这种方式,简化了数据库设计,总结一下就是:在领域建模时,我们可以将部分对象设计为值对象,保留对象的业务涵义,同时又减少了实体的数量;在数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化数据库设计。
另外,也有 DDD 专家认为,要想发挥对象的威力,就需要优先做领域建模,弱化数据库的作用,只把数据库作为一个保存数据的仓库即可。即使违反数据库设计原则,也不用大惊小怪,只要业务能够顺利运行,就没什么关系。
5. 值对象的优势和局限
值对象是一把双刃剑,它的优势是可以简化数据库设计,提升数据库性能。但如果值对象使用不当,它的优势就会很快变成劣势。“知彼知己,方能百战不殆”,你需要理解值对象真正适合的场景。
值对象采用序列化大对象的方法简化了数据库设计,减少了实体表的数量,可以简单、清晰地表达业务概念。这种设计方式虽然降低了数据库设计的复杂度,但却无法满足基于值对象的快速查询,会导致搜索值对象属性值变得异常困难。
值对象采用属性嵌入的方法提升了数据库的性能,但如果实体引用的值对象过多,则会导致实体堆积一堆缺乏概念完整性的属性,这样值对象就会失去业务涵义,操作起来也不方便。
所以,你可以对照着以上这些优劣势,结合你的业务场景,好好想一想了。那如果在你的业务场景中,值对象的这些劣势都可以避免掉,那就请放心大胆地使用值对象吧。
实体和值对象的区分
实体和值对象是领域驱动设计(DDD)中的两个核心概念,它们在领域模型中扮演着不同的角色,并且在设计、代码实现和数据库映射中有着不同的表现。
在 DDD 中,对实体与值对象进行了严格的区分。可变性是实体的特点,而不变性则是值对象的本质。例如,北京是一个城市,架构师是一个职务,人民币是一个币种,这些事物的特性是永远不变的。
在实际项目中,我们可以根据业务需求的不同,灵活选用实体还是值对象。
比如,在线订餐系统中,根据业务需求的不同,菜单既可以设计成实体,也可以设计成值对象。例如,“宫保鸡丁”是一个菜品,如果将其按照值对象设计,则整个系统中“宫保鸡丁”只有一条记录,所有饭店的菜单如果有这道菜,都是引用的这条记录;如果按照实体进行设计,则是认为每个饭店的“宫保鸡丁”都是不同的,比如每个饭店的“宫保鸡丁”的价格都是不尽相同的。因此,将其设计成有多条记录、有各自不同的 ID,每个饭店都是使用自己的“宫保鸡丁”。
主要区别如下:
1. 标识符(Identity)
- 实体:实体拥有唯一的标识符(ID),这个标识符在实体的整个生命周期中保持不变。即使实体的属性发生变化,只要标识符不变,它仍然是同一个实体。例如,用户实体通过用户ID来标识,无论用户的姓名、年龄等信息如何变化,用户ID始终不变。
- 值对象:值对象没有唯一的标识符,它通过其属性值来识别。值对象的相等性是通过其属性值来比较的,而不是通过ID。例如,地址值对象通过省、市、街道等属性值来识别,而不是通过一个唯一的ID。
2. 可变性(Mutability)
- 实体:实体是可变的,它的属性可以随着时间的推移而发生变化。例如,用户的年龄、地址等信息可能会发生变化,但用户ID保持不变。
- 值对象:值对象是不可变的,一旦创建,它的属性值就不能被修改。如果需要改变值对象,通常会用一个新的值对象来替换它。例如,地址值对象一旦创建,就不能修改其中的省、市等信息,只能用一个新地址来替换。
3. 生命周期
- 实体:实体的生命周期通常较长,可能会跨越系统的多个操作和状态变化。实体的标识符在整个生命周期中保持不变。
- 值对象:值对象的生命周期通常较短,它通常作为实体的一部分存在,随着实体的变化而被替换或丢弃。
4. 业务逻辑
- 实体:实体通常包含丰富的业务逻辑和行为,它不仅仅是一个数据的容器,还包含了与业务相关的操作和规则。例如,用户实体可能包含修改用户信息、验证用户权限等业务逻辑。
- 值对象:值对象通常不包含复杂的业务逻辑,它主要用于描述实体的某些特征或属性。值对象的行为通常仅限于数据初始化和简单的相等性比较。
5. 数据库映射
- 实体:实体通常与数据库中的表一一对应,每个实体对应一个数据库表,实体的属性对应表中的列。在某些复杂场景下,实体可能与多个表相关联。
- 值对象:值对象通常不单独映射到数据库表,而是作为实体的一部分嵌入到实体表中。值对象的属性可以直接存储在实体表中,或者通过序列化的方式存储为一个大对象。
6. 设计目的
- 实体:实体的设计目的是为了表示具有唯一标识和生命周期的业务对象,它关注的是对象的延续性和标识。
- 值对象:值对象的设计目的是为了将一组相关的属性组合成一个概念整体,它关注的是属性的不可变性和整体性。
小结
- 实体:具有唯一标识、可变、生命周期长、包含丰富业务逻辑、通常与数据库表一一对应。
- 值对象:没有唯一标识、不可变、生命周期短、主要用于描述属性、通常嵌入到实体中存储。
在实际设计中,选择将领域对象设计为实体还是值对象,取决于具体的业务场景和需求。理解它们的区别和应用场景,有助于更好地进行领域建模和系统设计。
贫血模型 vs 充血模型
服务、实体与值对象是领域驱动设计的基本元素。然而,要将业务领域模型最终转换为程序设计,还要加入相应的设计。通常,将业务领域模型转换为程序设计,有两种设计思路:贫血模型与充血模型。
贫血模型与充血模型
- 2004 年,软件大师 Eric Evans 发表了他的不朽著作《领域驱动设计》。虽然已经过去十多年了,这本书直到今天依然对我们有相当大的帮助。
- 接着,另一位软件大师 Martin Fowler 在自己的博客中提出了“贫血模型”的概念。这位“马大叔”有一个非常大的特点,那就是软件行业中各种名词都是他发明的,包括如今业界影响巨大的软件重构、微服务,也是他的杰作。
然而,马大叔在提出“贫血模型”的时候,却将其作为反模式提出来批评:所谓的“贫血模型”,就是在软件设计中,有很多的 POJO(Plain Ordinary Java Object)对象,它们除了有一堆 get/set 方法,几乎没有任何业务逻辑。这样的设计被称为“贫血模型”。
贫血模型
如上图所示,在领域模型中有 VIP 会员的领域对象,该对象除了有一堆属性以外,还有“会员打折”“会员福利”“会员特权”等方法。如果将该领域模型按照贫血模型进行设计,就会设计一个 VIP 会员的实体对象与 Service,
- 实体对象包含该对象的所有属性,以及这些属性包含的数据;
- 然后,将所有的方法都放入 Service 中,在调用它们的时候,必须将领域对象作为参数进行传输。
这样的设计,将领域对象中的这些方法,以及这些方法在执行过程中所需的数据,割裂到两个不同的对象中,打破了对象的封装性。它会带来什么问题呢?
如上图所示,在领域模型中的 VIP 会员通过继承分为了“金卡会员”与“银卡会员”。如果将该领域模型按照贫血模型进行设计,
- 会设计出一个“金卡会员”的实体对象与 Service,
- 同时设计出一个“银卡会员”的实体对象与 Service。
“金卡会员”的实体对象应当调用“金卡会员”的 Service,如果将“金卡会员”的实体对象去调用了“银卡会员”的 Service,系统就会出错。所以,除了进行以上设计以外,
- 还需要有一个客户程序去判断,当前的实体对象是“金卡会员”还是“银卡会员”?这时,系统变更就变得没有那么灵活了。
比如,现在需要在原有基础上,再增加一个“铂金会员”,那么不仅要增加一个“铂金会员”的实体对象与 Service,还要修改客户程序的判断,系统变更成本就会提高。
充血模型
针对贫血模型的问题,马大叔提出了“充血模型”的概念。所谓“充血模型”,就是将领域模型的原貌直接转换为程序中领域对象的设计。这时,各种业务操作就不再在“服务”中实现了,而是在领域对象中实现。如图所示,在程序设计时,既有父类的“VIP 会员”,又有子类“金卡会员”与“银卡会员”。
但充血模型与贫血模型不同的是:
-
那些在领域对象中的方法也同样保留到了程序设计的实体对象中,这样,通过继承,虽然“金卡会员”与“银卡会员”都有“会员打折”,但“金卡会员”的“会员打折”与“银卡会员”的“会员打折”是不一样的;
-
虽然在充血模型中也有 Service,里面也有“会员打折”“会员福利”“会员特权”等方法,但是充血模型的 Service 只干一件非常简单的事,那就是接收到用户的请求后,就直接去调用实体对象中的相应方法,其他的什么都不干。
这样,“VIP 会员”Service 不需要去关注现在调用的是“金卡会员”还是“银卡会员”,它只需要去调用“会员打折”就行了:
-
如果当前拿到的是“金卡会员”,就是执行“金卡会员”的“会员打折”;
-
如果当前拿到的是“银卡会员”,就是执行“银卡会员”的“会员打折”;
-
如果要再增加一个“铂金会员”,就只需要写一个“铂金会员”的子类,重写“会员打折”方法,而“VIP 会员”Service 不需要做任何修改,变更的维护成本就大大降低了。
两种设计思路的优劣比较
采用充血模型的设计,有诸多的好处:
-
它保持了领域模型的原貌,领域模型什么样,就直接转换成程序的设计,这样,当领域模型在随着业务变更而频繁甚至大幅度调整时,可以比较直接地映射成程序的变更,代码修改起来比较直接;
-
如以上案例所述,充血模型保持了对象的封装性,使得领域模型在面临多态、继承等复杂结构时,易于变更。
充血模型在理论上非常优雅,然而在工程实践上却不尽人意。而贫血模型虽然从表面上看简单粗暴,但在工程实践上依然有许多优异的特性,主要体现在以下 3 个方面。
1. 贫血模型比充血模型更加简单易行
充血模型是将领域模型的原貌直接映射成了程序设计,因此在程序设计时需要增加更多的诸如仓库、工厂的组件,对设计能力与架构提出了更高的要求。
譬如,现在要设计一个订单系统,在领域建模时,每个订单需要有多个订单明细,还要对应相关的客户信息、商品信息。
因此,
- 在装载一个订单时,需要同时查出它的订单明细,以及对应的客户信息、商品信息,这些需要有强大的订单工厂进行装配;
- 装载订单以后,还需要放到仓库中进行缓存,需要订单仓库具备缓存的能力;
- 此外,在保存订单的时候,还需要同时保存订单和订单明细,并将它们放到一个事务中。
所有这些都需要强有力的技术平台的支持。
相反,贫血模型就显得更加贫民化。在贫血模型中,MVC 层直接调用 Service,Service 通过DAO进行数据访问。在这个过程中,每个 DAO 都只查询数据库中的某个表,然后直接交给 Service 去使用,去完成各种处理。
以订单系统为例,订单有订单 DAO,负责查询订单;订单明细有订单明细 DAO,负责查询订单明细。它们查询出来以后,不需要装配,而是直接交给订单 Service 使用。在保存订单时,订单 DAO 负责保存订单,订单明细 DAO 负责保存订单明细。它们都是通过订单 Service 进行组织,并建立事务。贫血模型不需要仓库,不需要工厂,也不需要缓存,一切都显得那么简单粗暴但一目了然。
2. 充血模型需要具备更强的设计与协作能力
充血模型的设计实现给开发人员提出了更高的能力要求,需要具有更强的 OOA/D(面向对象分析/设计) 能力、分析业务、业务建模与设计能力。
譬如,在订单系统这个案例中,开发人员要先进行领域建模,分析清楚该场景中的订单、订单明细、用户、商品等领域对象的关联关系;还要分析各个领域对象在真实世界中都有什么行为,对应到软件设计中都有什么方法,在此基础上再进行设计开发。
同时,充血模型需要有较强的团队协作能力。
比如,在该场景中,当订单在进行创建时,需要对用户以及用户地址的相关信息进行查询。此时,订单 Service 不能直接去查询用户和用户地址的相关表,而是去调用用户 Service 的相关接口,由用户 Service 去完成对用户相关表的查询。这时候,开发订单模块的团队,需要向开发用户模块的团队提出接口需求。
与充血模型相比,贫血模型就比较简单与直接。所有业务处理过程都交给 Service 去完成。在业务处理过程中,需要哪些表的数据,就去调用相应的 DAO:需要订单就找订单 DAO;需要用户就找用户 DAO;需要商品就找商品 DAO。程序简单就易于理解,日后维护起来也比较容易。
总之,充血模型就有一种贵族气质,“讲究人”——昂贵而高雅;贫血模型就是“草根”——简单而直接。
3. 贫血模型更容易应对复杂的业务处理场景
充血模型在进行设计时,是将所有的业务处理过程在领域对象的相应方法中实现的。
这样的设计,如果业务处理过程比较简单,还可以从容应对;但如果是面对非常复杂的业务处理场景时,就有一些力不从心。
在这些复杂的业务处理场景中,如果采用贫血模型,可以将复杂的业务处理场景,划分成多个相对独立的步骤;然后将这些独立的步骤分配给多个 Service 串联起来执行。这样,各个步骤就是以一种松耦合的形式串联地组织在一起,以领域对象作为参数在各个Service 中进行传递。
在这样的设计中,领域对象既可以作为各个方法调用的输入,又可以作为它们的输出。
比如,在上图的案例中,领域对象作为参数首先调用 ServiceA;调用完以后将结果数据写入领域对象的前 5 个字段,传递给 ServiceB;ServiceB 拿到领域对象以后,既可以作为输入去读取前 5 个字段,又可以作为输出将执行结果写入中间 5 个字段;最后,将领域对象传递给 ServiceC,执行完操作以后去写后面 5 个字段;当所有字段都写入完成以后,存入数据库,完成所有操作。
在这个过程中,如果日后需要变更,要增加一个处理过程,或者去掉一个处理过程,再或者调整它们的执行顺序,都是比较容易的。这样的设计要求处理过程必须在领域对象之外,在 Service 中实现。然而,如果采用的是充血模型的设计,就必须要将所有的处理过程都写入这个领域对象中去实现,无论这些处理过程有多复杂。这样的设计势必会加大日后变更维护的成本。
所以,不论是贫血模型还是充血模型,它们各有优缺点,到底应当采用贫血模型还是充血模型,争执了这么多年,但我认为它们并不是熊掌和鱼的关系,我们应当把它们结合起来,取长补短,合理利用。关键是要先弄清楚它们的差别,也就是业务逻辑应当在哪里实现:贫血模型的业务逻辑在 Service 中实现,但充血模型是在领域对象中实现。清楚了这一点,在今后的软件设计时,可以将那些需要封装的业务逻辑放到领域对象中,按照充血模型去设计;除此之外的其他业务逻辑放到 Service 中,按照贫血模型去设计。
那么,哪些业务逻辑需要封装起来按照充血模型设计呢?这个仁者见仁智者见智,总结了以下几个方面的内容。
-
如前所述,如果在领域模型中出现了类似继承、多态的情况,则应当将继承与多态的部分以充血模型的形式在领域对象中实现。
-
如果在软件设计的过程中需要将一些类型或者编码进行转换,则将转换的部分封装在领域对象中。例如,一些布尔类型的字段,在数据库中是没有布尔类型的,不同的人习惯不同,有的人习惯采用 0 和 1,有的人习惯用 Y 和 N,或者 T 和 F,这样就会给上层开发人员诸多的困惑,到底哪些字段是 Y 和 N,哪些是 T 和 F。这时就可以将它们封装在领域对象中,然后转换为布尔类型展现给上层开发,按充血模型来设计。
-
希望在软件设计中能更好地表现领域对象之间的关系。比如,在查询订单的时候想要显示每个订单对应的用户,以及每个订单包含的订单明细。这时,除了要将领域模型中的关系体现在领域对象的设计外,还需要有仓库与工厂的支持。如装载订单时需要同时查询订单和订单明细,并通过订单工厂装配;查询订单以后需要通过工厂补填相应的用户与明细。
-
最后一种情况被称为聚合,也就是在真实世界中那些代表整体与部分的事物。比如,在订单中有订单和订单明细,一个订单对应多个订单明细。从业务关系来说,它们是整体与部分的关系,订单明细是订单的一个部分,没有了这张订单,它的订单明细就没有任何意义了。这时,我们在操作订单的时候,就应当将对订单明细的操作封装在订单对象中,按照充血模型的形式进行设计。
总结
基于 DDD 的程序设计,领域模型分析只是软件需求分析的中间过程,它最终需要落地到程序设计。领域模型的最终落地是三种类型的对象:服务、实体与值对象,而设计思路有两种:贫血模型与充血模型。通过这样的落地,领域模型就能很好地指导程序开发,提高设计质量。
在 DDD 落地的过程中,不必过于纠结到底是实体还是值对象,应当将更多的精力集中于对业务的分析与理解。同时,将贫血模型与充血模型结合起来,取长补短、合理编码。
然而,领域模型的落地还有诸多难题需要解决。因此,接下来看一看 DDD 的聚合、仓库与工厂及其设计思路。
评论记录:
回复评论: