0%

领域驱动设计理论实践

战略设计

战略设计是将“混沌”解构成“清晰”的过程,在该过程从开始到结束的历程之中,我们会划分出领域、界定通用语言范围、确定出系统限界上下文以及上下文之间的映射方式。

ddd-design

领域划分

战略设计在领域驱动设计中起着关键作用,因为其决定了前进的方向是否正确;而领域划分又是在战略设计中最关键的一点,它决定了系统最终架构的合理性。

领域划分要做的事情是识别出核心域和子域。核心域是系统的内核,是系统建设的重点方向,是资源倾斜的重点;子域又分支撑子域和通用子域,可以考量是否做成基座能力。

限界上下文

限界上下文的确定过程,是系统划分的过程。当领域划分完成后,存在多个领域,哪儿些域可以独立成一个系统?哪儿些域可以在共同建设在同一系统中?就是需要在这一步进行确定的。

就如何去确定限界上下文而言,我们应该从多个方面考量:

  • 领域划分的合理性
  • 限界系统建设成本
  • 限界系统维护成本

通用语言

通常在一个系统内有自己独立的通用语言,但是更希望将其形成全域的通用语言,形成共识,这样沟通起来才会形成概念上的统一。实际落地过程中,我们应该将相关术语进行详细阐述并统一维护,沉淀为领域资产。

上下文映射

上下文映射是限界系统之间的交互方式,在战略设计阶段,应该从架构合理性的角度进行考量。

战略设计案例

这里以一个简易的交易系统来进行战略分析,涉及场景如下:

  • 用户购买商品
  • 平台进行配送
  • 用户签收商品

此时可以做出如下战略设计:
ddd-domain

从场景分析作为切入点,划分出商品域、交易域、用户域和配送通用域,并形成对应的限界上下文系统;同时,考虑到域之间的连接问题,可以将商品域和交易合并为一个更大一点的交易域。

战术设计

实体

何为实体?不是由属性来定义,而是通过一连串的连续事件和标识定义的一种对象。

实体对象拥有唯一标识符,且标识符在历经各种状态变更后仍能保持一致,而对象的其他属性相对于标识而言显得并不是那么得重要,这些属性只是该实体在某种时刻或某种状态下的一种内容修饰,用来丰富实体自身。

对实体来说,标识符是非常重要的概念,就像身份证之于人一样,是实体自身的身份认证,无论实体数据流转到哪儿个系统,标识符始终不会发生改变。例如实际场景中就订单实体来说,标识符这是订单ID,订单的生命周期无论发生怎样的变化,此ID在订单的整个轨迹生涯中以及在不同上下文中的流转,始终唯一。

所以,就领域实体的建模而言,标识符是必须存在的,且不应该同数据库的标识ID混淆,因为数据库只是实体的持久化方式,对于实体的持久化,既可以选择关系型、文档型、索引型数据库,亦可以选择内存存储,数据存储介质的ID在对于领域实体而言被称为派生标识。

以订单实体来说,建模样例简略如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class OrderInfo {

private String orderId;
private String userId;
private String addressId;
private BigDecimal amount;
private OrderStatus orderStatus;

/* 其他字段信息省略*/

public String getId() {
return this.orderId;
}
}

对于连续性事件,订单的状态则是订单实体连续性事件的体现,经历多个状态的变迁,实体的标识并不会发生变化,变化的知识实体用有的内容和处于某个阶段的状态。

值对象

何为值对象?一种描述了某种特征或属性,但没有概念标识的对象。

值对象是对实体属性的一种组合,进行概念化统一。它将多个相关属性组合为一个概念整体,可以保证属性归类的清晰和概念的完整性,避免属性零碎的散落在实体中。

值对象在代码中有两类形态存在:

  • 如果值对象是单一属性,则直接定义为实体类的属性。比如一个‘Integer’属性,一个枚举属性,都可以称之为值对象,索引从值对象的角度来看对于实体的基本类型属性而言,要定义为包装类型对象。
  • 如果是多个散落属性的组合概念,则会设计为一个类。比如金额,可以有金额大小和单位两个属性,从本质上来将其和‘Integer’一样,只不过‘Integer’已经被封装成了对象,而对于这里的‘金额’而言需要我们自己封装。

对于订单而言,金额值对象‘Amount’和用户id值对象‘addressId’如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class OrderInfo {

private String orderId;
private String userId;
private String addressId;
private Amount amount;
private OrderStatus orderStatus;

/* 其他字段信息省略*/

public String getId() {
return this.orderId;
}

public static class Amount {
private BigDecimal amount;
private String unit;
}
}

就值对象的定义而言,我们应该根据值对象的属性和特征进行业务语义的归类,语义内聚,避免属性无规则平铺且语义泛化的值对象存在;当然从领域建模角度,我们应尽量用值对象来承载业务,减少实体的数量,这样可以降低业务模型的复杂度。

