DDD vs MVC:如何选择?
📖 前置阅读:本文假设读者已理解 DDD 的核心概念(实体/值对象/聚合根/限界上下文)和战术代码模板(四层架构/Repository/Domain Service)。如果还不熟悉,建议先阅读 DDD 本质 和 DDD 战术落地。
一、⚡ DDD 这么好——是不是所有服务都要重构一遍?
看完前两篇——概念清楚了——代码模板也有了——冲动上来了:
"先把所有微服务用 DDD 重构一遍!"
① user-service → DDD
② order-service → DDD
③ product-service → DDD
④ account-service → DDD
⑤ inventory-service → DDD
→ 加班 2 个月——重构了一堆——代码没更好——反而更复杂了
DDD 不是银弹——不是所有代码都值得用 DDD。这篇的核心就是告诉你:什么该改、什么不改、改到什么程度。
二、🔍 诊断——我们现有的三个服务——各自是什么情况
2.1 user-service——经典 MVC——不改
// user-service——现有结构
controller/
└─ UserController.java @RestController——GET/POST/PUT
service/
└─ UserService.java 简单的增删改查 + 缓存操作
mapper/
└─ UserMapper.java MyBatis——selectById/insert/update
model/
└─ User.java 15 个字段——getter/setter
// UserService 最复杂的方法——也就 20 行
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate<String, User> redisTemplate;
public User getUser(Long userId) {
String cacheKey = "user:" + userId;
User cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) return cached;
User user = userMapper.selectById(userId);
if (user != null) redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
return user;
}
public void updateUser(User user) {
user.setUpdatedAt(LocalDateTime.now());
userMapper.updateById(user);
redisTemplate.delete("user:" + user.getId()); // 失效缓存
}
}
判断——不需要 DDD:
user-service 的特征:
✅ 业务逻辑简单——基本就是 CRUD
✅ 没有复杂的状态迁移——User 的状态就 ACTIVE/DISABLED 两个
✅ 没有跨聚合的不变量——User 不需要保护自己的内部数据一致性
✅ Service 方法 < 30 行——没有膨胀
如果用 DDD 重构——会变成什么?
→ User 变成聚合根——加一堆业务方法
→ 建 UserRepository 接口 + UserRepositoryImpl 实现——代替直接调 UserMapper
→ 建 UserDomainService——管理 User 的状态迁移
→ Application Service 编排
结果:4 个新类 + 转换器——替代原来 1 个 Service
→ 代码量翻倍——复杂度增加——可读性反而下降
→ 在 user-service 这种纯 CRUD 上——DDD 是过度设计
结论:user-service 保持 MVC——不需要 DDD。
2.2 product-service——中等复杂——部分 DDD
// product-service——现有结构
controller/
└─ ProductController.java
service/
└─ ProductService.java 有一些业务逻辑——库存管理
mapper/
└─ ProductMapper.java
model/
└─ Product.java
// ProductService 中有业务逻辑——但不算特别复杂
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
// 扣库存——有业务规则:库存不能为负
public void deductStock(Long productId, int quantity) {
Product product = productMapper.selectById(productId);
if (product == null) throw new BusinessException("商品不存在");
if (product.getStock() < quantity) throw new BusinessException("库存不足");
product.setStock(product.getStock() - quantity);
productMapper.updateById(product);
}
// 上架/下架——有状态迁移
public void updateStatus(Long productId, ProductStatus newStatus) {
Product product = productMapper.selectById(productId);
if (product == null) throw new BusinessException("商品不存在");
// 校验状态迁移——简单的 switch
switch (newStatus) {
case ON_SALE:
if (product.getStock() <= 0)
throw new BusinessException("库存为 0 不能上架");
break;
case OFF_SHELF:
// 任意状态都可以下架
break;
}
product.setStatus(newStatus);
productMapper.updateById(product);
}
}
判断——可以做轻量 DDD:
product-service 的特征:
✅ 有业务规则——扣库存不能为负、库存为 0 不能上架
✅ 有状态迁移——ON_SALE/OFF_SHELF/DISCONTINUED
✅ Service 方法开始膨胀——30-50 行
⚠️ 但也不算特别复杂——不需要完整的 DDD 四层架构
轻量 DDD 方案——只要把 Product 变成充血模型:
① Product 加上业务方法(deductStock/putOnSale/takeOffShelf)
② 保留 Service——但只做编排
③ 不需要建 Domain Service、Repository 接口层——项目量级不够
④ 不需要建防腐层——Product 没有外部依赖
// 轻量 DDD——Product 充血模型
public class Product {
private Long id;
private String name;
private String description;
private Money price;
private int stock;
private ProductStatus status;
// ========== 业务方法——从 Service 移过来 ==========
// 扣库存
public void deductStock(int quantity) {
if (quantity <= 0) {
throw new BusinessException("扣减数量必须大于 0");
}
if (this.stock < quantity) {
throw new BusinessException("库存不足——当前:" + this.stock + "——扣减:" + quantity);
}
this.stock -= quantity;
}
// 恢复库存(订单取消时)
public void restoreStock(int quantity) {
if (quantity <= 0) {
throw new BusinessException("恢复数量必须大于 0");
}
this.stock += quantity;
}
// 上架
public void putOnSale() {
if (this.status == ProductStatus.ON_SALE) {
throw new BusinessException("商品已在上架状态");
}
if (this.stock <= 0) {
throw new BusinessException("库存为 0 不能上架");
}
this.status = ProductStatus.ON_SALE;
}
// 下架
public void takeOffShelf() {
if (this.status == ProductStatus.OFF_SHELF) {
throw new BusinessException("商品已下架");
}
this.status = ProductStatus.OFF_SHELF;
}
// 查询——不影响状态
public boolean isOnSale() { return this.status == ProductStatus.ON_SALE; }
public boolean hasEnoughStock(int quantity) { return this.stock >= quantity; }
public boolean isOutOfStock() { return this.stock <= 0; }
// getter——没有 setter
}
// Service 变薄——只做编排
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
@Transactional
public void deductStock(Long productId, int quantity) {
Product product = productMapper.selectById(productId);
if (product == null) throw new BusinessException("商品不存在");
product.deductStock(quantity); // 业务逻辑在 Product 里
productMapper.updateById(product); // 只负责持久化
}
@Transactional
public void putOnSale(Long productId) {
Product product = productMapper.selectById(productId);
if (product == null) throw new BusinessException("商品不存在");
product.putOnSale(); // 业务逻辑在 Product 里
productMapper.updateById(product);
}
}
// Product 的单测——不需要 Mock Mapper——直接 new 就能测
@Test
void deductStock_shouldThrow_whenInsufficient() {
Product product = new Product("键盘", new Money(199, "CNY"), 5);
assertThrows(BusinessException.class, () -> product.deductStock(10));
}
@Test
void deductStock_shouldReduce_whenEnough() {
Product product = new Product("键盘", new Money(199, "CNY"), 5);
product.deductStock(3);
assertEquals(2, product.getStock());
}
结论:product-service 适合轻量 DDD——对象变充血——不建完整四层。
2.3 order-service——复杂——完整 DDD
// order-service——现有结构
controller/
└─ OrderController.java
service/
└─ OrderService.java 800 行——20 个方法——上帝类
mapper/
├─ OrderMapper.java
├─ OrderItemMapper.java
├─ UserMapper.java ← 跨表——OrderService 直接调
├─ ProductMapper.java ← 跨表
└─ AccountMapper.java ← 跨表
model/
├─ Order.java 贫血——只有 getter/setter
└─ OrderItem.java 贫血
// OrderService.createOrder()——800 行中最典型的 80 行
// 问题:逻辑散落——没有不变量保护——测试困难——改代码不敢改
判断——需要完整 DDD:
order-service 的特征:
❌ 业务逻辑重——下单流程涉及 5 步校验(用户/商品/库存/余额/金额)
❌ 跨聚合操作——Order 和 Product 和 Account 的修改混在一起
❌ 状态迁移复杂——待支付→已支付→已发货→已送达→已完成、待支付→已取消
❌ Service 膨胀 800 行——20 个方法——"上帝类"
❌ 改需求困难——"加首单折扣"要改 3 处代码
❌ 测试困难——必须 Mock 5 个 Mapper——测试不跑数据库就没法测
→ 完整 DDD——聚合根 + Domain Service + Repository + 防腐层 + 领域事件
结论:order-service 是 DDD 的最佳场景——复杂业务逻辑 + 多状态 + 跨聚合协调。
三、🔄 重构 order-service——完整的 Before / After
3.1 Before——当前贫血模型
// ===== Before——现有代码 =====
// 贫血的 Order——只有字段和 getter/setter
public class Order {
private Long id;
private String orderNo;
private Long userId;
private BigDecimal totalAmount;
private String status; // "PENDING_PAY", "PAID", ...
private String province;
private String city;
private String district;
private String detail;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// 30 个 getter / setter——省略
}
// 贫血的 OrderItem
public class OrderItem {
private Long id;
private Long orderId;
private Long productId;
private String productName;
private BigDecimal price;
private int quantity;
// getter / setter——省略
}
// 上帝类 Service——800 行
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderItemMapper orderItemMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private ProductMapper productMapper;
@Autowired
private AccountMapper accountMapper;
@Autowired
private RocketMQTemplate rocketMQTemplate;
public Order createOrder(CreateOrderRequest request) {
// ① 校验用户——调 UserMapper
User user = userMapper.selectById(request.getUserId());
if (user == null || user.getStatus() == UserStatus.BANNED) {
throw new BusinessException("用户不存在或已封号");
}
// ② 校验商品——调 ProductMapper——循环中逐条查
BigDecimal totalAmount = BigDecimal.ZERO;
List<OrderItem> items = new ArrayList<>();
for (CreateOrderItemRequest itemReq : request.getItems()) {
Product product = productMapper.selectById(itemReq.getProductId());
if (product == null || product.getStatus() != ProductStatus.ON_SALE) {
throw new BusinessException("商品 " + itemReq.getProductId() + " 不可售");
}
if (product.getStock() < itemReq.getQuantity()) {
throw new BusinessException("商品 " + itemReq.getProductId() + " 库存不足");
}
// 直接改 Product 表——跨聚合修改
product.setStock(product.getStock() - itemReq.getQuantity());
productMapper.updateById(product);
totalAmount = totalAmount.add(product.getPrice()
.multiply(BigDecimal.valueOf(itemReq.getQuantity())));
items.add(new OrderItem(itemReq.getProductId(), itemReq.getQuantity(), product.getPrice()));
}
// ③ 扣余额——直接调 AccountMapper——跨聚合修改
Account account = accountMapper.selectByUserId(request.getUserId());
if (account.getBalance().compareTo(totalAmount) < 0) {
throw new BusinessException("余额不足");
}
account.setBalance(account.getBalance().subtract(totalAmount));
accountMapper.updateById(account);
// ④ 创建订单
Order order = new Order();
order.setOrderNo(generateOrderNo());
order.setUserId(request.getUserId());
order.setTotalAmount(totalAmount);
order.setStatus("PENDING_PAY");
order.setCreatedAt(LocalDateTime.now());
orderMapper.insert(order);
// ⑤ 保存订单项
for (OrderItem item : items) {
item.setOrderId(order.getId());
orderItemMapper.insert(item);
}
// ⑥ 发 MQ——直接发——事务还没提交
rocketMQTemplate.syncSend("order-created", order);
return order;
}
// ... 还有 19 个方法——cancel/confirmReceipt/refund/...——总共 800 行
}
3.2 After——DDD 重构
Step 1:目录结构
order-service/
├── interfaces/rest/
│ ├── OrderController.java
│ └── dto/
│ ├── CreateOrderRequest.java
│ └── OrderResponse.java
├── application/
│ └── OrderApplicationService.java
├── domain/
│ ├── model/aggregate/
│ │ └── Order.java ← 聚合根——充血
│ ├── model/entity/
│ │ └── OrderItem.java ← 聚合内部实体
│ ├── model/valueobject/
│ │ ├── Money.java ← 值对象——金额
│ │ ├── Address.java ← 值对象——地址
│ │ └── OrderStatus.java ← 枚举——状态
│ ├── model/event/
│ │ ├── OrderCreatedEvent.java
│ │ └── OrderPaidEvent.java
│ ├── repository/
│ │ └── OrderRepository.java ← 接口
│ └── service/
│ └── OrderDomainService.java ← 跨聚合逻辑
└── infrastructure/
├── persistence/
│ ├── OrderRepositoryImpl.java ← 实现
│ ├── mapper/
│ │ ├── OrderMapper.java
│ │ └── OrderItemMapper.java
│ └── converter/
│ └── OrderConverter.java
└── messaging/
└── RocketMQEventPublisher.java
Step 2:Domain 层——聚合根 + 值对象 + Repository 接口
// ===== domain/model/aggregate/Order.java =====
// 聚合根——所有下单相关的业务逻辑都在这里
public class Order {
private Long id;
private String orderNo;
private Long userId; // ← 引用 User 聚合——只存 ID
private Money totalAmount; // ← 值对象——不是 BigDecimal
private Address deliveryAddress; // ← 值对象——不是 5 个 String
private OrderStatus status; // ← 枚举——不是 String
private List<OrderItem> items;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private List<DomainEvent> domainEvents = new ArrayList<>();
private Order(Long userId, Address deliveryAddress, List<OrderItem> items) {
if (userId == null) throw new IllegalArgumentException("用户 ID 不能为空");
if (deliveryAddress == null) throw new IllegalArgumentException("收货地址不能为空");
if (items == null || items.isEmpty()) throw new IllegalArgumentException("订单项不能为空");
this.userId = userId;
this.deliveryAddress = deliveryAddress;
this.items = new ArrayList<>(items);
this.orderNo = generateOrderNo();
this.status = OrderStatus.PENDING_PAY;
this.totalAmount = calculateTotalAmount();
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
this.domainEvents.add(new OrderCreatedEvent(this.id, this.orderNo,
this.userId, this.totalAmount));
}
public static Order create(Long userId, Address deliveryAddress,
List<OrderItem> items) {
return new Order(userId, deliveryAddress, items);
}
// ========== 业务方法——原来散落在 Service 800 行中 ==========
public void pay(Money paidAmount) {
if (this.status != OrderStatus.PENDING_PAY) {
throw new BusinessException("只有待支付订单才能支付——当前:" + this.status);
}
if (!this.totalAmount.equals(paidAmount)) {
throw new BusinessException("支付金额不匹配——应付:" + this.totalAmount
+ "——实付:" + paidAmount);
}
this.status = OrderStatus.PAID;
this.updatedAt = LocalDateTime.now();
this.domainEvents.add(new OrderPaidEvent(this.id, this.orderNo, this.userId));
}
public void cancel(String reason) {
if (this.status == OrderStatus.SHIPPED || this.status == OrderStatus.DELIVERED) {
throw new BusinessException("已发货的订单不能取消");
}
if (this.status == OrderStatus.CANCELLED) {
throw new BusinessException("订单已取消");
}
this.status = OrderStatus.CANCELLED;
this.updatedAt = LocalDateTime.now();
this.domainEvents.add(new OrderCancelledEvent(this.id, this.orderNo, reason));
}
public void ship(String trackingNumber) {
if (this.status != OrderStatus.PAID) {
throw new BusinessException("只有已支付订单才能发货");
}
this.status = OrderStatus.SHIPPED;
this.updatedAt = LocalDateTime.now();
}
public void confirmReceive() {
if (this.status != OrderStatus.SHIPPED) {
throw new BusinessException("只有已发货订单才能确认收货");
}
this.status = OrderStatus.DELIVERED;
this.updatedAt = LocalDateTime.now();
}
public void changeAddress(Address newAddress) {
if (this.status != OrderStatus.PENDING_PAY) {
throw new BusinessException("只有待支付订单才能修改地址");
}
if (Duration.between(this.createdAt, LocalDateTime.now()).toMinutes() > 10) {
throw new BusinessException("下单超过 10 分钟不能修改地址");
}
this.deliveryAddress = Objects.requireNonNull(newAddress);
this.updatedAt = LocalDateTime.now();
}
// 查询方法
public boolean canBeCancelled() {
return this.status == OrderStatus.PENDING_PAY || this.status == OrderStatus.PAID;
}
public int getTotalItemCount() {
return items.stream().mapToInt(OrderItem::getQuantity).sum();
}
// 领域事件收集
public List<DomainEvent> pollDomainEvents() {
List<DomainEvent> events = new ArrayList<>(this.domainEvents);
this.domainEvents.clear();
return events;
}
// 包级可见——给 Repository 重建用
static Order reconstruct(Long id, String orderNo, Long userId, Money totalAmount,
Address address, OrderStatus status, List<OrderItem> items,
LocalDateTime createdAt) {
Order order = new Order(userId, address, items);
order.id = id;
order.orderNo = orderNo;
order.totalAmount = totalAmount;
order.status = status;
order.createdAt = createdAt;
return order;
}
private String generateOrderNo() { /* ... */ }
private Money calculateTotalAmount() {
return items.stream().map(OrderItem::getSubTotal)
.reduce(new Money(BigDecimal.ZERO, "CNY"), Money::add);
}
// getter——只读
public Long getId() { return id; }
public String getOrderNo() { return orderNo; }
public Long getUserId() { return userId; }
public Money getTotalAmount() { return totalAmount; }
public Address getDeliveryAddress() { return deliveryAddress; }
public OrderStatus getStatus() { return status; }
public List<OrderItem> getItems() { return Collections.unmodifiableList(items); }
public LocalDateTime getCreatedAt() { return createdAt; }
void setId(Long id) { this.id = id; }
}
// ===== domain/model/valueobject/Money.java =====
public class Money {
private final BigDecimal amount; // final——不可变
private final String currency;
public Money(BigDecimal amount, String currency) {
if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("金额不能为空或负数");
}
this.amount = amount;
this.currency = Objects.requireNonNull(currency);
}
public Money add(Money other) {
if (!this.currency.equals(other.currency))
throw new BusinessException("不能加不同币种");
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(BigDecimal factor) {
return new Money(this.amount.multiply(factor), this.currency);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Money other)) return false;
return amount.compareTo(other.amount) == 0 && currency.equals(other.currency);
}
@Override
public int hashCode() { return Objects.hash(amount, currency); }
@Override
public String toString() { return currency + " " + amount; }
public BigDecimal getAmount() { return amount; }
public String getCurrency() { return currency; }
}
// ===== domain/repository/OrderRepository.java =====
public interface OrderRepository {
Optional<Order> findById(Long id);
Optional<Order> findByOrderNo(String orderNo);
List<Order> findByUserId(Long userId);
void save(Order order);
void delete(Order order);
List<Order> findPendingPaymentBefore(LocalDateTime deadline);
}
Step 3:Application Service——编排——不再包含业务逻辑
// ===== application/OrderApplicationService.java =====
@Service
public class OrderApplicationService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private ProductRepository productRepository; // Product 聚合的 Repository
@Autowired
private UserServiceAdapter userServiceAdapter; // 防腐层
@Autowired
private OrderDomainService orderDomainService; // Domain Service
@Autowired
private ApplicationEventPublisher eventPublisher;
@Transactional
public OrderResponse createOrder(CreateOrderCommand command) {
// ① 通过防腐层获取 Buyer——隔离外部 UserService
Buyer buyer = userServiceAdapter.getBuyer(command.getUserId());
// ② 通过 ProductRepository 加载 Product 聚合
List<Long> productIds = command.getItems().stream()
.map(CreateOrderCommand.ItemCommand::getProductId)
.toList();
List<Product> products = productRepository.findByIds(productIds);
// ③ Domain Service——创建 OrderItem——校验库存
List<OrderItem> orderItems = orderDomainService.createOrderItems(
command.getItems(), products);
// ④ Domain Service——计算价格
Money totalPrice = orderDomainService.calculatePrice(
command.getUserId(), orderItems);
// ⑤ 创建聚合根——业务校验在构造函数中
Order order = Order.create(command.getUserId(),
command.getDeliveryAddress(), orderItems);
// ⑥ 保存——只保存 Order 聚合——不保存 Product 和 Account
orderRepository.save(order);
// ⑦ 在事务提交后发布领域事件——异步扣库存 + 发优惠券
for (DomainEvent event : order.pollDomainEvents()) {
eventPublisher.publishEvent(event); // Spring 内部事件——事务提交后转发到 MQ
}
// ⑧ 返回
return OrderResponse.from(order);
}
@Transactional
public void payOrder(Long orderId, Money paidAmount) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new BusinessException("订单不存在"));
order.pay(paidAmount); // 业务逻辑在聚合根中
orderRepository.save(order); // 保存
for (DomainEvent event : order.pollDomainEvents()) {
eventPublisher.publishEvent(event);
}
}
@Transactional
public void cancelOrder(Long orderId, String reason) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new BusinessException("订单不存在"));
order.cancel(reason); // 业务逻辑在聚合根中
orderRepository.save(order);
for (DomainEvent event : order.pollDomainEvents()) {
eventPublisher.publishEvent(event);
}
}
}
Step 4:领域事件——异步处理跨聚合操作
// ===== 事件监听——在事务提交后处理 =====
@Component
public class OrderEventDispatcher {
@Autowired
private InventoryServiceAdapter inventoryAdapter; // 防腐层——隔离 Inventory 服务
@Autowired
private AccountServiceAdapter accountAdapter; // 防腐层——隔离 Account 服务
@Autowired
private RocketMQTemplate rocketMQTemplate;
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
// ① 异步扣库存——通过防腐层调用——不在 Order 的事务中
inventoryAdapter.deductStock(event.orderId());
// ② 发送 MQ——通知其他服务(营销、物流等)
rocketMQTemplate.syncSend("order-created-topic", event);
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCancelled(OrderCancelledEvent event) {
// ① 恢复库存
inventoryAdapter.restoreStock(event.orderId());
// ② 发送 MQ
rocketMQTemplate.syncSend("order-cancelled-topic", event);
}
}
3.3 Before / After 对比——一图看懂
Before(贫血 MVC):
┌────────────────────────────────────────────┐
│ OrderService.java — 800 行 │
│ ① 查用户 ② 查商品 ③ 扣库存 ④ 扣余额 │
│ ⑤ 建订单 ⑥ 建订单项 ⑦ 发 MQ │
│ 直接调 5 个 Mapper——跨 4 张表——一个事务 │
│ Order.java — 只有 getter/setter │
│ OrderItem.java — 只有 getter/setter │
└────────────────────────────────────────────┘
测试:必须 Mock 5 个 Mapper
改代码:在 800 行中找——改 3 处
After(DDD):
┌──────────────────────────────────────────────┐
│ OrderApplicationService.java — 40 行 │
│ 编排:查聚合 → 调 Domain Service → 创建聚合 │
│ → 保存 → 发事件 │
│ │
│ Order.java(聚合根)— 200 行 │
│ 业务逻辑:pay() / cancel() / ship() / ... │
│ 保护不变量:状态迁移校验、金额匹配校验 │
│ │
│ OrderDomainService.java — 50 行 │
│ 跨聚合逻辑:createOrderItems / calculatePrice │
│ │
│ Money.java(值对象)— 30 行 │
│ Address.java(值对象)— 40 行 │
│ │
│ UserServiceAdapter.java(防腐层)— 20 行 │
│ 翻译外部 UserDTO → Buyer │
└──────────────────────────────────────────────┘
测试:聚合根可以直接 new——不依赖数据库
改代码:找到对应的聚合根方法——改一处——不影响其他
四、📊 DDD vs MVC——决策框架
4.1 一张决策图
flowchart TD
Start["这个服务应该用 DDD 吗?"] --> Q1{"业务逻辑复杂吗?\n(不是纯 CRUD)"}
Q1 -->|"不复杂\n(查改删)"| MVC["保持 MVC\nuser-service 就是这类型"]
Q1 -->|"复杂"| Q2{"有复杂的状态迁移吗?\n(> 3 个状态)"}
Q2 -->|"没有"| Light["轻量充血模型\nproduct-service 就是这类型\n给 Domain 对象加业务方法\n不建完整四层"]
Q2 -->|"有"| Q3{"有跨聚合/跨表的\n不变量需要保护吗?"}
Q3 -->|"没有"| Light
Q3 -->|"有"| Q4{"Service 膨胀了吗?\n(> 200 行或 > 10 个方法)"}
Q4 -->|"没有"| Light
Q4 -->|"膨胀了"| DDD["完整 DDD\norder-service 就是这类型\n四层架构 + 聚合根 +\nDomain Service + 防腐层"]
classDef style_MVC fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0;
classDef style_Light fill:#431407,stroke:#ea580c,stroke-width:2px,color:#fed7aa;
classDef style_DDD fill:#450a0a,stroke:#dc2626,stroke-width:2px,color:#fecaca;
class MVC style_MVC;
class Light style_Light;
class DDD style_DDD;```
### 4.2 三档方案的适用范围
| 方案 | 工作量 | 适用场景 | 示例 |
|------|:---:|------|------|
| MVC(贫血模型) | 标准 | 纯 CRUD——没有复杂业务逻辑 | user-service、config-service |
| 轻量充血模型 | +30% | 有业务规则——但状态迁移简单 | product-service、inventory-service |
| 完整 DDD 四层 | +80% | 复杂业务逻辑 + 多状态 + 跨聚合 | order-service、payment-service |
关键判断依据:
决定了是否用 DDD 的 5 个信号: ① Service 类 > 200 行 → 该拆了 ② 一个方法改了 3 个以上的表 → 聚合边界不清 ③ 改一个功能要翻 3 个以上的文件 → 逻辑散落 ④ 加新功能时心里没底——“会不会影响已有逻辑?” → 没有不变量保护 ⑤ 单测写不出来——必须跑数据库 → 逻辑和持久化耦合
只要出现 2 个信号——就该考虑轻量充血了 出现 4 个以上——完整 DDD 值得投入
## 五、⚠️ DDD 的三大误区——什么时候不该用
### 误区一:所有微服务都要 DDD
❌ 错误想法: “DDD 是微服务的标配——所有服务都应该按 DDD 来写”
✅ 正确做法: user-service → MVC 就够了——20 行一个方法——CRUD 而已 notification-service → MVC 就够了——发短信——没有业务逻辑 config-service → MVC 就够了——管理配置项——就是 restful CRUD
这些服务强行 DDD: → 多了 10 个类——每个类的代码不到 30 行 → 团队抱怨"好复杂——以前就一个 Service 搞定" → 新人看不懂——“为什么查个用户要过 3 层”
### 误区二:DDD = 不用 @Transactional
❌ 错误想法: “DDD 说一个事务只改一个聚合——那就不用事务了”
✅ 正确做法: 一个事务 = 一个聚合的修改——但事务还是要的
Application Service 的 createOrder() 加了 @Transactional → 事务内只改了 Order 聚合——没改 Product 和 Account → Product 的库存扣减通过领域事件异步处理——不在 Order 的事务中 → 但 Order 聚合本身(Order + OrderItem)是在事务中的——保证原子性
DDD 不是"不用事务"——是"事务边界 = 聚合边界"
### 误区三:聚合拆得越细越好
❌ 错误做法: 把 Order 和 OrderItem 拆成两个独立的聚合——“OrderItem 也很重要” → OrderItem 有独立的 Repository——外部可以直接调 orderItemRepository.save(item) → 绕过 Order 的校验——“数量必须 > 0”、“状态必须 PENDING_PAY”——全废了
✅ 正确做法: OrderItem 在 Order 聚合内部——没有独立的 Repository 所有对 OrderItem 的修改——必须通过 Order.addItem() / Order.changeItemQuantity() Order 是唯一的入口——保护 Order 聚合内的所有不变量
## 🎯 总结
1. <strong>DDD 不是银弹——看复杂度而不是跟风</strong>:user-service(纯 CRUD)保持 MVC,product-service(有业务规则但不算复杂)做轻量充血,order-service(复杂逻辑 + 多状态 + 跨聚合)用完整 DDD。5 个信号的 checklist 帮你决定——"200 行 Service / 3 个表的事务 / 改 3 个文件 / 改代码没底 / 单测写不出来"。
2. <strong>重构后最大的变化——业务逻辑从 Service 移到聚合根</strong>:不是增加代码——是代码换了个位置。聚合根的构造函数/业务方法保护不变量——Application Service 变成纯粹的编排——5 步流程(查聚合 → 调 Domain Service → 创建聚合 → 保存 → 发事件)一目了然。
3. <strong>跨聚合的修改——从事务内移到领域事件中异步处理</strong>:扣库存和扣余额不在 Order 的事务中——而是通过 OrderCreatedEvent 异步触发。事务边界 = 聚合边界——一个事务只改一个聚合——靠 MQ + 幂等消费保证最终一致性。
4. <strong>不要为了 DDD 而 DDD——过度设计比贫血更糟糕</strong>:纯 CRUD 的 Service 强行 DDD——多了 10 个类——没人维护得了。轻量充血是 80% 场景的最优解——对象有行为——但不过度分层。
> 📖 <strong>系列回顾</strong>:DDD 三部曲到此结束——
> 1. [<strong>DDD 本质——领域驱动设计的核心概念</strong>](/posts/ddd/dddfundamentals/) —— 实体/值对象/聚合根/限界上下文/领域事件
> 2. [<strong>DDD 战术落地——代码怎么写</strong>](/posts/ddd/dddtactical/) —— 四层架构/Repository/Domain Service/防腐层/Outbox
> 3. <strong>DDD 重构实战——什么时候该用 DDD</strong>(本文) —— MVC vs 轻量充血 vs 完整 DDD 的决策框架
>
> 📖 <strong>下一步预告</strong>:代码写完了——怎么提交到 Git?怎么跑 CI?怎么自动部署?下一系列——GitLab CI/CD 全家桶:从零搭建 GitLab + Runner,编译 → 单测 → SonarQube → 构建镜像 → Harbor → 多环境部署(dev/staging/prod)。