CC 咖啡猫的工作空间 Coding Space

DDD 领域驱动设计深入原理

DDD(Domain-Driven Design)是一种以业务领域为核心的软件设计方法。核心思想:代码结构应该反映业务结构,不是按技术分层,而是按业务领域划分。


一、传统分层架构有什么问题

1.1 典型的"大泥球"

// 传统 MVC 分层:Controller → Service → DAO

UserController          // 处理 HTTP 请求
    ↓
UserService             // 所有业务逻辑都往里塞
    ↓                    // 验证、调用外部接口、发通知、写缓存... 全在这
UserDao                 // 数据库操作

问题:当业务变复杂后,UserService 会变成几千行的"上帝类",什么都往里塞,最后改不动也看不懂。

1.2 传统分层的核心矛盾

技术视角(传统分层):
  表现层 → 业务层 → 持久层 → 数据库

问题:业务逻辑散落在各层,一个需求修改要跨 4 层找代码

业务视角(DDD):
  订单域 → 商品域 → 用户域 → 支付域

优势:一个需求通常只改一个域,业务逻辑集中

1.3 什么时候需要 DDD

场景 用 DDD 用简单分层
业务逻辑复杂(多状态、多规则)
团队 > 5 人,多人改同一模块
CRUD 型系统(增删改查为主)
业务规则少、只是表单提交
希望代码反映业务、便于沟通

一句话:CRUD 项目不需要 DDD,复杂业务系统才需要。别为了 DDD 而 DDD。


二、核心概念

2.1 Entity(实体) vs Value Object(值对象)

Entity:有唯一标识,改了属性还是同一个东西
Value Object:没有唯一标识,属性相同就是同一个东西
// Entity:用户
//    即使改了名字,还是同一个用户(ID 没变)
public class User {
    private Long id;           // 唯一标识 — Entity 的标志

    private String name;       // 可以修改
    private Address address;   // 可以修改

    // 两个 Entity 是否相同?看 ID
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        return Objects.equals(id, ((User) o).id);
    }
}

// Value Object:地址
//    如果省市区都相同,就是同一个地址(不需要 ID)
public class Address {
    private String province;
    private String city;
    private String district;

    // 两个 Value Object 是否相同?看所有属性
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Address)) return false;
        Address a = (Address) o;
        return Objects.equals(province, a.province)
            && Objects.equals(city, a.city)
            && Objects.equals(district, a.district);
    }
}

// 使用
User user = new User(1001L, "张三", new Address("浙江", "杭州", "西湖"));
user.setName("李四");  // 改了名字,还是 User 1001

2.2 Aggregate(聚合) 与 Aggregate Root(聚合根)

聚合:一组强相关的 Entity + Value Object 的集合
聚合根:聚合的"入口",外部只能通过聚合根访问聚合内部的对象

举例:订单聚合

// 聚合根:Order — 外部只能通过它操作聚合内的对象
public class Order {  // ← 聚合根(Aggregate Root)

    private Long id;               // 订单ID
    private Long userId;           // 用户ID
    private OrderStatus status;    // 订单状态
    private BigDecimal totalPrice; // 总价

    private List<OrderItem> items; // ← 订单明细(聚合内部对象)

    // 业务方法:不是简单的 setter,而是有业务含义的方法
    public void addItem(Long productId, BigDecimal price, int quantity) {
        // 业务规则验证
        if (status != OrderStatus.DRAFT) {
            throw new BizException("只有草稿状态才能添加商品");
        }

        OrderItem item = new OrderItem(productId, price, quantity);
        items.add(item);

        // 重新计算总价
        recalculateTotalPrice();
    }

    public void submit() {
        if (items.isEmpty()) {
            throw new BizException("订单不能为空");
        }
        this.status = OrderStatus.SUBMITTED;
    }

    public void pay() {
        if (status != OrderStatus.SUBMITTED) {
            throw new BizException("只有已提交的订单才能支付");
        }
        this.status = OrderStatus.PAID;
    }

