领域驱动设计模式:聚合、值对象、领域事件、领域服务与分层架构

领域驱动设计,简称DDD。说实话,我第一次接触这个概念是在一个大型电商项目中。当时团队被复杂的业务逻辑搞得焦头烂额,代码里到处都是if-else,改一个需求要翻遍十几个文件。后来我们引入了DDD,才慢慢把局面扭转过来。

今天我就把DDD中最核心的几个概念——聚合、值对象、领域事件、领域服务,以及分层架构——给你讲透。这些东西不是理论空谈,都是我在实战中踩过坑、填过坑后沉淀下来的经验。

1. 值对象:不可变的小零件

先说说值对象。很多人觉得它不重要,其实恰恰相反。值对象是DDD里最容易被忽视、但最有价值的概念之一。

值对象的特点是:没有唯一标识,只描述属性。比如地址、金额、颜色、坐标这些。两个值对象如果属性值相同,我们就认为它们是同一个对象。

核心特征:

  • 不可变性——创建后就不能修改
  • 通过属性值比较相等性
  • 没有全局唯一ID
  • 可以包含行为(比如金额的加法)

举个例子。你想想看,一个订单里的收货地址,如果地址变了,是修改原来的对象还是新建一个?按DDD的规矩,应该新建。因为地址变了,本质上就是一个新地址。

// Java 示例:值对象
public final class Address {
    private final String province;
    private final String city;
    private final String detail;
    
    public Address(String province, String city, String detail) {
        this.province = province;
        this.city = city;
        this.detail = detail;
    }
    
    // 只提供getter,不提供setter
    public String getProvince() { return province; }
    public String getCity() { return city; }
    public String getDetail() { return detail; }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Address)) return false;
        Address address = (Address) o;
        return province.equals(address.province) &&
               city.equals(address.city) &&
               detail.equals(address.detail);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(province, city, detail);
    }
}

我的经验:我曾经在一个项目中把金额设计成实体,结果每次修改金额都要走一遍ORM的更新流程,性能差不说,还容易产生并发问题。后来改成值对象,用不可变的方式处理,问题迎刃而解。

2. 聚合:业务一致性的边界

聚合是DDD里最难理解、也最容易出错的概念。说白了,聚合就是一组相关对象的集合,我们把它当作一个整体来操作。

每个聚合都有一个聚合根,它是访问聚合的唯一入口。外部不能直接操作聚合内部的子对象,必须通过聚合根来操作。

为什么要这么设计?我遇到过这样一个场景:订单和订单项。如果允许外部直接修改订单项的数量和价格,那订单的总金额就可能不一致。这就是数据不一致的根源。

// Java 示例:聚合根
public class Order {
    private OrderId id;          // 聚合根ID
    private List<OrderItem> items;
    private Money totalAmount;
    
    // 外部只能通过聚合根的方法来操作
    public void addItem(Product product, int quantity) {
        OrderItem item = new OrderItem(product, quantity);
        items.add(item);
        recalculateTotalAmount();
    }
    
    public void removeItem(OrderItemId itemId) {
        items.removeIf(item -> item.getId().equals(itemId));
        recalculateTotalAmount();
    }
    
    private void recalculateTotalAmount() {
        this.totalAmount = items.stream()
            .map(OrderItem::getSubtotal)
            .reduce(Money.ZERO, Money::add);
    }
}

避坑指南:我曾经把一个聚合设计得太大,包含了十几个实体。结果每次操作都要加载大量数据,性能惨不忍睹。后来我学乖了:聚合要小,越小越好。一个聚合通常只包含3-5个对象就够了。

3. 领域事件:解耦的利器

领域事件是DDD里我最喜欢的概念。它让不同聚合之间可以松耦合地通信。

举个例子。订单支付成功后,需要做三件事:更新订单状态、发送通知、更新库存。如果把这些逻辑都写在订单服务里,代码会变得非常臃肿。用领域事件就清爽多了。

