贫血模型 vs 充血模型深度指南:领域模型设计的核心艺术
在面向对象设计和领域驱动设计(DDD)中,一个核心问题是:业务逻辑应该放在哪里?
这个问题直接引出了两种截然不同的建模风格:贫血模型(Anemic Domain Model)和充血模型(Rich Domain Model)。理解这两种模型的差异,对于构建可维护、可扩展的软件系统至关重要。
什么是贫血模型?
贫血模型是一种反模式,在这种设计中,领域对象(通常是Java中的POJO或Python中的数据类)仅包含数据(属性),而不包含任何业务逻辑。
典型特征
// 贫血模型示例 - Java
public class Order {
private String id;
private List<OrderItem> items;
private BigDecimal total;
private OrderStatus status;
// 只有getter和setter,没有任何业务方法
public String getId() { return id; }
public void setId(String id) { this.id = id; }
// ... 其他getter/setter
}
// 业务逻辑全部放在Service层
public class OrderService {
public void calculateTotal(Order order) {
BigDecimal sum = BigDecimal.ZERO;
for (OrderItem item : order.getItems()) {
sum = sum.add(item.getPrice().multiply(item.getQuantity()));
}
order.setTotal(sum);
}
public void submitOrder(Order order) {
if (order.getStatus() != OrderStatus.DRAFT) {
throw new IllegalStateException("只能提交草稿状态的订单");
}
// ... 更多验证和业务逻辑
order.setStatus(OrderStatus.SUBMITTED);
}
}
贫血模型的表象
- 对象 = 数据容器:只有getter/setter
- Service类膨胀:所有业务逻辑集中在Service层
- 缺乏封装:业务规则散落在各处
- 难以测试:需要mock大量依赖
什么是充血模型?
充血模型将业务逻辑直接放在领域对象中,对象既是数据的载体,也是行为的承担者。
典型特征
// 充血模型示例 - Java
public class Order {
private String id;
private List<OrderItem> items;
private OrderStatus status;
// 业务方法直接存在于领域对象中
public BigDecimal calculateTotal() {
return items.stream()
.map(item -> item.getPrice().multiply(item.getQuantity()))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
public void submit() {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("只能提交草稿状态的订单");
}
validateOrder();
this.status = OrderStatus.SUBMITTED;
}
public void cancel() {
if (status == OrderStatus.SHIPPED || status == OrderStatus.DELIVERED) {
throw new IllegalStateException("已发货的订单无法取消");
}
this.status = OrderStatus.CANCELLED;
}
private void validateOrder() {
if (items == null || items.isEmpty()) {
throw new IllegalArgumentException("订单不能为空");
}
}
}
// Service变成协调者,而非业务逻辑的主要载体
public class OrderService {
private OrderRepository orderRepository;
public void createOrder(Order order) {
// 协调工作:保存、发送通知等
orderRepository.save(order);
notificationService.sendOrderCreated(order);
}
}
充血模型的优点
- 真正的封装:业务规则内聚在领域对象中
- 易于测试:直接测试领域对象的行为
- 表达力更强:代码更接近业务语言
- 降低重复:业务逻辑不会散落在多处
为什么贫血模型如此普遍?
尽管充血模型在理论上更"正确",但贫血模型在业界极为普遍,原因包括:
1. 框架的影响
很多ORM框架(如早期Hibernate)鼓励使用贫血模型,将实体视为简单的数据映射。
2. 事务脚本的惯性
开发者习惯于"过程式"编程,Service方法处理一切。
3. 团队知识储备不足
充血模型需要更深的OO和DDD设计能力。
4. 过度设计的担忧
小项目使用充血模型似乎"杀鸡用牛刀"。
如何从贫血模型转向充血模型?
步骤1:识别行为
找出Service中与特定实体相关的方法:
// Before: OrderService中的
public void applyDiscount(Order order, BigDecimal discount) {
if (discount.compareTo(order.getTotal()) > 0) {
throw new IllegalArgumentException("折扣不能超过订单金额");
}
order.setDiscount(discount);
}
步骤2:迁移到领域对象
// After: 移到Order类中
public void applyDiscount(BigDecimal discount) {
if (discount.compareTo(this.calculateTotal()) > 0) {
throw new IllegalArgumentException("折扣不能超过订单金额");
}
this.discount = discount;
}
步骤3:重构Service
Service变为协调者,处理事务边界和跨聚合操作:
@Service
public class OrderService {
private OrderRepository orderRepository;
@Transactional
public void placeOrder(Order order) {
// 协调多个领域对象
inventoryService.reserve(order.getItems());
orderRepository.save(order);
// ...
}
}
何时使用哪种模型?
选择贫血模型的情况
- 简单CRUD应用:业务逻辑极其简单
- 遗留系统维护:重构风险过高
- 团队DDD经验不足
- 快速原型开发
选择充血模型的情况
- 复杂业务域:需要清晰的业务规则表达
- 长期维护项目:可维护性至关重要
- DDD实践:充血模型是DDD的基础
- 需要高可测试性
总结
贫血模型不是"罪",但在复杂业务场景下,它会导致代码难以维护、测试困难、业务规则散落等问题。
充血模型代表了面向对象的本质——对象既是状态,也是行为。它需要更多的设计经验,但能带来更好的代码表达力和可维护性。
关键是理解两种模型的权衡,根据实际场景做出明智的选择。对于复杂度高的业务域,投资充血模型的设计是值得的。
推荐阅读:《领域驱动设计》- Eric Evans
本文是软件架构深度指南系列文章,欢迎关注更多架构设计相关内容。
Top comments (0)