DEV Community

架构师小白
架构师小白

Posted on

贫血模型 vs 充血模型深度指南:领域模型设计的核心艺术

贫血模型 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

贫血模型的表象

  • 对象 = 数据容器:只有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);
    }
}
Enter fullscreen mode Exit fullscreen mode

充血模型的优点

  • 真正的封装:业务规则内聚在领域对象中
  • 易于测试:直接测试领域对象的行为
  • 表达力更强:代码更接近业务语言
  • 降低重复:业务逻辑不会散落在多处

为什么贫血模型如此普遍?

尽管充血模型在理论上更"正确",但贫血模型在业界极为普遍,原因包括:

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);
}
Enter fullscreen mode Exit fullscreen mode

步骤2:迁移到领域对象

// After: 移到Order类中
public void applyDiscount(BigDecimal discount) {
    if (discount.compareTo(this.calculateTotal()) > 0) {
        throw new IllegalArgumentException("折扣不能超过订单金额");
    }
    this.discount = discount;
}
Enter fullscreen mode Exit fullscreen mode

步骤3:重构Service

Service变为协调者,处理事务边界和跨聚合操作:

@Service
public class OrderService {
    private OrderRepository orderRepository;

    @Transactional
    public void placeOrder(Order order) {
        // 协调多个领域对象
        inventoryService.reserve(order.getItems());
        orderRepository.save(order);
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

何时使用哪种模型?

选择贫血模型的情况

  • 简单CRUD应用:业务逻辑极其简单
  • 遗留系统维护:重构风险过高
  • 团队DDD经验不足
  • 快速原型开发

选择充血模型的情况

  • 复杂业务域:需要清晰的业务规则表达
  • 长期维护项目:可维护性至关重要
  • DDD实践:充血模型是DDD的基础
  • 需要高可测试性

总结

贫血模型不是"罪",但在复杂业务场景下,它会导致代码难以维护、测试困难、业务规则散落等问题。

充血模型代表了面向对象的本质——对象既是状态,也是行为。它需要更多的设计经验,但能带来更好的代码表达力和可维护性。

关键是理解两种模型的权衡,根据实际场景做出明智的选择。对于复杂度高的业务域,投资充血模型的设计是值得的。


推荐阅读:《领域驱动设计》- Eric Evans


本文是软件架构深度指南系列文章,欢迎关注更多架构设计相关内容。

Top comments (0)