贫血模型的改进

贫血模型的改进

引言

开发中,domain里常常定义了一些只含有特定字段的实体类,而相应的业务处理全部放在service中。在《架构整洁之道》(以及《领域驱动设计》)的观点下,这种"只有字段的领域对象 + 包含所有逻辑的Service"的模式,被称为"贫血领域模型"。这通常意味着没有真正采用面向对象的架构,而只是在做"事务脚本"编程。

贫血模型带来的问题

在整洁架构中,业务逻辑属于领域层,而不属于应用层(Service)。

  • 违背了封装性: 实体应该保证自己的内部状态是合法的,并且通过方法来改变状态。如果实体只有getter/setter,那么任何Service都可以随意修改它,导致业务规则(如"订单金额不能为负数")散落在各个Service中,难以维护。
  • 丧失了"告诉,不要问"原则: Service需要先询问实体的状态(order.getStatus() == PAID),然后做判断,再调用setter(order.setStatus(SHIPPED))。更好的做法是直接告诉实体:order.ship(),让实体自己判断状态是否允许发货。
  • 低内聚: 与某个实体相关的逻辑(如计算、验证)被分散在多个Service中,而不是集中在实体内部。

如何改进------重新划分职责

改进的核心思路是:将属于实体的逻辑放回实体,并严格区分应用层和领域层。

第一步:让实体"富有行为"

不要让User只是一个带id和name的struct或类。让它包含该对象固有的、通用的业务逻辑。

java 复制代码
// 错误做法:贫血模型
public class User {
    private String name;
    private String status;
    // getters and setters...
}

// 正确做法:富领域模型
public class User {
    private String name;
    private String status;

    // 构造函数,确保对象创建时就合法
    public User(String name) {
        if (name == null || name.length() < 2) {
            throw new IllegalArgumentException("用户名过短");
        }
        this.name = name;
        this.status = "ACTIVE";
    }

    // 行为方法:激活用户(实体自己控制状态流转)
    public void activate() {
        if ("BLOCKED".equals(this.status)) {
            throw new IllegalStateException("已封禁用户无法激活");
        }
        this.status = "ACTIVE";
    }

    // 业务方法:修改密码(不仅仅是setPassword,可能包含加密和校验)
    public void changePassword(String oldPassword, String newPassword, PasswordEncoder encoder) {
        // 这里可以调用encoder,但依赖是通过参数传入,避免实体依赖基础设施
        // ...
    }
    
    // 可以不提供setter,或者只提供受保护的setter给ORM框架
}
第二步:明确Service的角色

通常将提到的Service细分为两层:

  1. 应用服务(Application Service)【即用例层】:
    • 职责: orchestrator(编排者)。它负责接收外部输入(DTO),验证权限,决定调用哪个领域对象,协调领域对象完成业务,然后调用基础设施(如Repository)保存结果。
    • 特点: 无业务逻辑,只有任务编排。它不应该写if/else判断业务规则,业务规则应该在领域对象内部。
  2. 领域服务(Domain Service):
    • 职责: 处理那些不适合放在某个具体实体中的逻辑。例如:两个账户之间的转账(涉及两个Account实体)、或者需要调用外部接口的复杂计算。
    • 特点: 属于领域层,操作的是领域对象。

Mapper层的本质与调用关系

在传统的三层架构(Controller-Service-Mapper)中,Mapper是持久化框架(如MyBatis)的一部分,通常由Service直接调用,负责将数据库记录映射成对象。但在整洁架构中,这个视角需要转变。

持久化Mapper(即通常理解的DAO/Repository实现):
  • 本质: 它属于基础设施层。
  • 调用者: 在整洁架构中,应用层或领域层不直接依赖Mapper接口。它们依赖的是Repository接口。
  • 机制(依赖倒置):
    • 在领域层定义 UserRepository 接口(这是一个抽象,声明了 save(User user) 等方法)。
    • 在基础设施层实现这个接口(MyBatisUserRepositoryImpl),这个实现内部会调用真正的 MyBatis Mapper 来进行CRUD。
    • 调用链: 应用层Service -> UserRepository 接口(依赖注入) -> MyBatisUserRepositoryImpl -> MyBatis Mapper -> 数据库。
    • 图景:
      • Controller (请求进入)
      • -> Application Service (编排用例,调用Repository接口)
      • -> Repository 接口 (定义在Domain层)
      • -> Repository 实现 (在Infrastructure层,内部调用Mapper)
      • -> Mapper/ORM (真正读写数据库)

总结:改进后的分层调用关系

  1. Controller/Adapter: 接收HTTP请求,将DTO通过 DTO-Entity Mapper 转换成 领域对象(或直接传参给应用服务),调用 Application Service。
  2. Application Service (用例层):
    • 获取当前用户上下文,权限判断。
    • 通过 Repository接口 从数据库加载 领域对象(此时对象是活的,带有行为)。
    • 调用领域对象的业务方法(如 order.completePayment(amount))。
    • 再次通过 Repository接口 保存领域对象。
  3. 领域层:定义领域模型和领域服务,执行核心业务逻辑,维护自身状态的正确性。定义Repository接口,获取和存储聚合根的契约。
  4. Infrastructure (含MyBatis Mapper/ORM):
    • 实现 UserRepository 接口。
    • 在实现类中,调用 MyBatis 的 Mapper 或 JPA 将内存中的领域对象状态同步回数据库。
    • 注意:如果使用JPA这类ORM,实体本身可能带有注解,这是为了映射表结构,此时JPA注解可以视为基础设施的一部分,但应尽量保持领域对象干净,可以用XML配置或代码生成的方式隔离。

通过这样分层,的领域层就变得独立且可测试了,不再依赖数据库和框架,让技术和业务分离,独自演化。


愿我都能在各自的领域里不断成长,勇敢追求梦想,同时也保持对世界的好奇与善意!

相关推荐
lsx2024062 小时前
AngularJS 事件处理机制详解
开发语言
小书房2 小时前
Kotlin的内联函数
java·开发语言·kotlin·inline·内联函数
码农阿豪2 小时前
Python 操作金仓数据库的完全指南(上篇):连接管理与高可用
开发语言·数据库·python
计算机学姐2 小时前
基于微信小程序的校园失物招领管理系统【uniapp+springboot+vue】
java·vue.js·spring boot·mysql·信息可视化·微信小程序·uni-app
xyq20243 小时前
CSS Backgrounds(背景)
开发语言
Aurorar0rua3 小时前
CS50 x 2024 Notes C - 06
开发语言·学习方法
AI服务老曹3 小时前
架构实战:基于 GB28181 与 RTSP 的异构设备统一接入方案,深度解析 Docker 化 AI 视频管理平台
人工智能·docker·架构
xyq20243 小时前
SQLite Like 子句详解
开发语言
Highcharts.js3 小时前
线形比赛积分增长或竞赛图|Highcharts企业图表代码示列
开发语言·前端·javascript·折线图·highcharts·竞赛图