认识领域模型元素

DDD里的模型元素

  1. 值对象(Value Object)
  2. 实体(Entity)
  3. 聚合(Aggregate)
  4. 资源库(Repository)
  5. 工厂(Factory)
  6. 领域事件(Domain Event)
  7. 领域服务(Domain Service)
  8. 应用服务(Application Service)- 应用层

领域模型的职责分类

  • 表示模型的是:实体、值对象、领域服务。这意味着所有领域逻辑都应该在这三种对象里。后续引入的领域事件也是一种领域逻辑。
  • 聚合是为了进一步让领域逻辑更内聚,起到边界保护的作用。怎么保护呢?详见下面聚合设计原则部分。
  • 工厂和资源库是为了管理领域对象的生命周期。工厂负责对象的创建(在复杂创建逻辑下才需要)。资源库负责聚合的加载、添加、修改、删除。
  • 应用服务处于应用层,对领域逻辑编排、封装之后对上层接口层暴露。一次编排就是一个用户用例。

模型和服务是指什么?

模型只是个逻辑概念,模型的数据方面由实体、值对象、领域消息来承载。领域行为包括实体里的行为、值对象里的行为、领域服务里的行为。

服务是个统称,在分层架构里不同的分层里对应不同的服务。领域层里有领域服务(domain service)、应用层里有应用服务(application service)、基础设施层里有基础设施服务、与外部系统交互的服务(open host services)。

深入各领域模型元素

深入解释各领域元素设计原则,从元素定义,设计原则,设计案例三个部分展开。

实体

实体定义:

简单说实体是具有属性和行为的对象。但这个定义不足以说明实体本质。因为值对象也有属性和行为。

实体的本质特征是具有唯一标识和具有业务连续性变化。业务连续性变化指随着时间推移实体的某些属性会发生变化,比如订单的状态随着订单履约流程随时变化,而订单号这个唯一标识是用来追踪这一变化的。

唯一标识

  • 标识是唯一的,用来对外沟通的手段。就像中国公民具有身份证,订单具有订单号一样,实体有自己的唯一标识,可以是系统内部生成的也可以是系统外部传入的,可以由业务规则生成,也可以无业务含义。
  • 标识是不会变的:实体的其他属性可能会变,但唯一标识在实体的整个生命周期都不会变。订单号不会随着订单的创建、支付、发货、关闭而改变。

属性

属性是实体的特征表现,具体分为基本属性和组合属性。

  • 基本属性:是那些由开发语言内置的基本类型就能表示的属性如name、description字段往往是个字符串。
  • 组合属性:不是一个基本类型就能表示出来的特征,比如重量Weight,既有数字也有单位,2kg和2g。组合属性会被设计为值对象。

行为

相对于属性代表的是实体的静态特征,行为是实体的动态特征。实体的行为有:

  • 变更属性的行为比如更改描述
  • 对自身属性加工计算的行为
  • 跟别的属性协作的行为。需要依赖别的实体信息来做出决策。这一类行为的设计需要特别注意,和协作的对象要分清职责范围。

注意:实体的创建、增删改查不属于实体自身的行为,是实体的生命周期管理,需要交由工厂和资源库来负责。

实体设计原则

  1. 把内聚性更强的属性尽量设计为单独的值对象,甚至包括没有唯一标识的”实体”。
  2. 需要遵循”信息专家“原则,避免实体变为贫血对象。实体不能只是拥有数据,也要具备数据对应的领域行为。应该由实体做的事情必须在实体内定义领域行为。
  3. 确保实体的创建方法返回的是一个有效的实体 - 接受必要的、经过校验的入参、如有集合成员属性需要初始化好
  4. 实体里须抛出特定业务类异常
  5. 如果实体逻辑里需要别的实体
    1. 如果是依赖本聚合内的实体,可以直接引用
    2. 如果是依赖本聚合外的实体,要么通过参数传入,要么把逻辑放置领域服务,建议通过领域服务来实现跨聚合的逻辑。

