贫血模型 vs 充血模型:前端业务逻辑应该写在 Service 层还是 Entity 类中?

贫血模型 vs 充血模型:前端业务逻辑该写在 Service 层还是 Entity 类中?

各位开发者朋友,大家好!今天我们来聊一个看似简单、实则非常关键的话题——贫血模型(Anemic Domain Model)与充血模型(Rich Domain Model)的区别,以及在实际项目中,业务逻辑到底应该放在 Service 层还是 Entity 类中?

这个问题不是“非黑即白”的选择题,而是一个需要结合团队规模、项目复杂度、维护成本和未来演进能力的综合判断题。如果你正在设计一个系统架构,或者已经在用某种模式但感到困惑,那这篇讲座式的文章非常适合你。


一、什么是贫血模型?什么是充血模型?

先从定义讲起。

✅ 贫血模型(Anemic Domain Model)

Entity 只有属性 + Getter/Setter,没有行为;所有业务逻辑都在 Service 层处理。

典型表现:

// User.java - 贫血模型示例
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;

    // getter/setter 省略...
}

// UserService.java - 所有业务逻辑都在这里
@Service
public class UserService {

    public void registerUser(User user) {
        if (user.getAge() < 18) {
            throw new IllegalArgumentException("用户年龄必须大于等于18");
        }
        if (!isValidEmail(user.getEmail())) {
            throw new IllegalArgumentException("邮箱格式不合法");
        }
        // save to database...
    }

    private boolean isValidEmail(String email) {
        return email != null && email.contains("@");
    }
}

✅ 好处:

  • 结构清晰,容易理解;
  • 单元测试简单(因为实体无状态逻辑);
  • 初期开发快,适合小型项目或 CRUD 导向的系统。

❌ 缺点:

  • 业务逻辑分散在多个 Service 中,难以复用;
  • 实体变成“数据容器”,丧失了语义表达力;
  • 当业务变复杂时,Service 层会变得臃肿不堪(俗称“上帝类”)。

✅ 充血模型(Rich Domain Model)

Entity 包含自己的行为(方法),Service 层主要负责协调和调用,而不是封装核心逻辑。

典型表现:

// User.java - 充血模型示例
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;

    public void register() {
        if (age < 18) {
            throw new IllegalStateException("用户年龄必须大于等于18");
        }
        if (!isValidEmail()) {
            throw new IllegalStateException("邮箱格式不合法");
        }
    }

    private boolean isValidEmail() {
        return email != null && email.contains("@");
    }

    // getter/setter ...
}

// UserService.java - 仅做协调工作
@Service
public class UserService {

    public void createUser(User user) {
        user.register(); // 把业务逻辑交给 entity 自己处理
        userRepository.save(user);
    }
}

✅ 好处:

  • 高内聚:每个实体知道自己如何被使用;
  • 更贴近现实世界建模(比如 “一个订单应该知道它是否已支付”);
  • 易于扩展和重构,尤其适合领域驱动设计(DDD)场景。

❌ 缺点:

  • 对初学者不够友好,需要一定设计思维;
  • 测试稍复杂(需 mock 外部依赖如数据库);
  • 如果滥用,也可能导致 Entity 过于复杂(违反单一职责原则)。

二、为什么这个问题如此重要?

我们来看一组真实案例对比:

场景 贫血模型问题 充血模型优势
用户注册流程变更 修改多个 Service 方法,可能遗漏某个地方 修改 User 的 register() 方法即可,统一入口
订单状态流转 每个 Service 写一遍 if(order.getStatus() == X) Order 自己管理状态变化规则(如:只能从 PENDING → PAID)
团队协作 多人同时改同一个 Service,冲突频繁 各自专注不同 Entity 的行为实现,减少耦合

📌 关键洞察:

业务逻辑的本质是“对象的行为”,而不是“服务的调用”。
把业务逻辑塞进 Service 层,就像把家庭成员的责任都推给居委会一样——效率低下且缺乏归属感。


三、实战建议:什么时候用哪种模型?

下面这张表格帮你快速决策:

项目特征 推荐模型 理由
小型项目 / MVP / 快速原型 ❗️贫血模型 开发快,无需过度设计,后期可迁移
中大型项目 / DDD 应用 ✅ 充血模型 有利于长期维护、团队协作、模块化拆分
团队成员经验不足 ❗️贫血模型 学习曲线低,避免初期陷入复杂设计陷阱
已有大量贫血代码 🔄 渐进式改造 不要一次性重写全部,优先对高频使用的 Entity 改造
涉及复杂状态机(如订单、审批流) ✅ 充血模型 Entity 自身能表达状态转移逻辑,更安全可靠

💡 补充建议:
不要一刀切!你可以在一个项目中混合使用两种模型。例如:

  • 核心实体(如 User、Order)用充血模型;
  • 辅助工具类(如 EmailUtil、DateHelper)保持独立;
  • 数据传输对象 DTO 依然用贫血结构。