    private void recalculateTotalPrice() {
        this.totalPrice = items.stream()
            .map(OrderItem::getSubTotal)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

// OrderItem 是聚合内部对象,不能独立于 Order 存在
// 没有 ID 作为独立 Entity,它的生命周期由 Order 控制
public class OrderItem {  // ← 聚合内部对象(不是聚合根)

    private Long productId;
    private BigDecimal price;
    private int quantity;

    public BigDecimal getSubTotal() {
        return price.multiply(BigDecimal.valueOf(quantity));
    }
}

聚合根规则

1. 外部只能通过聚合根访问聚合内部的实体
   ✅ orderRepository.findById(id)    // 通过聚合根
   ❌ orderItemRepository.findById()   // 不存在这个 Repository!

2. 聚合根内部保证业务规则的一致性
   在 addItem() 里检查状态,在 pay() 里检查前置条件

3. 一次操作只修改一个聚合
   不要在一个用例里同时修改 Order 和 User 两个聚合

2.3 Repository(仓储)— 聚合的"数据接口"

// Repository 只为聚合根定义,不是每个表一个
// 它把数据库操作封装起来,让业务代码不感知 MySQL/Redis

public interface OrderRepository {

    // 根据 ID 查完整的订单聚合
    Optional<Order> findById(Long orderId);

    // 保存整个聚合(订单 + 明细一起持久化)
    void save(Order order);

    // 按条件查询
    List<Order> findByUserIdAndStatus(Long userId, OrderStatus status);

    // 注意:不存在 OrderItemRepository!
    // 因为 OrderItem 不是聚合根,只能通过 Order 操作
}

// 实现层(Infrastructure)
@Repository
public class OrderRepositoryImpl implements OrderRepository {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private OrderItemMapper orderItemMapper;

    @Override
    public Optional<Order> findById(Long orderId) {
        OrderDO orderDO = orderMapper.selectById(orderId);
        if (orderDO == null) return Optional.empty();

        List<OrderItemDO> itemDOs = orderItemMapper.selectByOrderId(orderId);

        // DO → Domain 对象转换
        return Optional.of(OrderConverter.toDomain(orderDO, itemDOs));
    }

    @Override
    public void save(Order order) {
        // 保存订单 + 明细(全量替换或增量更新)
        OrderDO orderDO = OrderConverter.toDO(order);
        orderMapper.insertOrUpdate(orderDO);

        // 先删旧明细,再插新明细
        orderItemMapper.deleteByOrderId(order.getId());
        List<OrderItemDO> itemDOs = OrderItemConverter.toDOList(order.getItems());
        orderItemMapper.batchInsert(itemDOs);
    }
}

2.4 Domain Service(领域服务)

当业务逻辑不属于任何一个 Entity 时,放在 Domain Service:

// 场景:转账 — 涉及两个账户,不属于单个 Entity 的行为
// 这个逻辑放在 Account 聚合里不合适,放在 Domain Service 里
@Service
public class TransferService {  // 领域服务(无状态)

    @Autowired
    private AccountRepository accountRepository;

    @Transactional
    public void transfer(Long fromAccountId, Long toAccountId, BigDecimal amount) {
        // 1. 加载两个 Account 聚合
        Account from = accountRepository.findById(fromAccountId)
            .orElseThrow(() -> new BizException("转出账户不存在"));
        Account to = accountRepository.findById(toAccountId)
            .orElseThrow(() -> new BizException("转入账户不存在"));

        // 2. 执行领域逻辑(在聚合上操作)
        from.debit(amount);  // 扣钱
        to.credit(amount);   // 加钱

        // 3. 持久化
        accountRepository.save(from);
        accountRepository.save(to);
    }
}

2.5 Domain Event(领域事件)

聚合内部发生了重要事情,通知其他模块:

// 1. 定义领域事件
public class OrderPaidEvent {
    private final Long orderId;
    private final Long userId;
    private final BigDecimal amount;
    private final LocalDateTime paidAt;

    public OrderPaidEvent(Long orderId, Long userId, BigDecimal amount) {
        this.orderId = orderId;
        this.userId = userId;
        this.amount = amount;
        this.paidAt = LocalDateTime.now();
    }
    // getters...
}

// 2. 在聚合内注册事件
public class Order {
    private List<Object> domainEvents = new ArrayList<>();

    public void pay() {
        if (status != OrderStatus.SUBMITTED) {
            throw new BizException("只有已提交的订单才能支付");
        }
        this.status = OrderStatus.PAID;

        // 注册领域事件(不直接发 MQ,而是先记录)
        domainEvents.add(new OrderPaidEvent(id, userId, totalPrice));
    }

    // 调用方拿到 events 后发布
    public List<Object> getDomainEvents() {
        return domainEvents;
    }
}

// 3. Service 层发布事件
@Service
public class OrderApplicationService {

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Transactional
    public void pay(Long orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new BizException("订单不存在"));

        order.pay();
        orderRepository.save(order);

        // 发布领域事件 → 通知积分服务、短信服务等
        for (Object event : order.getDomainEvents()) {
            eventPublisher.publishEvent(event);
        }
    }
}

// 4. 监听事件(异步处理,不阻塞主流程)
@Component
public class OrderPaidListener {