值对象

值对象是对实体的描述。一个值对象只描述一个实体,少数情况一个值对象是个通用的描述,可能会用来描述多个实体。

注意:值对象也有自己的行为,尽可能把属于值对象自己的行为放到值对象里。

什么时候该定义为值对象?

  1. 无唯一标识】没有唯一标识的对象都是值对象。这也值对象和实体的本质区别。
    1. 甚至订单项OrderLine也是一个值对象。对用户而言只关心订单里的哪个产品、数量多少、价格多少等信息,不关心第几个订单项。只是订单项可以作为单独表存储,从数据存储的角度要求这个表要有个唯一id,但这只是个技术id,不是唯一标识。
  2. 验证逻辑】有自我验证逻辑的属性,这些属性的验证逻辑如果放到实体里,会导致职责不够清晰,实体容易变的膨胀。
    1. email属性,需要对是否一个有效的email地址做验证
    2. 收发货地址,需要验证地址格式有效性等
  3. 计算逻辑】有自我计算、换算逻辑的属性:
    1. Weight重量属性,两个重量间的四则运算逻辑应归属于Weight值对象
    2. Money金额属性,金额包含数量和货币单位,Money字段间有换算逻辑
  4. 【同类属性】多个属性在描述一个领域特征
    1. 电话号码、座机、传真都在描述联系方式

值对象如何存储?

存储到哪?如果够简单就随实体表一起保存,如果复杂可以有单独的表存储(此时单独表里每行记录的唯一id只是为了技术手段使用,在聚合外这个id没有任何领域含义)

谁来触发存储?所属的实体

实体和值对象的区别?

是否具备唯一标识是实体和对象的本质区别。实体一旦创建会有后续状态变化,每个变化都需要通过唯一标识来追踪。

领域概念显性化(Domain Primitive)

在完成了实体和值对象的设计后,有的时候会发现有些概念其实在领域上是存在的,但设计和代码里没有Class来体现,可能仅仅是一个基本类型参数加上散落的对该参数的判断检验逻辑,这个时候还需要思考应该把这个概念显性化,定义专门的Class并包含相应逻辑,入出参以相应Class为类型。

比如联系人实体的电话号码,它应该成为一个领域概念出现,电话号码的校验不属于其他实体或者值对象,应该属于TelPhone自己。有些书籍把这类概念叫做domain primitive,就好比编程语言有基本类型primitive type和自定义高级类型一样。但本质上这也是一种值对象。

领域概念显性化的好处:1. 有意义的领域概念。2. 入出参具备强类型检查。3. 相关逻辑典型的是校验、内部格式规整等具有合适归属地。

case study: 联系人实体,包含属性电话号码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ContactEntity {
String telPhone; // Good ->: TelPhone telPhone;
}
// Bad,代码散落没有抽取,职责不合理
if (telPhone == null && isValidTelPhone(telPhone)) {
throw new ValidationException();
}
// Bad,职责不合理
ValidationUtils.validateTelPhone(telPhone);
// Bad,注解方式只适合简单校验比如判断null
@NotNull @Pattern(regexp = "^0?[1-9]{2,3}-?\\d{8}$") String telPhone

//Good,在相应实体的校验中,委托给了telPhone值对象自己校验
contactEntity.validate() -> telPhone.validate()

ContactEntity findByPhone(String telPhone); // Good ->: ContactEntity findByPhone(TelPhone telPhone);

聚合

引入聚合是为了划分对象之间的边界,聚合即DDD引入的第四重边界。一个聚合至少有一个实体,承担最主要概念的实体就是聚合根,也叫根实体。聚合边界之内需要保持领域概念的完整性,边界之外聚合需要有独立性,需要跟别的聚合协作。对象之间是否应该构成一个聚合是DDD设计里的难点和重点。独立性和完整性是聚合的两个最重要特性。

聚合的独立性

