共计 2474 个字符,预计需要花费 7 分钟才能阅读完成。
1. 问题场景:当代码变成 ” 面条式 ” 结构
最近接手一个电商促销模块时,发现修改满减规则需要同时改动 Controller、Service 和 DAO 层——这正是一个典型的 霰弹式修改 症状。更严重的是,核心下单接口的响应时间从最初的 200ms 逐渐劣化到 1.2 秒,日志显示其中有 6 次重复的库存查询操作。

2. 架构选型:DDD vs 传统 MVC
传统 MVC 的痛点
- 贫血模型 :Service 层充斥
setter/getter的搬运逻辑 - 强耦合:数据库表结构直接渗透到业务逻辑
- 复用困难:一个 Service 方法可能被多个 Controller 调用
DDD 分层优势(架构图示意)
┌─────────────────┐
│ Interface 层 │ ◄── HTTP/RPC 适配
├─────────────────┤
│ Application 层 │ ◄── 用例编排 / 事务控制
├─────────────────┤
│ Domain 层 │ ◄── 核心业务逻辑
├─────────────────┤
│ Infrastructure │ ◄── 技术实现(DB/ 缓存等)
└─────────────────┘
3. 核心实现
3.1 领域服务示例
// 订单领域服务
@RequiredArgsConstructor // Lombok 生成构造函数
public class OrderDomainService {
private final InventoryRepository inventoryRepo;
// 保持领域语言:placeOrder 而非 createOrder
public Order placeOrder(Order order) {
// 领域规则校验
if (order.getItems().isEmpty()) {throw new BusinessException("订单项不能为空");
}
// 聚合根操作
order.getItems().forEach(item -> {Inventory inventory = inventoryRepo.findById(item.getSkuId());
inventory.reduceStock(item.getQuantity()); // 修改库存聚合根
});
return order.confirm(); // 返回已确认状态的订单}
}
3.2 DTO 转换技巧
@Data // Lombok 注解
public class OrderDTO {
private String orderNo;
private List<OrderItemDTO> items;
// 转换方法保持单向依赖
public Order toDomain() {Order order = new Order();
order.setOrderNo(this.orderNo);
order.setItems(this.items.stream()
.map(OrderItemDTO::toDomain)
.collect(Collectors.toList())
);
return order;
}
}
3.3 单元测试示范
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepo;
@InjectMocks
private OrderDomainService service;
@Test
void should_throw_exception_when_empty_items() {Order order = new Order();
assertThrows(BusinessException.class,
() -> service.placeOrder(order));
}
@Test
void should_reduce_stock_for_each_item() {
// Given
OrderItem item = new OrderItem("SKU001", 2);
Order order = new Order(Arrays.asList(item));
// When
when(inventoryRepo.findById("SKU001"))
.thenReturn(new Inventory("SKU001", 10));
// Then
Order result = service.placeOrder(order);
assertEquals(8, inventoryRepo.findById("SKU001").getStock());
}
}
4. 性能优化
4.1 N+ 1 查询解决方案
// 使用 @BatchSize 优化关联查询
@Entity
public class Order {@OneToMany(mappedBy = "order")
@BatchSize(size = 50) // 一次加载 50 个子项
private List<OrderItem> items;
}
// 查询时使用 JOIN FETCH
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Order findByIdWithItems(@Param("id") Long id);
4.2 缓存策略选择
| 场景 | 推荐方案 | 示例 |
|---|---|---|
| 高频读低频写 | Redis+ 本地缓存 | 商品分类数据 |
| 强一致性要求 | 数据库乐观锁 | 库存扣减 |
| 复杂计算结果 | Guava Cache | 促销规则匹配结果 |
5. 避坑指南
5.1 过度抽象反模式
- 抽象泄漏:基础设施细节渗透到领域层(如:在领域对象中处理 JSON 序列化)
- 万能服务 :一个
CommonService处理 20 种业务场景 - 接口膨胀:为每个方法都创建 interface 导致维护负担
5.2 事务划分原则
- 聚合根操作必须在一个事务内完成
- 跨聚合操作考虑最终一致性(Saga 模式)
- 查询服务通常不需要事务
- 避免在事务中调用外部服务
6. 开放讨论
在实践中发现,严格的领域模型会导致初期开发效率下降约 30%。是否应该在 MVP 阶段采用 贫血模型 + 事务脚本,待业务稳定后再重构?欢迎分享你的项目经验。
特别提醒:领域驱动不是银弹,适合复杂核心业务场景。简单的 CRUD 应用使用 MVC 反而更高效。
正文完
