AI摘要
文章以供应链系统重构为例,展示从贫血模型到DDD的演进:先指出贫血模型“失忆、碎片化、难维护”的痛点;再给出富血实体、值对象、领域服务、聚合、领域事件等战术设计,让业务规则内聚于代码;通过测试、分层架构、渐进式迁移策略,实现“代码即文档”,最终达成业务与技术同频,提升系统可维护性与响应变化能力。
前言:在我工作的第四年,接手了一个“代码写得不错,但业务逻辑看不懂”的供应链系统。实体类里全是getter/setter,Service层里充斥着if(status == 1){...}else if(status == 2){...}。更可怕的是,业务专家说“订单超过30分钟未支付自动取消”,代码里却分散在三个地方:定时任务、支付回调、订单查询。这次经历让我意识到:如果代码不能清晰表达业务,那它只是在模拟业务,而不是实现业务。一、 从“数据表驱动”到“领域驱动”的认知转变
传统贫血模型的困局
先看看我接手的那个“反例”——一个典型的贫血模型实现:
// 贫血的订单实体(只是个数据容器)
@Data
@Entity
@Table(name = "t_order")
public class Order {
@Id
private Long id;
private String orderNo;
private BigDecimal amount;
private Integer status; // 1-待支付 2-已支付 3-已发货 4-已完成 5-已取消
private Long userId;
private LocalDateTime createTime;
private LocalDateTime payTime;
private LocalDateTime deliverTime;
private LocalDateTime completeTime;
private LocalDateTime cancelTime;
private String cancelReason;
// 还有30多个字段...
}
// 贫血的服务层(承载所有业务逻辑)
@Service
@Transactional
@Slf4j
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private PaymentService paymentService;
@Autowired
private InventoryService inventoryService;
@Autowired
private MessageService messageService;
/**
* 创建订单(业务逻辑分散在各个Service中)
*/
public Long createOrder(CreateOrderRequest request) {
// 1. 参数校验(业务规则?)
if (request.getItems() == null || request.getItems().isEmpty()) {
throw new RuntimeException("订单商品不能为空");
}
if (request.getUserId() == null) {
throw new RuntimeException("用户ID不能为空");
}
// 2. 创建订单实体(只是set数据)
Order order = new Order();
order.setOrderNo(generateOrderNo());
order.setUserId(request.getUserId());
order.setAmount(calculateAmount(request.getItems()));
order.setStatus(1); // 硬编码状态:待支付
order.setCreateTime(LocalDateTime.now());
// 3. 保存订单
Order savedOrder = orderRepository.save(order);
// 4. 扣减库存(业务逻辑在另一个Service)
inventoryService.deductStock(request.getItems());
// 5. 发送消息(另一个Service)
messageService.sendOrderCreatedMessage(savedOrder.getId());
return savedOrder.getId();
}
/**
* 支付订单(if-else的海洋)
*/
public void payOrder(PayOrderRequest request) {
Order order = orderRepository.findById(request.getOrderId())
.orElseThrow(() -> new RuntimeException("订单不存在"));
// 状态检查(业务规则分散在各处)
if (order.getStatus() != 1) {
throw new RuntimeException("订单状态不是待支付,不能支付");
}
// 支付金额校验
if (order.getAmount().compareTo(request.getPayAmount()) != 0) {
throw new RuntimeException("支付金额不正确");
}
// 调用支付服务
PaymentResult result = paymentService.pay(
order.getUserId(), order.getAmount(), request.getPayMethod());
if (result.isSuccess()) {
// 更新订单状态
order.setStatus(2); // 硬编码:已支付
order.setPayTime(LocalDateTime.now());
orderRepository.save(order);
// 发送支付成功消息
messageService.sendPaymentSuccessMessage(order.getId());
// 触发发货检查(又调用另一个Service)
deliveryService.checkAndDeliver(order.getId());
} else {
throw new RuntimeException("支付失败: " + result.getErrorMessage());
}
}
/**
* 取消订单(重复的状态检查)
*/
public void cancelOrder(Long orderId, String reason) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("订单不存在"));
// 又来了!重复的状态检查逻辑
if (order.getStatus() != 1 && order.getStatus() != 2) {
throw new RuntimeException("订单当前状态不能取消");
}
// 检查是否超过30分钟(业务规则!但写在Service里)
if (order.getStatus() == 1) { // 待支付订单
LocalDateTime now = LocalDateTime.now();
if (order.getCreateTime().plusMinutes(30).isAfter(now)) {
// 未超时,可以取消
order.setStatus(5); // 已取消
order.setCancelTime(now);
order.setCancelReason(reason);
orderRepository.save(order);
// 恢复库存
inventoryService.restoreStock(order.getId());
} else {
throw new RuntimeException("待支付订单超过30分钟,系统会自动取消");
}
} else if (order.getStatus() == 2) { // 已支付订单
// 不同的取消逻辑...
}
}
}问题诊断
这种代码有四个致命问题:
- 贫血失忆症:
Order类不知道自己能做什么,只是数据的搬运工 - 业务逻辑碎片化:状态检查、业务规则分散在各个Service方法中
- 重复代码泛滥:同样的状态检查逻辑出现在多个地方
- 业务知识丢失:为什么是30分钟?为什么某些状态不能取消?这些业务知识淹没在代码里
二、 DDD的核心武器:用代码表达业务
第一步:识别核心领域和限界上下文
我和业务专家一起工作坊,画出了供应链系统的核心领域:
供应链核心域:
┌─────────────────────────────────────────────────────────┐
│ 订单域 (Order) │
│ - 订单创建、支付、取消、发货、完成 │
│ - 核心规则:支付超时、库存校验、状态流转 │
├─────────────────────────────────────────────────────────┤
│ 商品域 (Product) │
│ - 商品信息、库存管理、价格策略 │
│ - 核心规则:库存扣减、价格计算 │
├─────────────────────────────────────────────────────────┤
│ 用户域 (User) │
│ - 用户信息、地址管理、积分体系 │
│ - 核心规则:地址校验、积分计算 │
└─────────────────────────────────────────────────────────┘关键洞察:订单域是我们的核心域,其他是支撑域。我们决定从订单域开始DDD改造。
第二步:设计领域模型(战术模式落地)
1. 实体(Entity)设计:有身份和行为
/**
* 订单聚合根 - 富血模型
* 核心原则:实体不是数据容器,是业务概念的载体
*/
@AggregateRoot
@Entity
@Table(name = "t_order")
@Getter // 注意:只有getter,没有setter!
@Slf4j
public class Order extends BaseEntity {
@EmbeddedId
private OrderId id; // 值对象作为ID
private OrderNo orderNo; // 值对象:订单号
private Money amount; // 值对象:金额
@Enumerated(EnumType.STRING)
private OrderStatus status; // 枚举:状态
@Embedded
private CustomerInfo customerInfo; // 值对象:客户信息
@Embedded
private Address shippingAddress; // 值对象:收货地址
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "order_id")
private List<OrderItem> items = new ArrayList<>(); // 实体集合
@Embedded
private OrderTimeline timeline; // 值对象:时间线
// 重要:保护无参构造器,仅给JPA使用
protected Order() {}
/**
* 创建订单 - 静态工厂方法
* 业务规则:必须有商品、必须有客户、必须计算金额
*/
public static Order create(
CustomerInfo customerInfo,
Address shippingAddress,
List<OrderItem> items,
OrderPolicy orderPolicy) {
// 业务规则校验(在实体内部!)
if (items == null || items.isEmpty()) {
throw new DomainException("订单必须包含商品");
}
if (customerInfo == null) {
throw new DomainException("客户信息不能为空");
}
// 应用业务策略:计算金额
Money amount = orderPolicy.calculateAmount(items);
// 创建订单(业务概念的体现)
Order order = new Order();
order.id = OrderId.nextId();
order.orderNo = OrderNo.generate();
order.customerInfo = customerInfo;
order.shippingAddress = shippingAddress;
order.items = new ArrayList<>(items);
order.amount = amount;
order.status = OrderStatus.CREATED; // 初始状态
order.timeline = OrderTimeline.create();
// 领域事件:订单已创建
order.registerEvent(new OrderCreatedEvent(order.id, order.orderNo));
log.info("订单创建成功: {}", order.orderNo);
return order;
}
/**
* 支付订单 - 核心业务行为
* 业务规则:只能支付待支付订单、金额必须匹配、更新支付时间
*/
public void pay(Payment payment, PaymentValidator validator) {
// 业务规则:状态检查
if (!status.canPay()) {
throw new DomainException(
String.format("订单[%s]当前状态[%s]不能支付", orderNo, status));
}
// 业务规则:金额校验
if (!amount.equals(payment.getAmount())) {
throw new DomainException(
String.format("支付金额[%s]与订单金额[%s]不匹配",
payment.getAmount(), amount));
}
// 业务规则:支付方式校验(使用领域服务)
validator.validate(payment);
// 状态转换(业务逻辑的核心)
this.status = OrderStatus.PAID;
this.timeline.recordPaid(LocalDateTime.now());
// 领域事件:订单已支付
registerEvent(new OrderPaidEvent(this.id, payment.getPaymentNo()));
log.info("订单支付成功: {}, 支付方式: {}", orderNo, payment.getMethod());
}
/**
* 取消订单 - 复杂的业务逻辑
* 业务规则:不同状态有不同的取消逻辑
*/
public void cancel(CancelReason reason, CancelPolicy cancelPolicy) {
// 业务规则:能否取消
if (!status.canCancel()) {
throw new DomainException(
String.format("订单[%s]当前状态[%s]不能取消", orderNo, status));
}
// 业务策略:应用取消策略(如超时检查)
CancelResult cancelResult = cancelPolicy.apply(this, reason);
if (cancelResult.isAllowed()) {
// 执行取消
this.status = OrderStatus.CANCELLED;
this.timeline.recordCancelled(LocalDateTime.now(), reason);
// 领域事件:订单已取消
registerEvent(new OrderCancelledEvent(
this.id,
this.status,
reason
));
log.info("订单取消成功: {}, 原因: {}", orderNo, reason);
} else {
throw new DomainException(cancelResult.getRejectReason());
}
}
/**
* 检查支付超时 - 业务规则的具体体现
* 业务规则:创建后30分钟未支付自动取消
*/
public boolean isPaymentTimeout() {
if (status != OrderStatus.CREATED) {
return false;
}
LocalDateTime now = LocalDateTime.now();
LocalDateTime timeoutTime = timeline.getCreatedTime().plusMinutes(30);
return now.isAfter(timeoutTime);
}
// 其他业务行为:发货、完成、退货等...
/**
* 关键:没有setter!业务状态只能通过业务方法改变
*/
}2. 值对象(Value Object)设计:描述特征,没有身份
/**
* 订单号 - 值对象
* 特征:不可变、无标识、自解释
*/
@Embeddable
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode
public class OrderNo implements Serializable {
private String value;
// 工厂方法
public static OrderNo generate() {
// 业务规则:订单号生成规则
String timestamp = LocalDateTime.now().format(
DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
String random = String.format("%06d", ThreadLocalRandom.current().nextInt(1000000));
return new OrderNo("ORD" + timestamp + random);
}
// 业务方法
public String getDisplayNo() {
return value.substring(0, 4) + "..." + value.substring(value.length() - 4);
}
// 验证方法
public boolean isValid() {
return value != null && value.startsWith("ORD") && value.length() == 23;
}
@Override
public String toString() {
return value;
}
}
/**
* 金额 - 值对象
* 封装金额相关的所有业务规则
*/
@Embeddable
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Money implements Comparable<Money>, Serializable {
private static final Currency CNY = Currency.getInstance("CNY");
private BigDecimal amount;
private Currency currency;
// 工厂方法
public static Money of(BigDecimal amount) {
return new Money(amount.setScale(2, RoundingMode.HALF_UP), CNY);
}
public static Money zero() {
return of(BigDecimal.ZERO);
}
// 业务操作
public Money add(Money other) {
validateSameCurrency(other);
return new Money(amount.add(other.amount), currency);
}
public Money subtract(Money other) {
validateSameCurrency(other);
return new Money(amount.subtract(other.amount), currency);
}
public Money multiply(BigDecimal multiplier) {
return new Money(amount.multiply(multiplier).setScale(2, RoundingMode.HALF_UP), currency);
}
// 业务规则:不能为负
public boolean isPositive() {
return amount.compareTo(BigDecimal.ZERO) > 0;
}
public boolean isNegative() {
return amount.compareTo(BigDecimal.ZERO) < 0;
}
private void validateSameCurrency(Money other) {
if (!this.currency.equals(other.currency)) {
throw new DomainException("货币类型不一致");
}
}
@Override
public int compareTo(Money other) {
validateSameCurrency(other);
return this.amount.compareTo(other.amount);
}
@Override
public String toString() {
return currency.getSymbol() + amount;
}
}3. 领域服务(Domain Service)设计:协调多个实体的业务逻辑
/**
* 订单支付领域服务
* 职责:协调订单、支付记录、库存等多个实体的复杂业务逻辑
*/
@Service
@Transactional
@Slf4j
public class OrderPaymentService {
private final OrderRepository orderRepository;
private final PaymentRepository paymentRepository;
private final InventoryService inventoryService;
private final DomainEventPublisher eventPublisher;
/**
* 执行支付 - 跨聚合的业务逻辑
*/
public PaymentResult executePayment(PayOrderCommand command) {
// 1. 获取订单聚合根
Order order = orderRepository.findById(command.getOrderId())
.orElseThrow(() -> new OrderNotFoundException(command.getOrderId()));
// 2. 创建支付记录(另一个聚合根)
Payment payment = Payment.create(
command.getPaymentNo(),
order.getId(),
order.getAmount(),
command.getPayMethod(),
command.getPayTime()
);
try {
// 3. 调用订单的支付行为(核心业务逻辑在实体内部)
order.pay(payment, new DefaultPaymentValidator());
// 4. 保存支付记录
paymentRepository.save(payment);
// 5. 预留库存(调用领域服务)
inventoryService.reserveStock(order);
// 6. 发布领域事件
eventPublisher.publishAll(order.getDomainEvents());
order.clearDomainEvents();
return PaymentResult.success(payment.getPaymentNo());
} catch (DomainException e) {
log.error("订单支付失败: {}", e.getMessage());
payment.fail(e.getMessage());
paymentRepository.save(payment);
return PaymentResult.failure(e.getMessage());
}
}
}4. 领域事件(Domain Event)设计:解耦业务逻辑
/**
* 订单已支付领域事件
* 特征:用过去时态命名,包含事件发生的时间
*/
@Getter
@AllArgsConstructor
public class OrderPaidEvent implements DomainEvent {
private OrderId orderId;
private String paymentNo;
private LocalDateTime occurredOn;
public OrderPaidEvent(OrderId orderId, String paymentNo) {
this.orderId = orderId;
this.paymentNo = paymentNo;
this.occurredOn = LocalDateTime.now();
}
@Override
public String getEventType() {
return "order.paid";
}
}
/**
* 领域事件处理器 - 发送支付成功通知
* 关键:响应式处理,不阻塞主流程
*/
@Component
@Slf4j
public class OrderPaidEventHandler {
private final MessageService messageService;
private final DeliveryService deliveryService;
@EventListener
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleOrderPaid(OrderPaidEvent event) {
log.info("处理订单支付事件: {}", event.getOrderId());
try {
// 1. 发送支付成功通知(非核心业务)
messageService.sendPaymentSuccessNotification(event.getOrderId());
// 2. 触发发货检查(异步)
deliveryService.scheduleDeliveryCheck(event.getOrderId());
} catch (Exception e) {
log.error("处理订单支付事件失败: {}", e.getMessage(), e);
// 重要:事件处理失败不应影响主流程
}
}
}三、 聚合设计:保护业务不变性
订单聚合的边界设计
/**
* 订单聚合 - 定义聚合边界
* 设计原则:一个事务只修改一个聚合
*/
@Aggregate
public class OrderAggregate {
// 聚合根
private Order root;
// 聚合内的实体(都在一个事务边界内)
private List<OrderItem> items;
private List<OrderLog> logs;
// 业务规则:总金额必须等于所有商品金额之和
public void validateTotalAmount() {
Money total = items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.zero(), Money::add);
if (!root.getAmount().equals(total)) {
throw new DomainException("订单金额校验失败");
}
}
}
/**
* 订单项 - 聚合内的实体
* 特征:没有自己的Repository,生命周期由聚合根管理
*/
@Entity
@Table(name = "t_order_item")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderItem {
@EmbeddedId
private OrderItemId id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
private ProductId productId;
private String productName;
private Money unitPrice;
private Integer quantity;
// 业务方法
public Money getSubtotal() {
return unitPrice.multiply(new BigDecimal(quantity));
}
// 工厂方法
public static OrderItem create(
Order order,
ProductId productId,
String productName,
Money unitPrice,
Integer quantity) {
// 业务规则校验
if (quantity <= 0) {
throw new DomainException("商品数量必须大于0");
}
if (unitPrice.isNegative()) {
throw new DomainException("商品单价不能为负");
}
OrderItem item = new OrderItem();
item.id = OrderItemId.nextId();
item.order = order;
item.productId = productId;
item.productName = productName;
item.unitPrice = unitPrice;
item.quantity = quantity;
return item;
}
}四、 应用服务设计:薄薄的一层
/**
* 订单应用服务
* 职责:协调领域对象、事务管理、权限校验(非业务逻辑)
*/
@Service
@Transactional
@RequiredArgsConstructor
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final OrderFactory orderFactory;
private final OrderPaymentService orderPaymentService;
private final DomainEventPublisher eventPublisher;
/**
* 创建订单 - 应用服务方法
*/
public OrderId createOrder(CreateOrderCommand command) {
// 1. 参数校验(技术层面)
Validate.notNull(command, "命令不能为空");
Validate.notEmpty(command.getItems(), "订单商品不能为空");
// 2. 权限校验(技术层面)
SecurityUtils.checkPermission("ORDER_CREATE");
// 3. 调用领域层创建订单(业务逻辑在领域层)
Order order = orderFactory.createOrder(
command.getCustomerId(),
command.getShippingAddress(),
command.getItems()
);
// 4. 保存聚合根
orderRepository.save(order);
// 5. 发布领域事件
eventPublisher.publishAll(order.getDomainEvents());
return order.getId();
}
/**
* 支付订单 - 应用服务方法
*/
public PaymentResult payOrder(PayOrderCommand command) {
// 技术层面的校验
Validate.notNull(command, "支付命令不能为空");
// 委托给领域服务处理业务逻辑
return orderPaymentService.executePayment(command);
}
/**
* 取消订单 - 应用服务方法
*/
public void cancelOrder(CancelOrderCommand command) {
Order order = orderRepository.findById(command.getOrderId())
.orElseThrow(() -> new OrderNotFoundException(command.getOrderId()));
// 业务逻辑在实体内部!
order.cancel(
CancelReason.of(command.getReason()),
new TimeoutCancelPolicy()
);
orderRepository.save(order);
eventPublisher.publishAll(order.getDomainEvents());
}
}五、 测试驱动:用测试表达业务规则
领域模型的单元测试
class OrderTest {
@Test
void should_create_order_with_items_and_calculate_amount() {
// Given
CustomerInfo customer = CustomerInfo.of("user123", "张三");
Address address = Address.of("上海市", "浦东新区", "张江路123号");
List<OrderItem> items = Arrays.asList(
OrderItem.create(null, ProductId.of("p1"), "iPhone13", Money.of(new BigDecimal("5999")), 1),
OrderItem.create(null, ProductId.of("p2"), "AirPods", Money.of(new BigDecimal("1299")), 2)
);
// When
Order order = Order.create(customer, address, items, new DefaultOrderPolicy());
// Then
assertThat(order).isNotNull();
assertThat(order.getOrderNo()).isNotNull();
assertThat(order.getAmount()).isEqualTo(Money.of(new BigDecimal("8597"))); // 5999 + 1299*2
assertThat(order.getStatus()).isEqualTo(OrderStatus.CREATED);
assertThat(order.getItems()).hasSize(2);
}
@Test
void should_throw_exception_when_create_order_without_items() {
// When & Then
assertThatThrownBy(() ->
Order.create(
CustomerInfo.of("user123", "张三"),
Address.of("上海", "浦东", "张江路"),
Collections.emptyList(),
new DefaultOrderPolicy()
)
).isInstanceOf(DomainException.class)
.hasMessage("订单必须包含商品");
}
@Test
void should_pay_order_successfully() {
// Given
Order order = createTestOrder();
Payment payment = new Payment("pay123", order.getAmount(), PayMethod.ALIPAY);
// When
order.pay(payment, new DefaultPaymentValidator());
// Then
assertThat(order.getStatus()).isEqualTo(OrderStatus.PAID);
assertThat(order.getTimeline().getPaidTime()).isNotNull();
}
@Test
void should_throw_exception_when_pay_cancelled_order() {
// Given
Order order = createTestOrder();
order.cancel(CancelReason.of("用户取消"), new AlwaysAllowCancelPolicy());
Payment payment = new Payment("pay123", order.getAmount(), PayMethod.ALIPAY);
// When & Then
assertThatThrownBy(() ->
order.pay(payment, new DefaultPaymentValidator())
).isInstanceOf(DomainException.class)
.hasMessageContaining("不能支付");
}
@Test
void should_auto_cancel_when_payment_timeout() {
// Given
Order order = createTestOrder();
// 模拟31分钟前创建的订单
order.getTimeline().setCreatedTime(LocalDateTime.now().minusMinutes(31));
// When & Then
assertThat(order.isPaymentTimeout()).isTrue();
}
}六、 架构演进:分层架构落地
清晰的分层架构
订单域分层架构:
┌─────────────────────────────────────────────────────────┐
│ 接口层 (Interface Layer) │
│ - Controller:接收请求、返回响应 │
│ - DTO转换:Command/Query/Response │
└──────────────────────────┬──────────────────────────────┘
│
┌──────────────────────────▼──────────────────────────────┐
│ 应用层 (Application Layer) │
│ - ApplicationService:事务边界、权限校验 │
│ - 事件发布:发布领域事件 │
└──────────────────────────┬──────────────────────────────┘
│
┌──────────────────────────▼──────────────────────────────┐
│ 领域层 (Domain Layer) │
│ - 实体、值对象、领域服务:核心业务逻辑 │
│ - 领域事件:业务事件定义 │
└──────────────────────────┬──────────────────────────────┘
│
┌──────────────────────────▼──────────────────────────────┐
│ 基础设施层 (Infrastructure Layer) │
│ - Repository实现:数据持久化 │
│ - 消息队列、缓存、外部服务调用 │
└─────────────────────────────────────────────────────────┘依赖注入的实现
// 领域层定义接口(不依赖基础设施)
public interface OrderRepository {
Optional<Order> findById(OrderId orderId);
Order save(Order order);
List<Order> findPaymentTimeoutOrders(LocalDateTime timeoutTime);
}
// 基础设施层实现接口
@Repository
public class OrderRepositoryImpl implements OrderRepository {
private final JpaOrderRepository jpaRepository;
private final OrderMapper mapper;
@Override
public Optional<Order> findById(OrderId orderId) {
return jpaRepository.findById(orderId.getValue())
.map(mapper::toDomain);
}
@Override
public Order save(Order order) {
OrderEntity entity = mapper.toEntity(order);
OrderEntity saved = jpaRepository.save(entity);
return mapper.toDomain(saved);
}
}七、 实战经验:DDD落地的坑与解决方案
坑1:聚合设计过大
// 错误:把所有关联实体都放进一个聚合
public class OrderAggregate {
private Order order;
private List<OrderItem> items;
private List<Payment> payments; // 错误:支付应该是独立聚合
private List<Delivery> deliveries; // 错误:发货应该是独立聚合
}
// 正确:只放真正属于一个事务边界的实体
public class OrderAggregate {
private Order order;
private List<OrderItem> items; // 订单项生命周期与订单一致
private List<OrderLog> logs; // 订单日志生命周期与订单一致
}坑2:领域服务滥用
// 错误:把本应在实体中的逻辑放到领域服务
@Service
public class OrderService {
public void payOrder(Long orderId, Payment payment) {
Order order = repository.findById(orderId);
// 错误:业务逻辑在Service中
if (order.getStatus() != OrderStatus.CREATED) {
throw new Exception("不能支付");
}
order.setStatus(OrderStatus.PAID); // 错误:直接set状态
}
}
// 正确:业务逻辑在实体中
@Entity
public class Order {
public void pay(Payment payment) {
// 正确:业务逻辑在实体内部
if (!status.canPay()) {
throw new DomainException("不能支付");
}
this.status = OrderStatus.PAID; // 正确:通过业务方法改变状态
}
}坑3:贫血模型复发
// 错误:又变回了getter/setter
@Data
@Entity
public class Order {
private OrderStatus status;
// 错误:暴露了setter
public void setStatus(OrderStatus status) {
this.status = status;
}
}
// 正确:只有业务方法能改变状态
@Entity
public class Order {
private OrderStatus status;
// 正确:没有setter
public void pay() {
// 业务规则检查
if (!status.canPay()) {
throw new DomainException("不能支付");
}
// 状态转换
this.status = OrderStatus.PAID;
}
}八、 DDD带来的实际价值
价值1:代码即文档
// 看代码就能理解业务规则
public class Order {
// 业务规则:创建后30分钟未支付自动取消
public boolean isPaymentTimeout() {
if (status != OrderStatus.CREATED) return false;
return LocalDateTime.now().isAfter(
timeline.getCreatedTime().plusMinutes(30)
);
}
// 业务规则:只有待支付和已支付订单可以取消
public boolean canCancel() {
return status == OrderStatus.CREATED || status == OrderStatus.PAID;
}
}价值2:业务逻辑内聚
// 所有订单状态转换逻辑都在一起
public enum OrderStatus {
CREATED {
@Override public boolean canPay() { return true; }
@Override public boolean canCancel() { return true; }
@Override public OrderStatus afterPay() { return PAID; }
},
PAID {
@Override public boolean canPay() { return false; }
@Override public boolean canCancel() { return true; }
@Override public boolean canDeliver() { return true; }
},
CANCELLED {
@Override public boolean canPay() { return false; }
@Override public boolean canCancel() { return false; }
};
public abstract boolean canPay();
public abstract boolean canCancel();
public boolean canDeliver() { return false; }
public OrderStatus afterPay() { throw new IllegalStateException("不能支付"); }
}价值3:应对业务变化
当业务规则从"30分钟未支付自动取消"变为"VIP用户60分钟,普通用户30分钟":
// 只需要修改策略类,实体代码不变
public class VipCancelPolicy implements CancelPolicy {
@Override
public CancelResult apply(Order order, CancelReason reason) {
if (order.isVipCustomer()) {
// VIP用户60分钟
LocalDateTime timeout = order.getCreatedTime().plusMinutes(60);
boolean isTimeout = LocalDateTime.now().isAfter(timeout);
return isTimeout ?
CancelResult.reject("VIP用户有60分钟支付时间") :
CancelResult.allow();
} else {
// 普通用户30分钟
LocalDateTime timeout = order.getCreatedTime().plusMinutes(30);
boolean isTimeout = LocalDateTime.now().isAfter(timeout);
return isTimeout ?
CancelResult.reject("订单已超时") :
CancelResult.allow();
}
}
}九、 从CRUD到DDD的迁移策略
策略1:渐进式重构
- 第一步:识别核心域(订单)
- 第二步:设计领域模型(纸上设计)
- 第三步:实现值对象(无风险)
- 第四步:改造核心实体(从创建订单开始)
- 第五步:逐步迁移业务逻辑
策略2:新老系统并存
// 老系统接口保持不变
@Deprecated
public class LegacyOrderService {
// 老代码
}
// 新系统实现相同接口
@Primary
public class DDDOrderService implements OrderService {
// DDD实现
}
// 配置开关
@Configuration
public class OrderConfig {
@Bean
@ConditionalOnProperty(name = "order.ddd.enabled", havingValue = "true")
public OrderService dddOrderService() {
return new DDDOrderService();
}
@Bean
@ConditionalOnProperty(name = "order.ddd.enabled", havingValue = "false", matchIfMissing = true)
public OrderService legacyOrderService() {
return new LegacyOrderService();
}
}十、 总结:DDD不是银弹,是思维转变
经过一年的DDD实践,我最大的收获不是技术,而是思维的转变:
- 从数据建模到业务建模:不再先设计数据库表,而是先理解业务
- 从过程式编程到声明式编程:不再写"怎么做",而是定义"是什么"
- 从技术驱动到业务驱动:技术为业务服务,不是相反
DDD的核心价值:
- 代码能清晰地表达业务意图
- 业务逻辑高度内聚,不再分散
- 面对业务变化时,修改点明确且可控
开始DDD的建议:
- 从小处开始:选一个核心域
- 与业务专家合作:领域模型是共同语言
- 注重实践,不纠结理论:能解决实际问题的就是好DDD
记住:DDD不是关于框架和技术的,而是关于如何用代码更好地表达业务。当你的代码能让新同事一眼看懂业务规则时,你就成功了。