根实体是访问聚合边界的唯一入口。反过来讲若需要独立访问一个实体,则它只能作为聚合根。如果一个实体不是根实体,但同时真的需要被外界直接访问到,那么这个实体不应该在这个聚合中,应该独立成新的聚合。

  • case study】比如账户和账户交易记录作为一个聚合,账户设计为根实体。但业务需要不通过账户而单独查询账户交易记录,比如所有账户的交易记录。如果账户交易记录不独立为一个聚合,则业务被迫需要每次通过账户来访问账户交易记录,这显然是不合理的。

聚合的完整性

一个聚合代表一个完整的领域概念。完整性可以从几个方面理解:

  1. 【共享生命周期】就像面向对象设计的合成关系composite,对象间的生命周期一致,需要同时被创建,同时被销毁。组成一个聚合的其他实体的操作由根实体来协调
    • case study】订单Order与订单项OrderLine,订单项不能离开订单而存在,订单如果没有订单项包含的信息就不是一个完整订单。同时用户只关心订单这个领域概念,用户可以不了解订单项这个领域概念,对用户而言只关心产品、产品数量、产品价格等信息。因此满足合成关系的对象往往会被设计为一个聚合。
  1. 【保证业务规则约束】施加在聚合内部各对象之上的各种业务约束需要满足,聚合的自我完备性需要保证包括数据、行为、约束
    • case study】采购订单中添加和删除订单项,必须满足多个订单项金额之和不能超过订单的审批额度。如果订单和订单项不在一个聚合,这个业务规则很难得到保证
  1. 【满足事务一致性】聚合通常被认为是一个事务的边界。也就是说聚合内的根实体、其他实体、值对象数据应该保持在一个事务边界内。如果不一致说明聚合边界设计存在疑问。加载和修改聚合就是一个事务单元。事务虽然是技术实现的范畴,但可以反过来验证聚合设计是否合理
    • case study】如果订单和订单项是一个聚合,则订单和订单项的增删改必须满足一致性。如果分属不同的表存储,则订单表或者订单项表的插入失败会导致整个插入操作失败。当然事务的实际执行层面是聚合对应的资源库的职责。

聚合设计原则

  • 【独立性】只有聚合根才是访问聚合边界的唯一入口
  • 【完整性】尽量保持聚合领域概念的完整性
  • 【访问原则】聚合之间应通过聚合根的身份标识进行引用,聚合内部的实体间可以通过对象引用
    • 其他聚合需要加载一个聚合时(往往通过领域服务/应用服务协调),必须通过目标聚合的资源库,并返回聚合的完整内容,但资源库可以有支持多种加载条件的接口。注意:直接查询类:是否可以通过资源库返回聚合的部分信息?有些实施DDD的做法是查询类不走领域层,用类似DAO的方式而不是资源库方式。我建议我们保持领域完整性,都走资源库查询,而且每个查询返回聚合整体。在领域层或者应用层做信息裁剪
    • 新增一个聚合必须调用根实体必要的校验和领域逻辑,并传递给资源库一个完整聚合
    • 更新一个聚合必须调用根实体必要的校验和领域逻辑,并传递给资源库一个完整聚合。注意:资源库的实现层面可以做到局部更新
  • 【聚合协作】同一个BC的聚合间协作可以采用本地事务保持一致性,跨BC(即跨微服务)的聚合协作通过基于消息机制的最终一致性
  • 【聚合与资源库】不要在聚合中使用资源库
  • 【聚合粒度】在上述规则满足的前提下,聚合设计尽量小,小聚合带来小的事务粒度、有更好的性能

实体、值对象、聚合设计过程

  1. 理顺领域对象图(对象图是领域建模的分析阶段的产出物)
  2. 把领域对象设计为值对象,实体。值对象依附于某个实体。
  3. 基于领域完整性和独立性识别出聚合、定义聚合根。注意:当完整性和独立性产生矛盾时,优先满足独立性
    1. 本着聚合粒度尽量小原则,一开始可以把每个实体当作一个聚合。
    2. 合并实体。判断聚合是否足够完整?判断实体是否有独立性需求?如果实体A和实体B生命周期一致,同时A没有被外界独立访问的需求,则合并A和B为一个聚合。如果实体A有被独立访问的需求,则A作为单独聚合。
    3. 走查聚合设计粒度,综合考虑技术设计因素

