埃里克·埃文斯(Eric Evans)的《领域驱动设计:软件核心复杂性应对之道》的核心知识点可以概括为:通过建立统一的语言和严格的模型边界,将复杂的业务逻辑清晰地映射到软件设计中,从而应对核心领域的复杂性。

核心内容

核心基础:协作与语言

这是DDD的基石,贯穿所有实践。

  • 统一语言:这是DDD最核心的实践。强调在同一个限界上下文中,开发人员、领域专家和业务人员使用同一套精确的、无歧义的业务词汇。无论是开会、写代码、画图还是写文档,语言都必须一致。代码就是语言的载体,语言就是代码的反映。
  • 领域、子域与核心域:
    • 领域:软件应用的业务范围。
    • 子域:为了处理领域的复杂性,将其拆分为多个子域。
    • 核心域:业务的核心竞争力所在,是软件最需要投入精力的部分(书中强调:“将精力集中在核心域上”)。其他子域(支撑子域、通用子域)服务于核心域。

战略设计:应对复杂系统的架构

战略设计主要解决“模型在哪里适用”以及“不同模型如何交互”的问题。

  • 限界上下文:这是战略设计的核心。它定义了模型的显式边界。在这个边界内,统一语言和模型保持一致;边界之外,其他上下文有自己的模型。这解决了大系统中不同模块对同一概念(如“客户”)定义不同的问题。
  • 上下文映射:定义了不同限界上下文之间的集成关系。书中描述了多种集成模式:
    • 防腐层:最经典的模式。为下游系统(核心域)建立一个隔离层,将上游系统(外部系统或遗留系统)的模型转换为自己想要的模型,防止外部污染侵蚀核心域。
    • 开放主机服务:定义一套显式的协议供其他上下文调用。
    • 共享内核:两个上下文共享一部分模型,但需要严格控制变更。
    • 客户/供应商:上下游关系。

战术设计:构建模型的基本构建块

战术设计是一套在代码层面实现领域模型的设计模式,解决了“如何用代码表达业务”的问题。

  • 实体:具有唯一标识符的对象,其状态(属性)会变化,但标识不变(例如:人、订单)。实体需要实现生命周期连续性。
  • 值对象:没有唯一标识符,仅由其属性值定义的对象(例如:地址、颜色)。值对象通常是不可变的,使用起来更安全、更轻量。
  • 聚合:这是战术设计中最重要的模式。一组相关对象的集合(实体和值对象),作为数据修改的单元。聚合通过根实体(聚合根)控制外部访问,外部只能通过聚合根来操作内部对象。聚合内部保证事务一致性,聚合之间保证最终一致性。
  • 领域事件:表示领域中发生的重要事件(例如:订单已付款)。它用于实现聚合之间的解耦,也是构建事件驱动架构和微服务拆分的关键。
  • 领域服务:当某个业务逻辑不属于某个特定的实体或值对象时(例如:转账操作,涉及两个账户实体),将其放在领域服务中。
  • 资源库:模拟集合的机制,用于在聚合的“内存中查找”与“数据库存储”之间提供抽象。通常只为聚合根提供资源库。

建模过程:重构与探索

DDD强调模型不是一次性设计出来的,而是通过不断重构“涌现”出来的。

  • 持续集成:在同一个限界上下文内,保持模型的统一性,频繁合并代码,避免模型分裂。
  • 通过重构获得深层模型:书中强调,最初的模型往往是“贫血模型”。通过不断重构,挖掘业务中的深层概念(如“运输”不仅仅是两个地点,而是包含“装载”“卸载”“航行”等过程),将隐形的核心概念显式化地体现在代码中。

深入谈谈 聚合

什么是聚合

在面向对象设计中,对象之间往往互相引用。一个订单包含多个订单行,订单行指向一个商品,商品又有供应商……如果不加约束,当修改一个订单时,整个对象图就像一张巨大的网。聚合的目的就是打破这张网。 它将具有紧密业务关联的对象(实体和值对象)划分为一个生命周期和事务一致性单元。

聚合由三部分组成:
  1. 聚合根:聚合的唯一入口,外部对象只能持有聚合根的引用,不能直接修改聚合内部的其他对象。通常也是一个实体。
  2. 实体:聚合内部除了根以外,需要唯一标识的、会变化的业务对象。
  3. 值对象:聚合内部不可变的、描述性质的对象。

结合具体例子[电商订单]

错误的设计(无聚合概念)
// 外部Service可以直接修改订单行
Order order = orderRepository.findById(orderId);
OrderLine line = order.getLines().get(0);
line.setPrice(0); // 直接修改价格
order.setTotalAmount(0); // 忘了重新计算总价 -> 数据不一致
正确的聚合设计
  1. 确定聚合根:Order
    在订单聚合中,订单(Order)是聚合根。不能脱离订单去操作一个订单行。

  2. 聚合内部包含:

  • Order(聚合根,实体)
  • OrderLine(实体,隶属于订单)
  • Address(值对象,收货地址,不可变)
  1. 设计代码结构
// 聚合根: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时,可能会同时修改OrderLineTotalAmount
  • 这些修改必须在同一个数据库事务中提交,要么全成功,要么全失败。

为什么不能一个事务修改多个聚合,假设有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辅助生成,所有观点代表个人。


愿我都能在各自的领域里不断成长,勇敢追求梦想,同时也保持对世界的好奇与善意!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