《领域驱动设计:软件核心复杂性应对之道》读后总结与思考
《领域驱动设计:软件核心复杂性应对之道》读后总结与思考
埃里克·埃文斯(Eric Evans)的《领域驱动设计:软件核心复杂性应对之道》的核心知识点可以概括为:通过建立统一的语言和严格的模型边界,将复杂的业务逻辑清晰地映射到软件设计中,从而应对核心领域的复杂性。
核心内容
核心基础:协作与语言
这是DDD的基石,贯穿所有实践。
- 统一语言:这是DDD最核心的实践。强调在同一个限界上下文中,开发人员、领域专家和业务人员使用同一套精确的、无歧义的业务词汇。无论是开会、写代码、画图还是写文档,语言都必须一致。代码就是语言的载体,语言就是代码的反映。
- 领域、子域与核心域:
- 领域:软件应用的业务范围。
- 子域:为了处理领域的复杂性,将其拆分为多个子域。
- 核心域:业务的核心竞争力所在,是软件最需要投入精力的部分(书中强调:“将精力集中在核心域上”)。其他子域(支撑子域、通用子域)服务于核心域。
战略设计:应对复杂系统的架构
战略设计主要解决“模型在哪里适用”以及“不同模型如何交互”的问题。
- 限界上下文:这是战略设计的核心。它定义了模型的显式边界。在这个边界内,统一语言和模型保持一致;边界之外,其他上下文有自己的模型。这解决了大系统中不同模块对同一概念(如“客户”)定义不同的问题。
- 上下文映射:定义了不同限界上下文之间的集成关系。书中描述了多种集成模式:
- 防腐层:最经典的模式。为下游系统(核心域)建立一个隔离层,将上游系统(外部系统或遗留系统)的模型转换为自己想要的模型,防止外部污染侵蚀核心域。
- 开放主机服务:定义一套显式的协议供其他上下文调用。
- 共享内核:两个上下文共享一部分模型,但需要严格控制变更。
- 客户/供应商:上下游关系。
战术设计:构建模型的基本构建块
战术设计是一套在代码层面实现领域模型的设计模式,解决了“如何用代码表达业务”的问题。
- 实体:具有唯一标识符的对象,其状态(属性)会变化,但标识不变(例如:人、订单)。实体需要实现生命周期连续性。
- 值对象:没有唯一标识符,仅由其属性值定义的对象(例如:地址、颜色)。值对象通常是不可变的,使用起来更安全、更轻量。
- 聚合:这是战术设计中最重要的模式。一组相关对象的集合(实体和值对象),作为数据修改的单元。聚合通过根实体(聚合根)控制外部访问,外部只能通过聚合根来操作内部对象。聚合内部保证事务一致性,聚合之间保证最终一致性。
- 领域事件:表示领域中发生的重要事件(例如:订单已付款)。它用于实现聚合之间的解耦,也是构建事件驱动架构和微服务拆分的关键。
- 领域服务:当某个业务逻辑不属于某个特定的实体或值对象时(例如:转账操作,涉及两个账户实体),将其放在领域服务中。
- 资源库:模拟集合的机制,用于在聚合的“内存中查找”与“数据库存储”之间提供抽象。通常只为聚合根提供资源库。
建模过程:重构与探索
DDD强调模型不是一次性设计出来的,而是通过不断重构“涌现”出来的。
- 持续集成:在同一个限界上下文内,保持模型的统一性,频繁合并代码,避免模型分裂。
- 通过重构获得深层模型:书中强调,最初的模型往往是“贫血模型”。通过不断重构,挖掘业务中的深层概念(如“运输”不仅仅是两个地点,而是包含“装载”“卸载”“航行”等过程),将隐形的核心概念显式化地体现在代码中。
深入谈谈 聚合
什么是聚合
在面向对象设计中,对象之间往往互相引用。一个订单包含多个订单行,订单行指向一个商品,商品又有供应商……如果不加约束,当修改一个订单时,整个对象图就像一张巨大的网。聚合的目的就是打破这张网。 它将具有紧密业务关联的对象(实体和值对象)划分为一个生命周期和事务一致性单元。
聚合由三部分组成:
- 聚合根:聚合的唯一入口,外部对象只能持有聚合根的引用,不能直接修改聚合内部的其他对象。通常也是一个实体。
- 实体:聚合内部除了根以外,需要唯一标识的、会变化的业务对象。
- 值对象:聚合内部不可变的、描述性质的对象。
结合具体例子[电商订单]
错误的设计(无聚合概念)
// 外部Service可以直接修改订单行
Order order = orderRepository.findById(orderId);
OrderLine line = order.getLines().get(0);
line.setPrice(0); // 直接修改价格
order.setTotalAmount(0); // 忘了重新计算总价 -> 数据不一致
正确的聚合设计
-
确定聚合根:
Order
在订单聚合中,订单(Order)是聚合根。不能脱离订单去操作一个订单行。 -
聚合内部包含:
Order(聚合根,实体)OrderLine(实体,隶属于订单)Address(值对象,收货地址,不可变)
- 设计代码结构
// 聚合根:Order
public class Order {
// 聚合根持有对内部实体的引用
private List<OrderLine> orderLines;
private Address shippingAddress;
private OrderStatus status;
private Money totalAmount;
// 关键:业务行为。外部不能直接操作orderLines,只能通过聚合根的方法
public void addProduct(Product product, int quantity) {
// 1. 创建新的OrderLine(业务逻辑封装)
OrderLine newLine = new OrderLine(product, quantity);
this.orderLines.add(newLine);
// 2. 重新计算总价(保证内部数据一致性)
this.recalculateTotalAmount();
}
public void changeQuantity(String lineId, int newQuantity) {
// 外部不能直接 setQuantity,必须经过聚合根判断
OrderLine line = findLineById(lineId);
line.changeQuantity(newQuantity); // 内部实体也有行为
this.recalculateTotalAmount();
}
private void recalculateTotalAmount() {
// 确保总价永远等于各行的累加,不会有脏数据
this.totalAmount = this.orderLines.stream()
.map(OrderLine::getSubTotal)
.reduce(Money.ZERO, Money::add);
}
}
// 聚合内部的实体:OrderLine
public class OrderLine {
private String id;
private Product product;
private int quantity;
private Money price;
// 行为:修改数量
public void changeQuantity(int newQuantity) {
if (newQuantity > product.getStock()) {
throw new BusinessException("库存不足");
}
this.quantity = newQuantity;
// 注意:总价计算由Order根负责,OrderLine不包含totalAmount字段
}
public Money getSubTotal() {
return price.multiply(quantity);
}
}
// 资源库:只针对聚合根
public interface OrderRepository {
Order findById(OrderId id); // 只能以聚合根为单位查询
void save(Order order); // 保存时整个聚合的状态必须一致
}
聚合的核心原则
(1)事务一致性边界:一个事务只修改一个聚合实例
在微服务或分布式系统中,这是非常重要的约束。回到上面的例子:
- 当修改
Order时,可能会同时修改OrderLine和TotalAmount。 - 这些修改必须在同一个数据库事务中提交,要么全成功,要么全失败。
为什么不能一个事务修改多个聚合,假设有Order聚合和Inventory(库存)聚合。
- 如果下单时既要修改订单状态,又要扣减库存,把这两个不同的聚合放在一个数据库事务里,就会导致跨聚合的强耦合。
- 正确做法:先修改
Order聚合,提交事务;然后发送领域事件(如OrderPaidEvent),由消费者异步或最终一致性去修改Inventory聚合。
(2)通过标识引用,避免对象引用
在聚合之间,不要持有对象引用,只持有聚合根的ID。
// 错误:直接持有对象引用,容易导致跨聚合的级联修改
public class Order {
private Customer customer; // Customer是另一个聚合根
}
// 正确:只持有标识符
public class Order {
private CustomerId customerId; // 仅仅是ID
}
这保证了聚合之间的松耦合。当需要获取客户信息时,通过资源库根据CustomerId去查询。
(3)聚合要小而精
- 聚合应该尽量小,因为聚合越大,事务范围越大,并发冲突和性能问题越明显。
- 如果一个聚合包含太多实体,比如
Order包含Invoice(发票)、Payment(支付记录),就应该考虑拆分。因为这些对象往往不要求强一致性。
聚合 vs. 包/组件/模块
| 维度 | 聚合 | 包 / 组件 / 模块 |
|---|---|---|
| 核心目的 | 保证业务操作的数据一致性和业务不变量 | 管理代码的组织结构、技术复用和编译依赖 |
| 所属层级 | 领域逻辑层(业务概念) | 技术架构层(代码物理存放) |
| 颗粒度 | 通常是 2-4 个类(聚合根 + 若干实体/值对象) | 可以包含多个聚合,多个服务,可以是整个子域 |
| 生命周期 | 聚合根有独立的生命周期(通过资源库管理) | 包的划分随架构风格变化,不受生命周期约束 |
| 事务关系 | 强一致性(同一个事务内) | 内部可以包含多个聚合,但好的设计会让模块内包含的多个聚合之间遵循最终一致性 |
总结
通过阅读此书,获得了这样的认知:不再以数据结构(表)为中心去设计软件,而是以业务行为(领域)为中心去设计。它试图解决的是传统三层架构(UI-业务-数据)中常见的“业务逻辑泄露到Service层,导致领域对象变成仅含Get/Set的贫血模型”的问题。通过上述战略和战术模式,让代码成为业务的可执行蓝图。
聚合是 DDD 中最难掌握但也最关键的模式。它的本质是在对象图与数据库事务之间找到一个平衡点。记住埃文斯在书中的建议:“通过规定的聚合根来访问聚合内部的对象,保持事务只作用于单个聚合。” 这也是 DDD 战术设计能够落地到高并发、微服务架构的基石。
以上基于个人学习总结和AI辅助生成,所有观点代表个人。
愿我都能在各自的领域里不断成长,勇敢追求梦想,同时也保持对世界的好奇与善意!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)