实体值对象聚合设计案例:

case study #1

1
2
3
计算机与cpu、计算机与键盘
1. 计算机与cpu属于对象的合成关系,设计为一个聚合是合理的。计算机离开了cpu就不能称作为一台计算机,一台计算机里的cpu也不会单独被外界访问。适合设计为一个聚合,计算机为实体,cpu为值对象
2. 计算机与键盘属于对象的组合关系,这个依赖很弱,各自都可以单独存在,适合设计为两个聚合

case study #2

1
2
3
4
5
6
以银行的取款为例:当储户账户发起取款操作时,需要扣除账户Account的余额Balance,同时创建一条新的交易记录Transaction,以便于银行对账,并支持储户的交易查询功能。
1. 根据实体和值对象设计原则,Account和Transaction应该是实体,Balance是个典型的值对象(不存在唯一标识,对实体的一种描述)。
2. Account和Balance应该为一个聚合,Account是聚合根,没有争议。
3. 从聚合完整性考虑,账户余额扣除成功和取款的交易记录创建成功需要保持一致,否则会导致数据不一致。所以聚合的边界应该包括Account、Balance与Transaction这三个实体。
4. 从聚合独立性考虑,对于Transaction来说,由于储户可以执行交易查询功能,这意味着调用者可以绕开Account这个聚合根而单独查询Transaction。因此这里的Transaction需要具有独立性,应该单独为它建立一个聚合。
5. 这样的设计就打破了Account和Transaction的数据一致性问题,遵照聚合的独立性原则高于完整性原则这条,这个数据一致性问题需要用别的方案来解决。比如可以通过领域服务来协调两个聚合。

case study #3

1
2
3
考试系统的问题Question和答案Answer,知识问答论坛的问题和答案:
1. 考试系统的Question和Answer,概念上强相关,构成了一个考题的整体,外部不会绕开Question单独查看Answer,因此设计为一个聚合比较合理。
2. 知识类论坛的问题和答案跟考试系统的不一样,虽然问题和答案也是概念强相关,但每个答案可以被读者单独回复、评论、点赞等,从独立性考虑,答案应该被设计为单独的聚合。问题和答案两个聚合通过引用各自标识。

case study #4

image-20240425132802853

case study #5

1
2
3
4
产品分类ProductCatagory和产品Product
1. 从正常需求讲,产品属于一个分类,产品分类和产品各自维护信息,应该设计为两个聚合。产品聚合引用产品分类的唯一标识,即产品里有一个字段叫产品分类CatagoryID,产品分类里有一个字段ProductID
2. 如果问当删除一个产品分类的时候,是不是产品的数据一致性就破坏了?并没有,删除产品分类只是删除了产品分类和ProductID的关系,并不需要删除分类下的所有产品。
3. 如果需要在删除一个产品分类的时候删除分类下的产品,则产品分类更像是产品的一个属性,应该设计为一个聚合,产品是聚合根,产品分类是值对象

case study #6

1
2
3
4
拼团业务中,“团”与“团员”,业务规则:1.一个用户只能参团一次 2.团最多只能有N人,到达N人后,团的状态为”满团“
方案1:从概念完整性考虑,设计团为聚合根,下面有个List<团员>的实体集合。缺点:N很大比如=100的时候,有性能问题,加载一个团的时候需要把100个团员信息加载进来
方案2:除了看完整性,团员本身有独立的访问需求,设计团为聚合根、团员也是聚合根。团维护着它的业务规则:人数和状态,包含属性List<团员Id>。一个团员只能参加一个团的业务规则由领域服务承担
结论:方案2更优于方案1

