什么是贫血模式
一、什么是贫血模式(Anemic Domain Model)?
贫血模式 是一种反模式,指的是领域模型中只包含属性(getter/setter),而没有业务逻辑或行为的实现。所有的业务逻辑都集中在外部的服务类中处理,而不是封装在实体或值对象内部。
换句话说:
贫血模型 = 数据 + 外部服务,缺乏内聚的业务逻辑封装
二、为什么会产生贫血模式?
贫血模式通常是由以下几种原因导致的:
1. 过度依赖框架设计
很多现代框架(如 Spring、Hibernate)鼓励使用 POJO(Plain Old Java Object)来映射数据库表结构,强调数据驱动开发,忽略了对业务逻辑的封装。
- 实体变成只是数据库字段的映射。
- 所有业务逻辑放在
Service
层,实体成为“纯数据容器”。
2. 错误地理解 DDD 中的领域服务
开发者误以为所有业务逻辑都应该放在领域服务中,而不去思考是否可以将某些逻辑封装进聚合根或实体本身。
3. 团队协作与职责划分不清
多个团队可能分别负责不同的模块(如数据访问、业务逻辑),为了方便协作,把业务逻辑抽离到统一的服务层,结果导致实体空洞。
4. 追求“解耦”但牺牲了封装性
认为将业务逻辑抽离到服务中能提高灵活性和可测试性,但忽视了领域模型本身的封装性和语义完整性。
三、贫血模式的危害
危害 | 描述 |
---|---|
破坏面向对象的核心思想 | 面向对象的本质是“数据+行为”的封装,贫血模型将行为外移,违背了这一原则。 |
降低代码可读性和维护性 | 业务逻辑分散在多个服务中,难以追踪和维护。 |
容易引发重复代码 | 同样的逻辑可能在多个服务中被重复实现。 |
违反单一职责原则 | 服务承担过多职责,变得臃肿,难以测试和扩展。 |
四、举例说明
场景:银行转账系统
1. 贫血模型(错误做法)
// 贫血实体
public class Account {private String accountId;private BigDecimal balance;// Getter and Setter
}// 服务类集中处理业务逻辑
public class AccountService {public void transfer(Account fromAccount, Account toAccount, BigDecimal amount) {if (fromAccount.getBalance().compareTo(amount) < 0) {throw new InsufficientFundsException();}fromAccount.setBalance(fromAccount.getBalance().subtract(amount));toAccount.setBalance(toAccount.getBalance().add(amount));}
}
在这个例子中,所有的业务逻辑都在 AccountService
中完成,而 Account
类只是一个数据容器,没有任何行为。这就是典型的贫血模型。
2. 充血模型(正确做法)
// 充血实体
public class Account {private String accountId;private BigDecimal balance;public void withdraw(BigDecimal amount) {if (balance.compareTo(amount) < 0) {throw new InsufficientFundsException();}balance = balance.subtract(amount);}public void deposit(BigDecimal amount) {balance = balance.add(amount);}// Getter and other methods
}// 领域服务仅协调多个聚合
public class TransferService {public void transfer(Account from, Account to, BigDecimal amount) {from.withdraw(amount);to.deposit(amount);}
}
在这个例子中,Account
自己管理自己的状态变化(withdraw/deposit),符合面向对象的设计理念。TransferService
只负责协调两个账户之间的转账操作。
五、如何避免贫血模式?
-
将业务逻辑封装在聚合根或实体中
比如订单的calculateTotalPrice()
方法应属于Order
聚合根,而不是放到服务中。 -
合理使用领域服务
只有当一个行为涉及多个聚合时,才考虑用领域服务。例如跨账户转账需要协调两个账户,这时候可以用领域服务。 -
使用值对象封装业务规则
如Money
值对象封装金额计算规则,而不是让服务去做加减法。 -
坚持“不变性”原则
聚合应在每次变更后保持一致性,这些逻辑应由聚合自己保证。 -
持续重构
定期检查服务类是否有太多逻辑可以下推到实体或值对象中。
六、总结
对比项 | 贫血模型 | 充血模型 |
---|---|---|
实体 | 仅有属性 | 包含行为 |
服务 | 承担大部分逻辑 | 协调多个聚合 |
设计风格 | 过程式编程 | 面向对象 |
维护难度 | 高 | 低 |
是否符合 DDD | 否 | 是 |
✅ 结论
贫血模型是一种不良的设计模式,它违背了面向对象的基本原则,也违背了 DDD 的核心理念——将业务逻辑封装在领域模型中。我们应该尽可能地将行为封装在实体、值对象或聚合中,只有在无法避免的情况下才使用领域服务,从而避免陷入贫血模式的陷阱。