贫血模型是仅有字段和getter/setter的pojo,源于spring+orm实践与快速交付压力下的隐性妥协;充血模型要求对象封装自身行为,如iscancelable(),但需避免将数据库操作或外部调用写入entity。

贫血模型就是“只有字段的POJO”,不是设计选择而是隐性妥协
Java里看到User、Order这类类,只有private字段 + getXXX()/setXXX(),没任何校验、计算、状态流转逻辑——这基本就是贫血模型。它不是谁刻意设计出来的,而是Spring + MyBatis/ORM早期实践+快速交付压力下的自然产物。
- 常见错误现象:
UserService.cancelOrder()里反复判断order.getStatus() == 0、order.getCreatedAt().isBefore(Instant.now().minusHours(2)),同一段状态校验散落在5个Service方法里 - 使用场景:CRUD密集、业务规则简单(如后台管理系统的用户列表增删改查)、团队对DDD无共识、工期紧要先跑通流程
- 性能影响不大,但单元测试必须mock整个
OrderService才能测一个取消逻辑,领域规则无法被复用或独立验证
充血模型要求“对象自己管好自己的事”,但别一上来就往Entity里塞事务
充血不是把所有Service方法剪切粘贴进Order类。关键判断标准是:这个逻辑是否天然属于该对象的语义边界?比如order.isCancelable()、order.calculateTotal()可以,但order.sendNotification()或order.persistToDB()就不该放。
- 正确示例:
public class Order { private Integer status; private Instant createdAt; public boolean isCancelable() { return status == 0 && createdAt.isAfter(Instant.now().minusHours(2)); } public void cancel() { if (!isCancelable()) { throw new IllegalStateException("订单不可取消"); } this.status = 2; } } - 容易踩的坑:把数据库操作(
orderRepository.save(this))、外部调用(paymentClient.refund())写进Entity——这会让对象失去可测试性,也违反单一职责 - 兼容性注意:JPA/Hibernate默认要求Entity有无参构造、setter,若用Lombok的
@Data又加了setXXX(),等于变相鼓励外部随意修改状态,削弱封装性
Service层不该是“业务逻辑垃圾场”,而是协调者和守门人
充血模型下,OrderService不该再写if (order.getStatus() == 0) { ... },而应聚焦跨聚合、事务边界、权限、异步调度等非领域内职责。
- 典型职责:
placeOrder()协调Order创建、库存扣减、支付单生成;cancelOrder(Order order)只做事务控制和事件发布,具体“能不能取消”交给order.isCancelable() - 参数差异:贫血模型Service方法参数常是
Long orderId或OrderDTO;充血模型更倾向直接接收已加载的Order实体,避免重复查询和状态不一致 - 一个信号:如果Service方法里出现超过2个
if判断对象状态,大概率逻辑本该下沉到模型里
别用“是不是DDD”来决定模型,看业务复杂度和团队认知水位
小项目硬上充血模型,最后可能变成Order里混着发邮件、调HTTP、连Redis——这不是充血,是混乱。反过来,金融核心系统用贫血模型,Account.withdraw()逻辑散在10个Service里,审计和合规根本没法落地。
立即学习“Java免费学习笔记(深入)”;
- 真实分水岭:当同一个业务规则(如“用户余额不足不能下单”)在3个以上用例中重复出现,且规则本身会随监管变化频繁调整,就该考虑充血
- 最容易被忽略的点:充血模型成败不取决于代码写在哪,而在于团队是否接受“状态变更必须通过对象方法触发”。哪怕只是加个
order.setStatus(2)前加个order.cancel()包装,也是向充血迈出的第一步