资源库

  • 资源库是对聚合访问的一种抽象。资源库不局限于数据库,还可能是文件、网络存储。接口需要不依赖于具体的数据存储、ORM实现框架。
  • 资源库像集合一样,提供添加、更新、获取聚合等接口,达到聚合的生命周期管理。
  • 【与工厂的区别】工厂负责聚合实例的生,垃圾回收负责聚合实例的死,资源库就负责聚合记录的查询与增删改。
  • 【与DAO的区别】DAO模式也能做到数据访问,但DAO没有边界控制作用,没有聚合作为一个整体的概念,service类想查什么表就查什么表。

资源库设计原则

  • 一个聚合一个repository,对聚合的生命周期管理只有资源库这个入口。
  • 资源库的接口在领域层、实现在基础设施层
  • 要访问聚合内的其他实体和值对象,也只能通过聚合对应的资源库进行,这就保护了聚合的封装性。即通过资源库获取聚合的引用,通过对象图的单一遍历方向获得聚合内部对象
  • 不要包含领域逻辑
1
2
3
public interface IssueRepository {
List<IssueEntity> getInactiveIssues(); // 查找非活跃问题,如果拿到Issue集合后还需要综合判断多个状态得到非活跃问题,则属于领域逻辑,应在聚合内实现
}

case study #1 - 订单资源库设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 领域层代码
public interface OrderRepository {
// 查询方法的命名更加倾向于自然语言,而不是find等技术语言
Optional<OrderEntity> orderOfId(OrderId orderId);
// 以下两个方法在内部实现时,需要组装为通用接口的 criteria
Collection<OrderEntity> allOrdersOfCustomer(CustomerId customerId);
Collection<OrderEntity> allOrdersOfCustomerWithSpecificStatus(CustomerId customerId, OrderStatus status);

// 在底层实现中,新增和更新都可以视为是保存
void save(OrderEntity order);
void saveAll(List<OrderEntity> orders);
}

// 基础设施层代码,实现这个接口的时候委托给了一个能完成通用DAO操作的内部对象。
public class OrderRepositoryImpl implements OrderRepository {
@Resource
private GenericRepository<OrderEntity> genericRepository;


public Optional<OrderEntity> orderOfId(OrderId orderId) {
return genericRepository.findById(orderId);
}
......
}

工厂

工厂负责聚合的对象实例化工作,需不需要定义专门的工厂类依据创建逻辑复杂与否而定。

  • 不包含第二个实体的简单聚合可以直接new
  • 聚合根实体自己定义静态创建方法
  • 创建逻辑复杂时,设计单独的工厂类,隐藏创建细节

领域事件

领域内事件

如果一个应用服务对应的用例包含了多个聚合的状态改变,一个方案是通过领域内的事件来协同,但这带来异步响应编程模式。另一个方案是通过领域服务来编排多个聚合行为,这意味着所有事情都是在一个事务内同步完成。

两个方案各有优劣,事件方式优点是松耦合,异步响应,缺点是流程不直观,从应用服务角度很难知道一个完整流程是什么,不太方便查找问题,事件响应逻辑出错会带来事务一致性问题。领域服务编排方案优点是流程直观,事务一致性。缺点是如果编排逻辑发生变化需要改动的逻辑多,流程太多的话可能会有性能问题。

推荐:优先考虑使用领域服务编排完成。微服务架构下每个微服务的职责相对聚焦,一个用例通常不会同时修改多个聚合。如果需要修改多个聚合也应该是一个BC内的编排,可以通过应用服务启动的数据库事务来保证强一致性。另外领域设计提倡先从领域知识出发做聚合设计,技术实现问题比如性能问题往往放到后面做为走查领域设计的参考因素。如果没有强一致性需求的话,甚至可以通过向分布式消息队列发送领域间事件来达到自己发送自己消费的效果。

领域间事件

DDD提倡BC间尽量解耦,尽可能使用发布订阅协作模式做上下游解耦,发布订阅消息即指领域间事件。