    @EventListener
    @Async  // 异步执行
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)  // 事务提交后
    public void onOrderPaid(OrderPaidEvent event) {
        // 通知积分服务加积分
        // 通知短信服务发短信
        pointsService.addPoints(event.getUserId(), event.getAmount());
    }
}

三、战略设计

3.1 Bounded Context(限界上下文)

一个问题:同一个词在不同部门有不同含义。

销售部门说"订单" → 包含客户信息、折扣、总额
仓库部门说"订单" → 包含物流信息、库存流转
财务部门说"订单" → 包含收款状态、发票信息

这三个"订单"是三个不同的上下文

限界上下文 = 一个业务能力的边界。每个上下文有自己的模型、自己的语言。

电商系统限界上下文划分:

┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│  商品上下文   │  │  订单上下文   │  │  支付上下文   │
│              │  │              │  │              │
│ Product      │  │ Order        │  │ Payment      │
│ Category     │  │ OrderItem    │  │ Transaction  │
│ Inventory    │  │ OrderStatus  │  │ PaymentMethod│
└──────────────┘  └──────────────┘  └──────────────┘

┌──────────────┐  ┌──────────────┐
│  用户上下文   │  │  营销上下文   │
│              │  │              │
│ User         │  │ Coupon       │
│ Address      │  │ Promotion    │
│ MemberLevel  │  │ Campaign     │
└──────────────┘  └──────────────┘

3.2 Context Map(上下文映射)

不同限界上下文如何协作:

订单上下文 ←→ 商品上下文
     │             │
     │ 共享内核     │
     │ (Shared Kernel)
     ▼             ▼

订单需要查商品名称 → 通过 API 调用(Customer-Supplier)
订单创建后 → 通过领域事件通知支付上下文(Event-Driven)
映射模式 说明 示例
Customer-Supplier 下游调用上游 API 订单服务调商品服务查库存
Shared Kernel 共享一个核心模型 两个上下文用同一个 User 模型
Conformist 下游完全接受上游模型 使用第三方支付 SDK 的模型
Anti-Corruption Layer 隔离层,防止外部模型污染 对接老旧系统时,加一层转换
Open Host Service 对外提供统一的 REST API 多个系统通过同一接口访问订单
Published Language 使用统一的文档格式 JSON Schema、Protocol Buffers

3.3 Anti-Corruption Layer(防腐层)

// 场景:对接一个老旧系统的用户服务
// 老旧系统返回的用户模型非常丑:
// {"user_id": "1001", "user_nm": "zhangsan", "phn_num": "138..."}

// 1. 老系统的模型(不要直接引用到你的业务里)
public class LegacyUserDTO {
    private String user_id;
    private String user_nm;
    private String phn_num;
}

// 2. 防腐层:把旧模型转成你领域内的模型
@Component
public class UserAntiCorruptionLayer {

    public User convert(LegacyUserDTO dto) {
        return new User(
            Long.parseLong(dto.getUser_id()),
            dto.getUser_nm(),
            new PhoneNumber(dto.getPhn_num())
        );
    }
}

// 3. 业务代码只用领域模型 User,完全不感知 LegacyUserDTO
//    如果哪天旧系统换了,只需要改防腐层

四、DDD 项目目录结构

com.example.order/
│
├── interfaces/                    # 接口层(Controller)
│   └── OrderController.java       # REST API,只负责接收请求和返回结果
│
├── application/                   # 应用服务层(编排)
│   ├── OrderApplicationService.java  # 编排领域逻辑,处理事务
│   └── dto/
│       ├── CreateOrderCommand.java   # 入参
│       └── OrderResult.java          # 出参
│
├── domain/                        # 领域层(核心,不依赖外部框架)
│   ├── model/                     # 聚合、实体、值对象
│   │   ├── Order.java             # 聚合根
│   │   ├── OrderItem.java         # 实体
│   │   ├── OrderStatus.java       # 枚举
│   │   └── Address.java           # 值对象
│   ├── service/                   # 领域服务
│   │   └── OrderDomainService.java
│   ├── repository/                # 仓储接口(只定义接口)
│   │   └── OrderRepository.java
│   └── event/                     # 领域事件
│       └── OrderPaidEvent.java
│
└── infrastructure/                # 基础设施层(实现仓储、调用外部)
    ├── repository/
    │   └── OrderRepositoryImpl.java  # 仓储接口的实现
    ├── converter/
    │   └── OrderConverter.java       # Domain ↔ DO 转换
    ├── external/
    │   └── UserFeignClient.java      # 外部服务调用
    └── mq/
        └── OrderEventPublisher.java  # 消息发布

