DDD 领域驱动设计-看我如何应对业务需求变化?
DDD 领域驱动设计-看我如何应对业务需求变化?
作为程序员,我们最怕听到的一句话是什么?不是“有 Bug”,而是“需求变了”。需求变化是软件开发中唯一不变的事情。传统的贫血模型(Anemic Domain Model)往往将业务逻辑散落在 Service 层,一旦需求变动,我们就像在泥潭里挣扎——改一处,崩三处。而DDD(Domain-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 List<OrderItem> 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 元需要人工复核”,我们新增一个监听器:java@Componentpublic 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 的战术模式——你会发现,变化不再是灾难,而是系统演进的正常节奏。毕竟,需求变化是常态,而我们的目标是让变化可控。