设计原则:

  1. 消息内容需要包含全部必要信息,让消息订阅者不必再次回查事件内容。但必要信息一定不意味着整个领域实体,需要对领域实体做裁剪后重新定义出领域事件对象。
  2. 领域消息需要包含租户信息,可以通过继承包含了租户等字段的公共消息类
  3. 如何传递消息往往借助分布式消息中间件的二方库,事务最终一致性也是消息中间件需要同步考虑的事情。

BC间可以通过领域事件冗余信息吗?

领域设计时不应过多考虑性能等实现层面的事情,聚合间的引用只能是根实体ID。领域设计的最后可以从技术实现角度进行走查一遍。

  • 如果是一个BC内,不建议冗余数据。
  • 如果是BC间,则可以有多种实现方式,不冗余的话就通过客户方-供应方的接口协作查询。也可以考虑通过领域事件在BC内冗余数据存储,代价是数据的一致性需要额外关注。另外要坚持一点的是修改信息的来源只有一个,就是事件发布方。

领域事件的发布应该在领域层还是应用层?

只要不会破坏各层的依赖顺序,在哪发布都行。取决于领域事件定义在哪层?一般推荐定义在领域层的聚合内。因此即使应用层发布事件也不会破坏依赖方向。则聚合、领域服务、应用服务都可以发布事件。

领域服务

领域服务也是一种领域逻辑,在分层架构里属于领域层。但领域服务没有任何属性/数据,只是一个领域行为/动作。

领域服务的三个典型场景

  1. 协调多个聚合完成业务操作。因为聚合不能持有别的聚合对象,只能引用外部聚合根的唯一标识,因此需要跨聚合的业务操作需要领域服务来完成。(如果没有业务逻辑的话,部分场景也可以由应用服务来完成)。
  2. 不适合任何聚合的领域行为。比如一个单据的导出行为是否应该属于该订单所在的聚合?由于导出需求涉及到格式的多样性等,建议设计成领域服务,由一个接口和多个实现类(每种格式)组成。
  3. 领域行为需要与访问包括数据库在内的外部资源协作时。

领域服务设计Case Study

case study #1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 如何协调聚合根Account和聚合根Transaction之间的关系,需要用到领域服务来协调和封装
public class WithdrawingDomainService {
@Repository
private AccountRepository accountRepo;
@Repository
private TransactionRepository transRepo;

public void withDraw(AccountId id, Amount amount) {
AccountEntity account = accountRepo.findBy(id);
account.decrease(amount);
accountRepo.save(account);

Transaction transaction = Transaction.createFrom(id, amount);
transRepo.save(transaction);
}
}

case study #2

1
2
3
4
5
6
7
8
9
10
11
12
13
//“验证订单有效性”这一验证行为需要验证Order聚合边界内的信息比如是否提供了配送地址、联系人信息、是否含有有效订单项。但同时也要验证下订单的顾客是否为有效顾客,这是另一个聚合Customer的范畴。这一典型的跨聚合的行为需要领域服务。由于聚合之间的协作只能通过身份标识进行,Order聚合只是持有Customer的身份标识CustomerId,要获得Customer聚合就需要通过该Customer的资源库。

public class ValidatingOrderDomainService {
private CustomerRepository customerRepo;
public boolean isValid(Order order) {
//step1 验证订单内信息
order.validate();

//step2 验证客户信息
Optional<Customer> optCustomer = customerRepo.customerOf(order.getCustomerId());
return optCustomer.isValidCustomer();
}
}

领域服务设计原则

  • 粒度要小,一个领域服务只做一件事,便于应用服务编排。
  • 作为参考可以要求领域服务的名称必须包含一个动词。对于同一个聚合关联的添加、更新、查询、删除等相关领域服务建议设计为四个不同的类,避免领域类太大。
  • 一个比较常见的坏习惯是没有尽最大努力为领域行为找到一个归属的实体,轻易定义成领域服务,这容易让领域服务类变成”上帝类”。

应用服务

