
DDD 领域驱动设计看我如何应对业务需求变化作为程序员我们最怕听到的一句话是什么不是“有 Bug”而是“需求变了”。需求变化是软件开发中唯一不变的事情。传统的贫血模型Anemic Domain Model往往将业务逻辑散落在 Service 层一旦需求变动我们就像在泥潭里挣扎——改一处崩三处。而DDDDomain-Driven Design领域驱动设计提供了一套应对复杂业务变化的思维框架和工程实践。今天我将带你从零开始逐步理解 DDD 的核心概念并展示它是如何优雅地应对需求“七十二变”的。—## 1. 基础概念从“贫血模型”到“充血模型”传统开发中我们常写这样的代码java// 贫血模型实体只有 getter/setter没有行为public class Order { private Long id; private String status; private BigDecimal amount; public Long getId() { return id; } public void setId(Long id) { this.id id; } // ... 省略其他 getter/setter}// Service 层承载所有业务逻辑Servicepublic class OrderService { public void pay(Order order) { if (!CREATED.equals(order.getStatus())) { throw new RuntimeException(订单状态不正确); } // 调用支付网关... order.setStatus(PAID); orderRepository.save(order); }}这种模式的问题在于业务规则状态校验、支付流程全部耦合在 Service 中。当订单状态增多如待发货、已发货、已取消、退款中Service 方法会爆炸式增长且每个方法都要重复写校验逻辑。DDD 的解决思路将业务行为内聚到实体中形成“充血模型”。实体不仅包含数据还包含行为业务规则。我们来看第一个代码示例java// 充血模型实体自带行为业务规则内聚public class Order { private Long id; private OrderStatus status; private BigDecimal amount; // 业务行为支付 public void pay() { // 业务规则只有 CREATED 状态才能支付 if (this.status ! OrderStatus.CREATED) { throw new IllegalStateException(只有新建订单可以支付); } // 调用支付网关通过领域服务后面会讲 // 状态流转 this.status OrderStatus.PAID; } // 业务行为取消 public void cancel() { if (this.status OrderStatus.SHIPPED) { throw new IllegalStateException(已发货订单不能取消); } this.status OrderStatus.CANCELLED; }}关键变化我们把“支付前状态校验”和“状态流转”封装在Order实体内部。以后需求变动比如“已发货订单若超过 7 天可以取消”我们只需要修改cancel()方法而不用去翻 Service。这就是战术建模的第一步让实体成为有血有肉的“领域对象”。—## 2. 核心模式聚合与值对象当业务越来越复杂一个实体往往不能独立存在。比如一个“订单”包含多个“订单项”一个“用户”包含多个“地址”。DDD 引入了聚合Aggregate概念将一组强关联的对象视为一个整体外部只能通过聚合根Aggregate Root访问内部成员。java// 值对象不可变无标识public 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}// 聚合根Order 管理 OrderItem 的生命周期public class Order { private Long id; private ListOrderItem items; // 私有外部不能直接操作 private Address deliveryAddress; // 值对象 // 添加订单项业务规则例如不能添加已支付订单的项 public void addItem(Product product, int quantity) { if (this.status OrderStatus.PAID) { throw new IllegalStateException(已支付订单不能修改商品); } this.items.add(new OrderItem(product, quantity)); } // 计算总价遍历内部 items public BigDecimal totalAmount() { return items.stream() .map(OrderItem::getSubtotal) .reduce(BigDecimal.ZERO, BigDecimal::add); }}为什么要这样设计因为业务需求经常变。比如新需求“订单总价满 100 元免运费”。我们可以直接在totalAmount()中加逻辑。但更重要的聚合保证了一致性边界——外部不能绕过订单直接修改订单项所有变更必须通过聚合根方法。这样需求变化时我们只需要关注聚合根不会破坏内部关联。—## 3. 进阶应用领域服务和领域事件有些业务行为不属于任何实体或值对象。比如“转账”涉及两个账户“下单”需要校验库存和用户余额。这时我们需要领域服务Domain Service来协调多个领域对象。java// 领域服务跨聚合的业务逻辑Servicepublic class TransferService { // 转账涉及两个 Account 聚合 public void transfer(Account from, Account to, BigDecimal amount) { // 领域规则余额不足不能转账 if (from.getBalance().compareTo(amount) 0) { throw new InsufficientBalanceException(余额不足); } from.debit(amount); // 扣款 to.credit(amount); // 加款 // 发布领域事件通知风控、审计等 DomainEventPublisher.publish(new MoneyTransferredEvent(from.getId(), to.getId(), amount)); }}领域事件Domain Event是 DDD 中应对需求变化的高级武器。当需求变成“转账后发送短信通知”“转账后触发反洗钱检查”我们不需要修改TransferService只需要监听MoneyTransferredEvent。这就是事件驱动解耦。比如新需求“每笔转账超过 5000 元需要人工复核”我们新增一个监听器javaComponentpublic class LargeTransferHandler { EventListener public void onTransfer(MoneyTransferredEvent event) { if (event.getAmount().compareTo(new BigDecimal(5000)) 0) { // 创建复核任务 approvalService.createApproval(event.getTransactionId()); } }}注意我们完全没动原来的转账逻辑只是新增了一个监听类。这种开闭原则的体现正是 DDD 带来的最大价值之一。—## 4. 战略设计限界上下文与上下文映射图以上都是战术建模代码实现层面。但要应对真正的业务需求变化我们需要从宏观视角 ——战略设计。一个大型系统可能有“订单上下文”“库存上下文”“支付上下文”。每个上下文有自己独立的模型和语言。例如“商品”在“销售上下文”中叫Product具有price、stock属性在“仓储上下文”中叫Item具有location、quantity属性。如果我们强行统一成一个类需求变化时必然灾难。限界上下文Bounded Context就是给模型划清边界。上下文之间通过防腐层Anti-Corruption Layer通信防止内部模型被外部污染。python# 假设用 Python 演示防腐层模式# 仓储上下文外部系统class WarehouseItem: def __init__(self, sku, location, qty): self.sku sku self.location location self.qty qty# 销售上下文我们的核心领域class Product: def __init__(self, sku, name): self.sku sku self.name name self.saleable_quantity 0 # 防腐层方法将外部仓储模型转换为内部模型 classmethod def from_warehouse_item(cls, warehouse_item, name): product cls(warehouse_item.sku, name) # 只取我们需要的数据忽略其他字段 product.saleable_quantity warehouse_item.qty return product# 使用防腐层warehouse_item WarehouseItem(SKU123, A区-01, 100)product Product.from_warehouse_item(warehouse_item, 手机)print(f商品 {product.name} 可售数量{product.saleable_quantity})为什么这能应对需求变化比如仓储系统改版把qty改为available_qty我们只需要修改from_warehouse_item方法而销售上下文的Product类完全不受影响。边界清晰变更局部化。—## 5. 实战用 DDD 重构一个需求变更案例假设我们有一个电商系统原始需求是“订单支付后修改状态”。现在新需求来了“支付成功后如果商品是虚拟商品如充值卡需要立即自动发货如果是实体商品则进入待发货状态”。传统写法贫血模型会变成java// 传统 Service 层开始堆积 if-elsepublic void payOrder(Long orderId) { Order order orderRepo.findById(orderId); if (order.getType() VIRTUAL) { // 自动发货逻辑 deliveryService.autoDeliver(order); } else { order.setStatus(WAIT_SHIP); } // 还有更多 if...}DDD 写法我们把“支付后行为”建模为领域事件OrderPaidEvent并让不同聚合自行监听java// 领域事件public class OrderPaidEvent { private final Order order; public OrderPaidEvent(Order order) { this.order order; }}// 虚拟商品订单聚合public class VirtualOrder extends Order { Override public void onPaid() { this.status DELIVERED; // 自动发货 // 发送卡密等 }}// 实体商品订单聚合public class PhysicalOrder extends Order { Override public void onPaid() { this.status WAIT_SHIP; }}// 事件监听器Componentpublic class OrderPaidListener { EventListener public void handle(OrderPaidEvent event) { // 多态不同子类自行决定行为 event.getOrder().onPaid(); }}关键点新需求来临时我们只需要增加VirtualOrder子类修改onPaid()方法而不需要改动任何 Service。如果未来新增“电子书订单”再增加一个子类即可。这就是多态 事件驱动的威力。—## 总结回到标题DDD 如何应对业务需求变化1.充血模型业务规则内聚在实体中需求变化修改实体方法而不是散落各处。2.聚合边界通过聚合根管理一致性外部变更无法破坏内部状态变更局部化。3.领域事件解耦核心流程与扩展逻辑新增需求如通知、风控只需增加监听器。4.限界上下文不同模块独立建模通过防腐层隔离外部变化避免全局影响。5.战略设计从宏观上划分系统边界让团队可以各自演进减少沟通成本。当然DDD 不是银弹它适用于业务复杂、需求频繁变化的核心领域。对于简单的 CRUD 系统过度设计反而增加负担。但如果你正在经历“每次需求变更都如临大敌”的痛苦不妨从今天开始在核心业务模块尝试引入 DDD 的战术模式——你会发现变化不再是灾难而是系统演进的正常节奏。毕竟需求变化是常态而我们的目标是让变化可控。