聊一聊贫血模型和充血模型区别

20 阅读4分钟

在软件设计中,贫血模型(Anemic Domain Model)和充血模型(Rich Domain Model)是两种截然不同的领域模型设计模式,尤其在领域驱动设计(DDD)中它们的区别至关重要。以下是详细解释和对比:


1. 贫血模型(Anemic Domain Model)

定义

  • 数据与行为分离:领域对象(如实体、值对象)仅包含数据属性(Getter/Setter),业务逻辑分散在服务层(Service类)中。
  • 典型特征
    • 领域对象是“哑巴数据容器”。
    • 业务逻辑集中在Service类中,通过操作领域对象完成功能。
    • 常见于传统分层架构(如Spring MVC + DAO模式)。

代码示例

// 贫血的Order类(仅有数据)
public class Order {
    private String orderId;
    private List<OrderItem> items;
    private BigDecimal totalAmount;
    private String status;

    // Getter/Setter 方法(无业务逻辑)
    public String getOrderId() { return orderId; }
    public void setOrderId(String orderId) { this.orderId = orderId; }
    // ... 其他Getter/Setter
}

// 业务逻辑集中在Service类
@Service
public class OrderService {
    public void confirmOrder(Order order) {
        if (order.getItems().isEmpty()) {
            throw new IllegalStateException("订单不能为空");
        }
        order.setStatus("CONFIRMED");
        orderRepository.save(order);
    }
}

缺点

  • 业务逻辑分散:规则散落在多个Service类中,难以维护。
  • 领域对象无意义:对象仅是数据载体,无法表达业务意图。
  • 容易产生重复代码:相同逻辑可能在多个Service中重复实现。
  • 违背面向对象设计:对象失去封装性,成为“数据表映射工具”。

2. 充血模型(Rich Domain Model)

定义

  • 数据与行为统一:领域对象既包含数据属性,也封装业务逻辑
  • 核心原则
    • 高内聚:对象的行为与数据紧密关联。
    • 低耦合:通过方法暴露操作,隐藏内部实现细节。
    • 符合面向对象设计:对象是“智能的”,能自主完成职责。

代码示例

// 充血的Order类(数据 + 行为)
public class Order {
    private String orderId;
    private List<OrderItem> items;
    private Money totalAmount;
    private OrderStatus status;

    // 业务逻辑内聚到聚合根
    public void addItem(OrderItem item) {
        if (status != OrderStatus.DRAFT) {
            throw new IllegalStateException("已确认的订单不可修改");
        }
        items.add(item);
        totalAmount = totalAmount.add(item.calculateTotal());
    }

    public void confirm() {
        if (items.isEmpty()) {
            throw new IllegalStateException("订单不能为空");
        }
        this.status = OrderStatus.CONFIRMED;
    }

    // Getter方法(无Setter,通过方法控制状态变更)
    public String getOrderId() { return orderId; }
    public List<OrderItem> getItems() { return Collections.unmodifiableList(items); }
}

// 应用服务仅协调流程
@Service
public class OrderApplicationService {
    public void confirmOrder(String orderId) {
        Order order = orderRepository.findById(orderId);
        order.confirm();  // 业务逻辑在领域对象中
        orderRepository.save(order);
    }
}

优点

  • 业务逻辑集中:规则内聚在领域对象中,易于理解和维护。
  • 表达业务语义:代码直接反映业务概念(如order.confirm())。
  • 减少重复代码:相同逻辑无需在多个Service中重复。
  • 符合DDD原则:支持限界上下文、聚合等模式。

3. 贫血模型 vs. 充血模型

维度贫血模型充血模型
业务逻辑位置分散在Service类中内聚在领域对象中
对象职责数据容器数据 + 行为
代码可维护性低(逻辑分散)高(逻辑集中)
面向对象设计违背封装性符合封装性
典型应用场景简单CRUD系统复杂业务系统(如电商、金融)

4. 如何避免贫血模型?

设计原则

  1. Tell, Don't Ask
    对象应主动执行操作,而非被外部查询状态后修改。

    // ❌ 贫血模型:Service查询状态并修改
    if (order.getStatus().equals("DRAFT")) {
        order.setStatus("CONFIRMED");
    }
    
    // ✅ 充血模型:对象自己控制状态变更
    order.confirm();
    
  2. 封装不变条件
    将业务规则内聚到对象方法中,例如:

    public class BankAccount {
        private BigDecimal balance;
        
        public void withdraw(BigDecimal amount) {
            if (balance.compareTo(amount) < 0) {
                throw new InsufficientBalanceException();
            }
            this.balance = balance.subtract(amount);
        }
    }
    
  3. 减少公开Setter
    通过方法暴露操作,而非直接修改字段:

    // ❌ 允许外部随意修改状态
    order.setStatus("CONFIRMED");
    
    // ✅ 仅通过业务方法变更状态
    order.confirm();
    

5. 充血模型的实践挑战

  • 框架适配:JPA/Hibernate等ORM工具可能要求公开Setter或默认构造函数,可通过以下方式解决:

    • 使用protected修饰符限制访问。
    • 通过DTO与领域对象转换(如MapStruct)。
    @Entity
    public class OrderJpaEntity {
        // JPA要求字段可访问,但领域对象仍可封装逻辑
        private String status;
        
        // 转换方法:将JPA实体转换为领域对象
        public Order toDomain() {
            Order order = new Order(id);
            // 通过领域对象的方法恢复状态
            if ("CONFIRMED".equals(status)) {
                order.confirm();
            }
            return order;
        }
    }
    
  • 性能优化:避免因过度封装导致性能问题(如频繁加载关联对象),可通过延迟加载或CQRS解决。


总结

  • 贫血模型是反模式,将数据与行为分离,导致代码臃肿和维护困难。
  • 充血模型是DDD的核心实践,通过内聚业务逻辑到领域对象中,提升代码表达力和可维护性。
  • 关键设计原则:封装业务规则减少公开Setter通过方法暴露操作