应用服务属于分层架构里的应用层,每个用户用例对应一个方法。

  • 外观模式:对外提供粗粒度接口、对内分配职责的协作作用。类似设计模式的外观模式。
  • 坚持不做业务决策的原则:应用服务自己不做任何业务逻辑的决策。每当你觉得应用服务里的代码做出的业务决策是跟本BC业务逻辑有关联时,往往是把不必要的业务代码蔓延到这层了。
  • 应用服务方法的入参和出参都是DTO,调用领域层需要传入entity,返回interface层需要把entity转成DTO

应用服务的两个典型场景:

  • 不包含领域逻辑的业务服务应被定义为应用服务。
  • 与横切关注点协作的服务应被定义为应用服务:事务、认证授权等

DTO的设计原则

  • 入参DTO尽量少暴露字段
  • 多个用例下尽量不复用入参DTO
  • 尽量复用出参DTO(可以比要求的多暴露字段)
  • 创建和更新用例下,返回整个实体
  • DTO的校验不包含领域校验逻辑,比如唯一性校验

应用服务与领域服务的区别?

看起来领域服务和应用服务都能做编排功能,原则上是应用服务不应该包含领域层的逻辑,那怎么判断什么是领域层的业务逻辑?这个问题没有标准答案,设计时不用太条条框框。一个简单的判断标准可以自问一下:这段代码蕴含的知识是否与本BC的主要职责域直接有关?如果相关的话不要在放应用服务。

  • 洋葱架构作者和Eric Evans都提到过不用规定的太死,外层一定只能调用它的直接内层。只要调用关系不会破坏层次的耦合就行(外层依赖内层,内层不依赖外层)。也就是说应用服务不一定非要通过领域服务来访问资源库和根实体,也可以直接引用资源库和根实体,领域服务也可以引用资源库和根实体。
  • 不要用领域服务对根实体的方法做简单封装后供应用服务调用

case study #1 - 下订单

  1. 验证订单是否有效
  2. 提交订单
  3. 移除购物车中已购买的商品
  4. 发送短信通知买家

取决于设计者怎么理解这四件事情,语义上看前面三件事跟本BC有密切关系,第四件事发送短信与本BC没有密切关系,出现在应用层也合理。

  1. 方案1:把四个步骤封装成一个领域服务placeOrderDomainService,应用服务OrderAppService调用它
  2. 方案2:把前三个步骤封装成一个领域服务placeOrderDomainService,应用服务OrderAppService调用它和执行第四步
  3. 方案3:把四个步骤都放在应用服务OrderAppService来执行

领域模型元素间的关系

模型对象设计顺序?

  • 业务需求的分析过程是从上而下的,由业务流程,到用户用例,到领域模型。而设计过程是相反的,自下而上的。从领域元素设计开始,最后才是应用服务的编排。
  • 建议的设计优先级是先值对象 → 再实体 → 再聚合 → 再领域服务→ 最后是应用服务,优先考虑领域是否应该为值对象,其次是否为实体,划分出聚合。不属于实体或值对象中的领域行为放到领域服务,需要协调聚合的领域行为设计为领域服务或者应用服务。
  • 图解如下:(gateway意指adaptor)

image-20240425132839132

-

领域模型元素间的访问边界

应用服务 领域服务 聚合/根实体 资源库
应用服务 一个应用服务不要访问另一个应用服务 应用服务能访问多个领域服务 应用服务能访问聚合即根实体 应用服务能访问资源库
领域服务 领域服务不能访问应用服务 如果需要组装出粒度大的领域服务,领域服务能访问多个领域服务 领域服务能访问聚合即根实体 领域服务能访问资源库
聚合 聚合不能访问应用服务 聚合不能访问领域服务 通过领域服务来协作多个聚合,一个聚合只能接受另一个聚合作为参数传入 聚合不能访问资源库(聚合是业务的最小边界,聚合内不依赖任何外部资源)
资源库 资源库不能访问应用服务 资源库不能访问领域服务 资源库不能访问聚合,根实体作文入参传入资源库实现接口,需要先被转换成持久化对象 一个资源库不能访问另一个资源库