依赖方向

interfaces → application → domain ← infrastructure
                              ↑
                         基础设施实现领域定义的接口

domain 层不依赖任何外部框架(Spring、MyBatis、Redis)
只有纯 Java 代码

为什么? → 领域逻辑纯度高,可以独立测试,技术栈换了不影响业务

五、完整实战示例

5.1 应用服务层(编排)

// Application Service:编排领域对象,但自己不写业务逻辑
//    业务规则在 domain 层的 Order 里
//    Application Service 只负责:加载聚合 → 调用方法 → 保存
@Service
public class OrderApplicationService {

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private ProductFeignClient productFeignClient;

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    /**
     * 创建订单
     */
    @Transactional
    public Long createOrder(CreateOrderCommand command) {
        // 1. 校验(用 Feign 同步查商品信息)
        ProductVO product = productFeignClient.getProduct(command.getProductId());
        if (product.getStock() < command.getQuantity()) {
            throw new BizException("库存不足");
        }

        // 2. 创建订单聚合(业务规则在 Order 内部)
        Order order = Order.create(
            command.getUserId(),
            command.getProductId(),
            product.getPrice(),
            command.getQuantity()
        );

        // 3. 持久化
        orderRepository.save(order);

        // 4. 发布领域事件(事务提交后)
        //    不在这里直接发 MQ!用 @TransactionalEventListener 异步发
        for (Object event : order.getDomainEvents()) {
            eventPublisher.publishEvent(event);
        }

        return order.getId();
    }

    /**
     * 支付订单
     */
    @Transactional
    public void pay(Long orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new BizException("订单不存在"));

        order.pay();  // 业务规则在 pay() 里

        orderRepository.save(order);
    }
}

5.2 领域层(业务规则)

// Domain Object:包含业务规则,不是贫血模型
public class Order {

    private Long id;
    private Long userId;
    private Long productId;
    private BigDecimal unitPrice;
    private int quantity;
    private BigDecimal totalPrice;
    private OrderStatus status;
    private LocalDateTime createdAt;
    private List<Object> domainEvents = new ArrayList<>();

    // 私有不含参构造 — 通过静态工厂方法创建
    private Order() {}

    /**
     * 静态工厂方法 — 创建订单
     * 业务规则:
     * 1. 数量必须 > 0
     * 2. 单价必须 > 0
     * 3. 自动计算总价
     * 4. 初始状态为 DRAFT
     */
    public static Order create(Long userId, Long productId,
                                BigDecimal price, int quantity) {
        if (quantity <= 0) {
            throw new BizException("数量必须大于 0");
        }
        if (price.compareTo(BigDecimal.ZERO) <= 0) {
            throw new BizException("单价必须大于 0");
        }

        Order order = new Order();
        order.userId = userId;
        order.productId = productId;
        order.unitPrice = price;
        order.quantity = quantity;
        order.totalPrice = price.multiply(BigDecimal.valueOf(quantity));
        order.status = OrderStatus.DRAFT;
        order.createdAt = LocalDateTime.now();

        // 注册领域事件
        order.domainEvents.add(new OrderCreatedEvent(order));

        return order;
    }

    /**
     * 支付订单
     * 业务规则:只有已提交的订单才能支付
     */
    public void pay() {
        if (this.status != OrderStatus.SUBMITTED) {
            throw new BizException("只有已提交的订单才能支付");
        }
        this.status = OrderStatus.PAID;
        this.domainEvents.add(new OrderPaidEvent(this.id, this.userId, this.totalPrice));
    }