就数据库存储而言,属性组合的值对象持久化可以是实体的某个属性,以序列化形式(如JSON)的存在,当然亦可以根据具体的业务评估来决策将其定义为仿真实体(非真实实体),用专门的表来存储。

实体和值对象是领域中最基础核心的对象,基于这些基础领域对象,可以衍生出多样化的业务领域逻辑。

聚合

何为聚合?聚合就是一组相关对象的集合,我们把聚合作为数据修改的单元,外部对象只能引用聚合中的一个成员,我们把它称为根。

从持久化的角度而言,在聚合的边界之内应用一致性规则,即保证聚合边界内的事务一致性;从查询的角度而言,可以查询出整个聚合,也可以查询出部分实体,整个需要根据实际业务情况和性能情况的角度来考量。

订单的聚合简略如下:

1
2
3
4
5
6
7
8
9
public class OrderInfo extends Aggregate<String> {
private String orderId;
private String userId;
private String addressId;
private BigDecimal amount;
private OrderStatus orderStatus;
private List<OrderDetail> orderDetailList;
private OrderExtentInfo orderExtentInfo;
}

此时的聚合根为订单,聚合的相关对象包含:订单实体、子订单实体、订单扩展实体。

工厂

工厂是一种针对对象和聚合复杂创建逻辑的封装抽象,但不承担领域模型中的职责;其同设计模型中的工厂一样,是生产目标对象的一种方式。

在实际落地应用中,通常存在两种工厂概念的应用:

  • 实体、聚合、值对象等内部属性为参数进行构建对象,明确标识对象创建需要的参数;
  • 复杂对象生产,如静态编译、动态组合(运行依赖)等;

第一种方式以简单的查询条件为例,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class OrderQueryCondition {

private String userId;
private String orderId;
private OrderStatus orderStatus;

@DomainFactory
public static OrderQueryCondition build(String userId, String orderId, OrderStatus orderStatus) {
OrderQueryCondition condition = new OrderQueryCondition();
condition.setUserId(userId);
condition.setOrderId(orderId);
condition.setOrderStatus(orderStatus);
return condition;
}
}

第二种方式以领域事件为例,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@DomainFactory
public class OrderEventModelFactory {

public static OrderEventModel build(OrderEventAction action, OrderInfo orderInfo) {

OrderEventModel event = new OrderEventModel();
event.setAction(action);
event.setUserId(orderInfo.getUserId());
event.setOrderId(orderInfo.getOrderId());
event.setAddressId(orderInfo.getAddressId());
event.setAmount(orderInfo.getAmount());
event.setOrderStatus(orderInfo.getOrderStatus());
return event;
}
}

资源库

资源库通常是针对聚合设计,对聚合的存储和查询行为进行有效管理,可以是关系型数据库、索引、文档型数据库、内存数据库以及集合存储等。

在实际应用中,也就是我们通常见到的仓库(repository),遵循不同的实践方式存在不同的形式:

  • 从聚合的角度,资源库同聚合进行一对一映射,即一个聚合一个资源库;
  • 从实体的角度,资源库同实体进行一对一映射,即一个实体一个资源库;

聚合的角度资源库定义案例如下:

1
2
3
4
public interface OrderInfoRepository {
String save(OrderInfo orderInfo);
OrderInfo queryByOrderId(String userId, String orderId);
}

实体的角度资源库定义案例如下:

1
2
3
4
5
6
7
8
public interface OrderInfoRepository {
String save(OrderInfo orderInfo);
OrderInfo queryByOrderId(String userId, String orderId);
}
public interface OrderDetailRepository {
void batchInsert(List<OrderDetail> orderDetailList);
List<OrderDetail> queryByOrderId(String orderId);
}

领域服务

何为领域服务?当领域中的某个操作过程或者转换过程不是实体或者值对象的职责时,便可以将该操作放在一个单独的接口中,即封装一个领域服务。

领域服务可以存在于如下三种方式:

  • 显著的业务用例执行过程
  • 多个领域对象的业务编排
  • 可复用的应用层逻辑下层

什么是显著的业务用例?从交易系统的用例分析而言,如创建订单、取消订单、支付订单等等就是显著的业务用例,它们的执行过程包含业务规则校验和核心逻辑处理。

那么在实践中如何合理的使用应用服务和领域服务,并将领域服务做厚,将应用服务做薄?这里建议遵循一个原则:应用服务只做跨聚合跨应用的编排,核心规则下层领域

领域事件

何为领域事件?是将领域中聚合所发生的活动行为建模成一系列离散的事件行为,通常体现在聚合的状态变更。

在实际实际中可以分为应用内事件和应用外事件,应用内事件以监听器模式处理,应用外事件以消息模式处理。