// Java 示例:领域事件
public class OrderPaidEvent implements DomainEvent {
    private final OrderId orderId;
    private final Money paidAmount;
    private final LocalDateTime occurredOn;
    
    public OrderPaidEvent(OrderId orderId, Money paidAmount) {
        this.orderId = orderId;
        this.paidAmount = paidAmount;
        this.occurredOn = LocalDateTime.now();
    }
    
    // getter...
}

// 事件处理器
@Component
public class OrderPaidEventHandler {
    
    @EventListener
    public void handle(OrderPaidEvent event) {
        // 1. 更新订单状态
        orderRepository.updateStatus(event.getOrderId(), OrderStatus.PAID);
        
        // 2. 发送通知
        notificationService.sendPaymentSuccess(event.getOrderId());
        
        // 3. 更新库存
        inventoryService.deductStock(event.getOrderId());
    }
}

你想想看,如果以后要加一个新功能,比如支付成功后给用户积分,只需要新增一个事件处理器就行,完全不用改原有代码。这就是开闭原则的完美体现。

4. 领域服务:处理跨聚合的逻辑

有些业务逻辑不属于任何一个实体或值对象。比如转账,它涉及两个账户。这时候就需要领域服务出场了。

领域服务是无状态的,它只负责协调多个聚合完成一个业务操作。

// Java 示例:领域服务
public class TransferService {
    
    private final AccountRepository accountRepository;
    
    public TransferService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }
    
    public void transfer(AccountId fromId, AccountId toId, Money amount) {
        Account from = accountRepository.findById(fromId);
        Account to = accountRepository.findById(toId);
        
        // 业务逻辑:扣款和入账
        from.withdraw(amount);
        to.deposit(amount);
        
        // 保存聚合
        accountRepository.save(from);
        accountRepository.save(to);
        
        // 发布领域事件
        DomainEventPublisher.publish(new TransferCompletedEvent(fromId, toId, amount));
    }
}

我的建议:领域服务不要做得太厚。如果发现领域服务里全是if-else,那说明业务逻辑放错地方了。应该把逻辑下沉到实体或值对象里。

5. DDD分层架构

最后说说分层架构。DDD推荐四层架构:

层次 职责 包含内容
用户接口层 接收用户请求,返回响应 Controller、DTO、View
应用层 编排业务场景,协调领域对象 ApplicationService、DTO转换
领域层 核心业务逻辑 实体、值对象、聚合、领域服务、领域事件
基础设施层 技术实现 Repository实现、消息队列、缓存

这里有个关键原则:依赖倒置。领域层不依赖任何外部框架,基础设施层依赖领域层定义的接口。

我见过太多项目把业务逻辑写在Controller里,或者直接操作数据库。这样做的后果就是:业务逻辑散落一地,改一个需求要动好几个层。

DDD四层架构 用户接口层 (User Interface) Controller / DTO / View 应用层 (Application) ApplicationService / 任务编排 领域层 (Domain) 实体 / 值对象 / 聚合 / 领域服务 / 领域事件 核心业务逻辑,不依赖任何框架 基础设施层 (Infrastructure) Repository实现 / 消息队列 / 缓存 / ORM 依赖方向:外层 → 内层

嗯,这里要注意:应用层和领域层的区别。应用层只负责编排,不包含业务逻辑。真正的业务规则必须放在领域层。我见过有人把业务逻辑写在应用层,结果领域层变成了贫血模型,这就失去了DDD的意义。

总结一下我的经验:

  • 值对象用不可变,能省很多并发问题
  • 聚合要小,边界要清晰
  • 领域事件是解耦神器,多用没坏处
  • 领域服务只做协调,不做具体逻辑
  • 分层架构的核心是依赖倒置

DDD不是银弹,但它确实能帮我们应对复杂业务。我建议你从小项目开始尝试,先在一个模块里用起来,慢慢体会它的好处。别一上来就全盘推翻重写,那样风险太大。


公众号:蓝海资料掘金营,微信deep3321