领域驱动设计(DDD):驾驭复杂业务的架构哲学

引子:为什么我们需要 DDD?

想象你接手了一个电商系统的重构项目。打开代码一看:订单、支付、库存、物流的逻辑散落在各个 Service 里,一个 OrderService 有 3000 行代码,方法名叫 processOrder、handleOrder、dealOrder,看起来都差不多但又不知道具体干啥。更可怕的是,业务规则藏在各个角落:有的在 Controller 做校验,有的在 Service 计算,有的在数据库触发器里...

这就是典型的"贫血模型 + 事务脚本"带来的问题。而 DDD(Domain-Driven Design,领域驱动设计)就是为了解决这类问题而生的。


一、DDD 的核心思想:以领域为中心

1.1 什么是领域(Domain)?

领域就是你要解决的业务问题空间。 比如电商领域、金融领域、物流领域。

DDD 的第一个核心观点:软件的复杂度来自业务本身,而不是技术。 所以我们要把精力放在理解业务上,而不是一上来就想着用什么框架、什么中间件。

举个例子:

  • ❌ 传统思维:"这个功能需要一个订单表、一个订单详情表、然后写个 CRUD..."

  • ✅ DDD 思维:"在我们的业务里,'订单'代表什么?它有哪些状态?什么情况下可以取消?取消后库存怎么处理?"

1.2 通用语言(Ubiquitous Language)

DDD 强调团队要建立通用语言:开发、产品、运营都用同一套术语。

比如在电商领域:

  • "订单"不叫 Order,叫 PurchaseOrder(采购订单)

  • "支付"不叫 pay(),叫 completePayment()(完成支付动作)

  • "取消"不叫 delete(),叫 cancel()(业务动作)

代码就是文档,变量名就是业务语言。 当产品经理说"订单履约",你的代码里就应该有 OrderFulfillment 这个类。


二、战略设计:划分领域边界

2.1 限界上下文(Bounded Context)

这是 DDD 最重要的概念之一。不同的上下文中,同一个词可能有不同的含义。

以"商品"为例:

  • 商品上下文:商品有 SKU、SPU、规格、价格

  • 库存上下文:商品只关心库存数量、仓库位置

  • 营销上下文:商品关心优惠活动、促销标签

所以我们要划分限界上下文,每个上下文有自己的模型:

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   商品上下文    │    │   库存上下文    │    │   订单上下文    │
│                 │    │                 │    │                 │
│  Product        │───▶│  Stock          │◀───│  Order          │
│  - skuId        │    │  - skuId        │    │  - orderId      │
│  - name         │    │  - quantity     │    │  - items[]      │
│  - price        │    │  - warehouse    │    │  - totalAmount  │
└─────────────────┘    └─────────────────┘    └─────────────────┘

2.2 上下文映射(Context Mapping)

不同上下文之间如何交互?DDD 定义了几种模式:

1) 共享内核(Shared Kernel)

  • 两个上下文共享一部分模型

  • 适合紧密协作的团队

2) 客户-供应商(Customer-Supplier)

  • 下游依赖上游的接口

  • 比如订单上下文依赖库存上下文的接口查库存

3) 防腐层(Anti-Corruption Layer, ACL)

  • 这是最常用的!当你对接外部系统或遗留系统时,用 ACL 做一层转换

举个例子,对接第三方支付:

// 防腐层:隔离外部系统
public class PaymentAdapter {
    private ThirdPartyPaymentClient client;
    
    public PaymentResult pay(Order order) {
        // 将我们的领域模型转换为第三方格式
        ThirdPartyRequest request = convertToThirdParty(order);
        ThirdPartyResponse response = client.pay(request);
        
        // 将第三方结果转换为我们的领域模型
        return convertToDomain(response);
    }
}

防腐层的价值:当第三方系统变更或更换时,只需要修改 ACL,核心领域模型不受影响。

2.3 子域划分

把整个业务领域划分为:

  • 核心域(Core Domain):你的竞争力所在,比如电商的定价策略、推荐算法

  • 支撑域(Supporting Domain):必须有但不是核心竞争力,比如用户管理

  • 通用域(Generic Domain):可以买现成方案的,比如短信发送、文件存储

资源分配原则:核心域投入最好的人力,支撑域够用就行,通用域尽量用开源/SaaS。


三、战术设计:构建领域模型

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

实体:有唯一标识,生命周期中属性可变

public class Order {
    private OrderId id;  // 唯一标识
    private Money totalAmount;
    private OrderStatus status;
    
    // 业务方法
    public void cancel() {
        if (status == OrderStatus.PAID) {
            throw new OrderCannotCancelException("已支付订单不能取消");
        }
        this.status = OrderStatus.CANCELLED;
    }
}

值对象:没有标识,通过属性判断相等,不可变

