DDD记录

2084 字
10 分钟
DDD记录

一些概念的理解#

### 界限上下文

image 37
image 37

  • 定义清晰的边界: 每个界限上下文都拥有自己的一套业务逻辑、实体、用语和规则。例如,一个“客户管理”系统和一个“订单管理”系统可能都涉及“客户”这一概念,但它们在各自的界限上下文中对“客户”的定义和处理方式可能不同。
  • 不同子域的隔离: 在DDD中,系统通常分为多个子域(Subdomains)。这些子域可能会有不同的需求、不同的模型、不同的业务规则。界限上下文明确了每个子域的业务范围和边界,避免不同子域之间的混淆和交叉干扰。
  • 上下文内的统一语言: 在一个界限上下文内,团队会使用统一的术语和语言来确保所有成员对同一概念有共同的理解。这就是DDD中提到的领域语言(Ubiquitous Language)。例如,在“订单管理”界限上下文中,“订单”的定义、属性和行为可能与其他上下文中的“订单”定义有所不同。
  • 与其他界限上下文的关系: 不同的界限上下文之间可能会存在交互,通常通过上下文映射(Context Mapping)来定义。常见的关系类型有:
    • 共享内核(Shared Kernel):多个上下文共享一些核心模型。
    • 客户-供应商(Customer-Supplier):一个上下文作为另一个上下文的“客户”,依赖于其提供的功能。
    • 防腐层(Anti-Corruption Layer):为避免不同上下文间的影响和混淆,定义一个层来隔离和转化数据。
    • 发布-订阅(Published-Subscribed):一个上下文发布事件,另一个上下文订阅这些事件。

举例

image 38
image 38

### 实体、值对象、聚合根、领域服务、领域事件
  • **实体 **当一个对象由其标识(而不是属性)区分时,这种对象称为实体(Entity)。理解上来说就是id,例如,人可以通过身份证号这种独一无二的标识来进行区分。而不是人的属性,例如身高,年龄等。
  • **值对象 **当一个对象用于对事物进行描述而没有唯一标识时,它被称作值对象(Value Object)。例如年龄28岁。使用值对象,可以更好的做系统优化和设计的精简优化。在实践中,需要保证值对象创建后就不能被修改,即不允许外部再修改其属性。
  • 聚合根 聚合一组相关对象的集合,作为一个整体被外界访问,聚合根是这个聚合的根节点。聚合由根实体、值对象和实体构成。聚合是一个非常重要的概念,==核心领域往往都需要用聚合来表达==。其次,聚合在技术上有非常高的价值,可以指导详细设计。
  • **领域服务 **一些重要的领域行为或操作,可以归类为领域服务。它既不是实体,也不是值对象的范畴。当我们采用了微服务架构风格,一切领域逻辑的对外暴露均需要通过领域服务来进行。如原本由聚合根暴露的业务逻辑也需要依托于领域服务
  • **领域事件 **是表示系统中发生了某个具有业务意义的事件。它通常用于表示业务中某些状态变化或者某个重要行为的发生。这些事件通常是不可变的,一旦发生就会携带相关的业务数据,表示一个已经发生的事实。

充血模型代码举例#

在电商场景中,订单聚合根 OrderAggregateRoot 往往包含一些以下实体与值对象

  • Order(订单)
  • OrderItem(订单商品明细)
  • ShippingAddress(订单收货地址)

DDD 战术上,充血模型希望通过 OrderAggregateRoot 来作为其管理的实体、值对象的管理入口

示例代码如下