    /**
     * 取消订单
     * 业务规则:
     * 1. 已支付超过 1 小时的订单不能取消
     * 2. 已完成/已取消的订单不能取消
     */
    public void cancel() {
        if (this.status == OrderStatus.COMPLETED || this.status == OrderStatus.CANCELLED) {
            throw new BizException("订单已完成或已取消,不能取消");
        }
        if (this.status == OrderStatus.PAID
            && Duration.between(this.createdAt, LocalDateTime.now()).toHours() > 1) {
            throw new BizException("已支付超过 1 小时,不能取消");
        }
        this.status = OrderStatus.CANCELLED;
    }

    // getters...
    public Long getId() { return id; }
    public List<Object> getDomainEvents() { return domainEvents; }
}

六、常见误区

6.1 贫血模型 ≠ DDD

// ❌ 贫血模型:只有 getter/setter,业务逻辑全在 Service 里
//    这是传统的分层架构,不是 DDD
public class Order {
    private Long id;
    private String status;
    // 只有 getter/setter,没有任何业务方法
    public void setStatus(String status) { this.status = status; }
}

// ✅ 充血模型:业务规则和数据在一起
public class Order {
    private Long id;
    private OrderStatus status;

    public void pay() {
        if (status != SUBMITTED) throw new IllegalStateException();
        this.status = PAID;
    }
    // 不让外部直接 setStatus,只能通过业务方法修改状态
}

6.2 每个表都要建一个聚合根?

误区:我有个 order_item 表,是不是要建一个 OrderItem 聚合根?

正确做法:
  - OrderItem 的生命周期由 Order 控制
  - 不存在独立的 OrderItemRepository
  - OrderItem 通过 Order 聚合根访问

一个聚合根 = 一个事务边界 = 一个业务一致性单元

6.3 聚合根上不要做跨聚合的事务

// ❌ 在一个方法里修改两个聚合
@Transactional
public void transfer() {
    orderRepository.save(order);        // 聚合1:订单
    accountRepository.save(account);    // 聚合2:账户
}
// 两个聚合在同一个事务里 = 耦合

// ✅ 用领域事件 + 最终一致性
@Transactional
public void pay() {
    order.pay();
    orderRepository.save(order);       // 只修改订单聚合

    // 发领域事件,异步处理账户操作
    eventPublisher.publishEvent(new OrderPaidEvent(order));
}

6.4 Repository 不要返回 DTO

// ❌ 错误:Repository 返回了展示层的东西
public interface OrderRepository {
    List<OrderDTO> findByUserId(Long userId);  // DTO 不是领域概念
}

// ✅ 正确:Repository 返回领域对象
public interface OrderRepository {
    List<Order> findByUserId(Long userId);  // 返回聚合根
}

七、DDD 的代价与取舍

7.1 DDD 的成本

成本 说明
学习曲线 概念多(聚合、限界上下文、领域事件...),团队需要培训
前期投入大 分析业务、划分上下文,比直接写 CRUD 慢
过度设计风险 简单的增删改查也用 DDD = 杀鸡用牛刀
性能开销 聚合整体加载、领域事件异步,可能引入额外复杂度

7.2 什么时候不该用 DDD

1. 纯 CRUD 系统(管理后台、表单提交)
2. 业务规则极少(只做转发、透传)
3. 团队 < 3 人,沟通成本低
4. 遗留系统改造(成本太高,不如局部优化)
5. 赶进度的小项目(以后可以重构)

7.3 务实建议

不要求完美的 DDD:

1. 先让你的代码按功能模块分目录
   (比按技术分层好)

2. 把核心业务放到 domain 包里
   (让业务逻辑和数据在一起)

3. 使用充血模型,而不是贫血模型
   (把业务方法放到对象里,而不是 Service 里)

4. 业务不复杂就保持简单
   (用 @Transactional + MQ 事务消息保证一致性就够了)

八、总结

概念 一句话 类比
Entity 有 ID,改了属性还是同一个东西 你改了名字还是你
Value Object 没有 ID,属性相同就是同一个 坐标 (x,y) 完全相同时就是同一个位置
Aggregate 一组强关联对象的集合 订单 + 订单明细是一个整体
Aggregate Root 聚合的"门",外部只能走这个门 订单是门,不能绕过订单直接改订单明细
Repository 聚合的"数据仓库" 仓库管理员,存取聚合
Domain Service 不属于某个 Entity 的业务逻辑 转账(涉及两个账户)
Domain Event 聚合内发生了重要事情 订单支付成功 → 通知积分服务
Bounded Context 一个业务能力的边界 订单上下文 vs 支付上下文
Anti-Corruption Layer 隔离外部系统的旧模型 翻译官,把旧系统的话翻译成你能懂的