四、代码示例:从贫血到充血的演进过程

假设我们要实现一个简单的“创建用户并发送欢迎邮件”的功能。

Step 1: 贫血模型版本(初始阶段)

@Entity
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
    // getter/setter
}

@Service
public class UserService {

    public void createUser(User user) {
        if (user.getAge() < 18) {
            throw new IllegalArgumentException("未成年不能注册");
        }
        if (!isValidEmail(user.getEmail())) {
            throw new IllegalArgumentException("邮箱无效");
        }

        userRepository.save(user);
        sendWelcomeEmail(user.getEmail());
    }

    private boolean isValidEmail(String email) {
        return email != null && email.matches("^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\.[A-Za-z]{2,})$");
    }

    private void sendWelcomeEmail(String email) {
        // 发送邮件逻辑...
    }
}

👉 问题:UserService 负责太多事,而且 isValidEmail 是通用逻辑却绑定了 User。


Step 2: 引入充血模型(推荐做法)

@Entity
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;

    public void register() {
        validate();
        // 如果验证通过,才允许保存
    }

    private void validate() {
        if (age < 18) {
            throw new IllegalStateException("用户年龄必须大于等于18");
        }
        if (!isValidEmail()) {
            throw new IllegalStateException("邮箱格式不合法");
        }
    }

    private boolean isValidEmail() {
        return email != null && email.matches("^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\.[A-Za-z]{2,})$");
    }

    // getter/setter
}

@Service
public class UserService {

    public void createUser(User user) {
        user.register(); // 交由实体自己决定是否可以注册
        userRepository.save(user);
        emailService.sendWelcomeEmail(user.getEmail()); // 分离关注点
    }
}

✅ 效果:

  • UserService 变得干净;
  • User 成为真正的领域对象;
  • 若将来需要支持多种注册方式(微信、手机号等),只需扩展 User 的行为即可。

五、常见误区澄清

❌ 误区一:“Entity 不能有逻辑,否则就是业务层乱套”

这是典型的误解。Entity 的逻辑 ≠ Service 的逻辑。
✅ 正确理解:Entity 的逻辑是“我是什么样的对象”,Service 的逻辑是“怎么操作这个对象”。

❌ 误区二:“充血模型会让 Entity 太复杂,不好维护”

其实不然。只要遵循以下原则就不会过载:

  • 单一职责:每个 Entity 只管自己的生命周期和规则;
  • 防御性编程:内部校验尽量提前拦截错误;
  • 依赖注入隔离:Entity 不应直接访问数据库或网络服务,而是通过参数传入。

比如:

public class User {
    private final EmailValidator validator; // 依赖注入进来,而非硬编码

    public User(EmailValidator validator) {
        this.validator = validator;
    }

    public void register() {
        if (!validator.isValid(email)) {
            throw new IllegalStateException("邮箱非法");
        }
        // ...
    }
}

这样既保证了灵活性,又避免了 Entity 直接持有外部资源。


六、总结:如何选择?

维度 贫血模型 充血模型
设计难度 ⭐☆☆☆☆ ⭐⭐⭐☆☆
可读性 ⭐⭐⭐⭐☆ ⭐⭐⭐⭐☆
可维护性 ⭐⭐☆☆☆ ⭐⭐⭐⭐⭐
扩展性 ⭐⭐☆☆☆ ⭐⭐⭐⭐⭐
团队适应性 ⭐⭐⭐⭐☆ ⭐⭐☆☆☆

🎯 最终结论:

对于大多数现代后端系统(尤其是微服务、DDD、高并发场景),强烈推荐使用充血模型。
它让代码更贴近真实业务语义,也更容易演化。
而贫血模型更适合快速上线的小型项目或遗留系统的过渡方案。

记住一句话:

“Entity 应该像人一样有思想,而不是像数据表一样只有字段。”


七、延伸思考:未来的趋势是什么?

随着领域驱动设计(DDD)、事件溯源(Event Sourcing)、CQRS 架构的普及,越来越多的团队开始拥抱充血模型。Spring Boot + JPA + DDD 的组合已经成为企业级开发的新标准。

未来几年,你会看到更多框架主动鼓励你编写“智能实体”而不是“哑巴数据类”。比如:

  • Quarkus 的 Panache ORM 提供了轻量级 Entity 行为支持;
  • Hibernate Reactive 在异步环境下天然适合充血模型;
  • Kotlin + Coroutines + Spring Boot 让 Entity 的状态管理更加优雅。

所以,现在花点时间学习充血模型,不仅是技术升级,更是职业发展的投资!


希望这篇文章能帮你理清思路,不再纠结“业务逻辑该放哪儿”。如果你觉得有用,请分享给你的同事或团队,一起迈向更好的代码质量!

谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注