public class Money {
    private final BigDecimal amount;
    private final Currency currency;
    
    public Money(BigDecimal amount, Currency currency) {
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("金额不能为负");
        }
        this.amount = amount;
        this.currency = currency;
    }
    
    public Money add(Money other) {
        // 返回新对象,不修改自己
        return new Money(this.amount.add(other.amount), this.currency);
    }
}

为什么要区分?

  • 实体关注"是谁"(身份),值对象关注"是什么"(属性)

  • 值对象可以共享、缓存,实体不行

  • 两个金额 100 元是相等的(值对象),但两个订单 ID 相同只能说明是同一个订单(实体)

3.2 聚合(Aggregate)与聚合根

聚合是一组相关对象的集合,有一个聚合根作为入口。

比如订单聚合:

Order (聚合根)
├── OrderItem (订单明细)
├── ShippingAddress (收货地址)
└── Payment (支付信息)

聚合的设计原则:

1) 边界内强一致性,边界外最终一致性

  • 订单和订单明细必须同时保存(强一致性)

  • 订单和库存可以异步更新(最终一致性)

2) 通过聚合根修改

// ❌ 错误:直接修改订单明细
orderItem.setQuantity(10);

// ✅ 正确:通过聚合根
order.updateItemQuantity(itemId, 10);

3) 聚合根持有其他实体的引用,外部只持有聚合根的 ID

public class Order {
    private OrderId id;
    private CustomerId customerId;  // 只持有 ID,不持有整个 Customer 对象
    private List<OrderItem> items;  // 持有内部实体
}

聚合大小的经验法则:

  • 聚合不要太大(控制在 2-3 层),否则并发冲突多、性能差

  • 如果两个对象总是一起修改,放在一个聚合;否则拆分

3.3 领域服务(Domain Service)

当一个业务逻辑不属于某个实体时,用领域服务。

比如"转账"操作涉及两个账户:

public class TransferService {
    public void transfer(Account from, Account to, Money amount) {
        from.debit(amount);  // 扣款
        to.credit(amount);   // 入账
        
        // 记录转账事件
        domainEventPublisher.publish(new MoneyTransferred(from.id(), to.id(), amount));
    }
}

领域服务 vs 应用服务:

  • 领域服务:包含业务逻辑,比如"转账规则"

  • 应用服务:编排流程,比如"调用转账、发通知、记日志"

3.4 仓储(Repository)

仓储是领域对象的持久化抽象,隐藏数据库细节。

public interface OrderRepository {
    Order findById(OrderId id);
    void save(Order order);
    List<Order> findByCustomerId(CustomerId customerId);
}

关键点:

  • 接口定义在领域层,实现在基础设施

  • 用领域语言(findByCustomerId),而不是 SQL 语言(selectByCustomerId)

  • 返回的是聚合根,而不是贫血的 PO 对象

3.5 领域事件(Domain Event)

当领域中发生重要的事情时,发布事件。

public class Order {
    public void pay() {
        this.status = OrderStatus.PAID;
        // 发布领域事件
        DomainEventPublisher.publish(new OrderPaidEvent(this.id, this.totalAmount));
    }
}

// 事件处理器
@EventHandler
public class OrderPaidEventHandler {
    public void handle(OrderPaidEvent event) {
        // 扣减库存、发送通知等
        inventoryService.reserve(event.getOrderId());
        notificationService.sendPaymentConfirmation(event.getOrderId());
    }
}

领域事件的价值:

  • 解耦:订单不需要知道支付后要做什么

  • 可扩展:新增需求时只需要加事件处理器

  • 最终一致性:跨聚合的操作通过事件异步完成


四、分层架构:让领域模型不受污染

DDD 典型的四层架构:

┌─────────────────────────────────────┐
│  User Interface Layer (接口层)      │  ← Controller、DTO
├─────────────────────────────────────┤
│  Application Layer (应用层)         │  ← ApplicationService、事务
├─────────────────────────────────────┤
│  Domain Layer (领域层)              │  ← Entity、ValueObject、DomainService
├─────────────────────────────────────┤
│  Infrastructure Layer (基础设施层)  │  ← Repository 实现、MQ、DB
└─────────────────────────────────────┘

依赖规则:只能向下依赖,领域层不依赖任何外层!

举个例子:

// 应用层:编排流程
public class OrderApplicationService {
    private OrderRepository orderRepository;
    private InventoryService inventoryService;
    
    @Transactional
    public void placeOrder(PlaceOrderCommand command) {
        // 1. 构建领域对象
        Order order = Order.create(command.getCustomerId(), command.getItems());
        
        // 2. 执行领域逻辑
        order.place();  // 领域方法
        
        // 3. 检查库存(领域服务)
        inventoryService.checkAndReserve(order.getItems());
        
        // 4. 持久化
        orderRepository.save(order);
    }
}


