贫血模型与充血模型是什么
贫血模型把数据与行为分开:实体只有字段与访问器,业务逻辑集中在服务层。充血模型让领域对象自带行为,服务层只做编排与事务边界。两者都不是错误——简单 CRUD 用贫血更直白,规则密集的领域用充血更稳。真正的风险是「一边贫血一边指望对象保证一致性」,规则于是散落在多个服务里。
基本信息
| 贫血模型 | 数据与行为分离,逻辑在 Service 层 |
|---|---|
| 充血模型 | 领域对象自带行为,服务只做编排 |
| 判断依据 | 规则复杂度、团队共识、是否需要强一致性约束 |
| 风险点 | 两种风格在同一模块混用,规则归属不清 |
选择与落地
- 表单式增删改查:贫血模型足够,不必强行引入领域对象
- 规则密集(计费、审批、风控):把规则收进领域对象,避免在多处服务里复制
- 领域对象内只放业务不变量,不要塞进 I/O 与事务
- 服务层负责取数、调用领域方法、提交事务与发事件
- 需要跨聚合一致性的场景,用领域事件与显式补偿而不是共享可变状态
- 两种风格并存时明确边界:哪些模块贫血、哪些充血,写进团队约定
对比
| 维度 | 贫血模型 | 充血模型 |
|---|---|---|
| 逻辑位置 | Service 层 | 领域对象内 |
| 上手成本 | 低,结构直白 | 中,需要领域建模 |
| 规则集中 | 易散落重复 | 封装在对象内 |
| 测试方式 | 依赖服务与桩 | 对象单测即可覆盖规则 |
| 适合 | CRUD、简单流程 | 规则密集的领域 |
充血示例
充血示例
class Order {
private final List<Item> items = new ArrayList<>();
private Status status = Status.DRAFT;
void cancel() {
if (status == Status.SHIPPED) {
throw new IllegalStateException("已发货订单不可取消");
}
status = Status.CANCELLED;
}
void addItem(Item item) {
if (status != Status.DRAFT) throw new IllegalStateException();
items.add(item);
}
}充血不等于把一切都放进实体:取数、发消息、开事务属于应用层职责。判断标准是「这条规则是否属于对象自身的一致性问题」——属于就放进去,需要外部协作就留在服务。
贫血到底的团队常出现同一个规则在多个 Service 复制:一处长度校验写三遍,改一次漏一处。此时不必全面改造,先把重复规则收敛到一个领域服务里就能明显降低改动成本。