// 订单聚合根
public class OrderAggregateRoot {
private Order order;
private List<OrderItem> orderItem;
private ShippingAddress shippingAddress;
@Getter
private final List<DomainEvent> domainEvents = new ArrayList<>();
private OrderAggregateRoot() {
}
public static OrderAggregateRoot create(CreateOrder cmd) {
// 规则校验
OrderAggregateRoot root = new OrderAggregateRoot();
Order order = new Order();
// order.setXxx(xxx);
root.setOrder(order);
addDomainEvent(new OrderCreatedEvent());
}
public static OrderAggregateRoot from(Order order, List<OrderItem> orderItem, ShippingAddress shippingAddress) {
OrderAggregateRoot root = new OrderAggregateRoot();
// root.setXxx(xxx);
}
public BigDecimal calculateTotalPrice(CalculateOrderPrice cmd) {
return BigDecimal.ZERO;
}
public String changeShippingAddress(ChangeShippingAddress cmd) {
return "";
}
public void markAsPaid(MarkAsPaid cmd) {
// 标记订单已支付
order.setOrderStatus("PAID");
addDomainEvent(new OrderPaidEvent());
}
public void markAsDelivered(MarkAsDelivered cmd) {
// 标记订单已发货
order.setOrderStatus("PAID");
addDomainEvent(new OrderDeliveredEvent());
}
public void cancelOrder(CancelOrder cmd) {
// 取消订单
order.setOrderStatus("PAID");
addDomainEvent(new OrderCanceledEvent());
}
private void addDomainEvent(DomanEvent event) {
this.domainEvents.add(event);
}
// 在持久化到数据库后,清空已发送的领域事件
public void clearDomainEvents() {
this.domainEvents.clear();
}
}
// 订单聚合根持久化接口
public interface OrderAggregateRootRepository {
// 从数据库加载
OrderAggregateRoot loadOrder(String orderNo);
// 实现类通过 change tracing 实现变更字段的更新
void saveOrder(OrderAggregateRoot orderAggregate);
}
// 订单领域服务
public class OrderService {
private OrderAggregateRootRepository orderAggregateRootRepository;
private DomainEventPublisher domainEventPublisher;
public void createOrder(Object cmd) {
OrderAggregateRoot root = OrderAggregateRoot.create(cmd);
// 先持久化
orderAggregateRootRepository.save(root);
// 发送领域事件
domainEventPublisher.publishEvents(root.getDomainEvents());
// 清理领域事件
root.clearDomainEvents();
}
public void cancelOrder(CancelOrder cmd) {
OrderAggregateRoot root = orderAggregateRootRepository.loadOrder(cmd.getOrderNo());
root.cancelOrder(cmd);
// 发送领域事件
domainEventPublisher.publishEvents(root.getDomainEvents());
// 清理领域事件
root.clearDomainEvents();
}
}

从上面的示例代码上,可以看出充血模型有以下特点:

  1. 所有写操作,都通过领域服务加载 **聚合根 **后,再通过 聚合根提供的修改操作来实现数据修改的逻辑,然后再通过一次 save 操作更新到数据库中
  2. 加载 聚合根 时,会一次性从数据库中加载相关的所有数据信息(也可以考虑延迟加载,但是代码复杂度会上升)
  3. 一些单元测试,可以直接通过构造 **聚合根 **来进行单测,可以做到和数据库不相关

DDD 中层的划分#

image 39
image 39

贫血模型的实现思考#

一般习惯了 MVC 开发模式的开发团队,向 DDD 充血模型转换时,会遇到比较大的困难:

  1. 领域模型包含了更多的业务规则、领域事件、验证逻辑等,这增加了模型的复杂性;
  2. 思维模式一时半会儿难以适应,可能会觉得创建和维护这些丰富的聚合根、实体和值对象变得难度加大,开发周期会拉长;
  3. DDD 的 data change tracking JPA天然支持,但是在国内 Mybatis 大流行的背景下,没有对应的主流开源实现,通常都是面向数据库编程;

所以,在项目落地时,是否可以适当妥协,继续使用贫血模型,但是,也借鉴 聚合根 的思想,精简设计我们的领域服务,减少不必要的 Service 类,例如以上例子,我们只考虑在代码中添加 OrderService

绝对不要出现 OrderItemServiceShippingAddressService ,所有涉及到 OrderItemShippingAddress 的写操作,都交由 OrderService 领域服务管理,在开发任务划分时,先讨论梳理出聚合根,再整理得到相关的操作,分配任务时,通过将聚合根来确定领域服务,实现任务功能的分配,这样每个分到任务的人,实现写业务逻辑时,能够相对独立完整的进行开发

public class OrderService {
private OrderDao orderDao;
private OrderItemDao orderItemDao;
private ShippingAddressDao shippingAddressDao;
public void createOrder(Object cmd) {
// 业务逻辑实现
}
public void cancelOrder(CancelOrder cmd) {
// 业务逻辑实现
}
}

TODO#

一些涉及到只读数据查询的代码落地最佳实践,例如后端管理界面一些数据的查询组合,结合 CQRS(Command Query Responsibility Segregation,命令查询职责分离),是否能够减少领域服务相关的查询逻辑代码,所有写的操作,都纳入到领域服务管理,查询相关的服务,单独构建一个服务,来管理这部分代码

那些场景适合使用DDD#

业务逻辑复杂、需要深刻理解领域知识的场景,在关系数据库模型上,通常会涉及到主表、字表状态的一致性,例如上面提到的订单主表和订单项字表,反例:字典表管理就不太适合使用DDD

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

DDD记录
https://blog.sephy.top/posts/ddd记录/
作者
虾米
发布于
2025-02-07
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
虾米
coder
分类
标签
站点统计
文章
61
分类
4
标签
52
总字数
64,663
运行时长
0
最后活动
0 天前
站点信息
构建平台
GitHub Actions
博客版本
Firefly v6.13.9
文章许可
CC BY-NC-SA 4.0