五、实战:从贫血模型到充血模型

贫血模型(反模式)

// 数据对象
public class Order {
    private Long id;
    private Integer status;
    private BigDecimal totalAmount;
    // 只有 getter/setter
}

// Service 里写所有逻辑
public class OrderService {
    public void cancelOrder(Long orderId) {
        Order order = orderDAO.selectById(orderId);
        if (order.getStatus() == 2) {
            throw new Exception("已支付不能取消");
        }
        order.setStatus(3);
        orderDAO.update(order);
    }
}

问题:

  • 业务逻辑散落在各个 Service

  • Order 对象没有行为,只是数据容器

  • 无法保证业务规则一致性(到处都能 setStatus)

充血模型(DDD)

// 领域对象:有数据也有行为
public class Order {
    private OrderId id;
    private OrderStatus status;
    private Money totalAmount;
    
    public void cancel() {
        if (status == OrderStatus.PAID) {
            throw new OrderCannotCancelException("已支付订单不能取消");
        }
        this.status = OrderStatus.CANCELLED;
        // 发布领域事件
        DomainEventPublisher.publish(new OrderCancelledEvent(this.id));
    }
    
    // 不提供 setStatus,只能通过业务方法修改状态
}

优势:

  • 业务规则封装在领域对象内

  • 状态变更有迹可循(通过方法名就知道发生了什么)

  • 测试简单(不需要数据库,直接测试领域对象)


六、DDD 的常见误区

误区 1:DDD = 微服务

  • DDD 是设计方法,微服务是架构风格

  • 可以在单体应用中用 DDD,也可以在微服务中不用 DDD

  • 但 DDD 的限界上下文确实为微服务拆分提供了指导

误区 2:DDD 必须用充血模型

  • 充血模型是 DDD 的常见实现方式,但不是唯一方式

  • 简单的 CRUD 场景用贫血模型也没问题

  • 只有复杂业务逻辑才需要 DDD

误区 3:为了 DDD 而 DDD

  • 不要过度设计,小项目用 DDD 是杀鸡用牛刀

  • DDD 的价值在于"驾驭复杂度",简单系统没必要

误区 4:DDD 就是设计很多类

  • 实体、值对象、聚合根...是工具,不是目的

  • 关键是理解业务、建立统一语言、划分清晰边界


七、DDD 在 Java 生态中的实践

7.1 框架选择

  • Spring Boot:最常见,用 @Service、@Repository 分层

  • Axon Framework:专为 CQRS + Event Sourcing 设计

  • JMolecules:提供 DDD 注解(@Entity、@ValueObject)

7.2 持久化

// 领域层:接口
public interface OrderRepository {
    Order findById(OrderId id);
    void save(Order order);
}

// 基础设施层:JPA 实现
@Repository
public class OrderJpaRepository implements OrderRepository {
    @PersistenceContext
    private EntityManager em;
    
    public Order findById(OrderId id) {
        OrderPO po = em.find(OrderPO.class, id.getValue());
        return OrderMapper.toDomain(po);  // PO → 领域对象
    }
    
    public void save(Order order) {
        OrderPO po = OrderMapper.toPO(order);  // 领域对象 → PO
        em.merge(po);
    }
}

7.3 事件驱动

// 使用 Spring Event
@Service
public class OrderDomainService {
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    
    public void payOrder(Order order) {
        order.pay();
        eventPublisher.publishEvent(new OrderPaidEvent(order.getId()));
    }
}

// 事件监听
@Component
public class InventoryEventListener {
    @EventListener
    @Async
    public void handle(OrderPaidEvent event) {
        // 扣减库存
    }
}


八、何时使用 DDD?

适合 DDD 的场景:

  • ✅ 业务逻辑复杂,规则多变

  • ✅ 需要长期维护迭代的系统

  • ✅ 团队有一定的技术能力

  • ✅ 需要多团队协作(划分限界上下文)

不适合 DDD 的场景:

  • ❌ 简单的 CRUD 系统

  • ❌ 短期项目、原型验证

  • ❌ 数据分析、报表类系统

  • ❌ 团队不熟悉 DDD(学习成本高)


九、总结:DDD 的本质

DDD 不是银弹,它的核心价值在于:

  1. 以业务为中心:技术服务于业务,而不是反过来

  2. 统一语言:减少沟通成本,代码即文档

  3. 清晰边界:通过限界上下文降低复杂度

  4. 封装变化:把易变的业务规则封装在领域模型中

记住 Eric Evans 的话:"DDD is not about technology, it's about discussions." DDD 的精髓在于和领域专家的深入交流,而不是炫技式地堆砌设计模式。


评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

C_x_330

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值