贫血模型的改进
贫血模型的改进引言贫血模型带来的问题如何改进——重新划分职责第一步让实体“富有行为”第二步明确Service的角色Mapper层的本质与调用关系持久化Mapper即通常理解的DAO/Repository实现总结改进后的分层调用关系引言开发中domain里常常定义了一些只含有特定字段的实体类而相应的业务处理全部放在service中。在《架构整洁之道》以及《领域驱动设计》的观点下这种“只有字段的领域对象 包含所有逻辑的Service”的模式被称为“贫血领域模型”。这通常意味着没有真正采用面向对象的架构而只是在做“事务脚本”编程。贫血模型带来的问题在整洁架构中业务逻辑属于领域层而不属于应用层Service。违背了封装性 实体应该保证自己的内部状态是合法的并且通过方法来改变状态。如果实体只有getter/setter那么任何Service都可以随意修改它导致业务规则如“订单金额不能为负数”散落在各个Service中难以维护。丧失了“告诉不要问”原则 Service需要先询问实体的状态order.getStatus() PAID然后做判断再调用setterorder.setStatus(SHIPPED)。更好的做法是直接告诉实体order.ship()让实体自己判断状态是否允许发货。低内聚 与某个实体相关的逻辑如计算、验证被分散在多个Service中而不是集中在实体内部。如何改进——重新划分职责改进的核心思路是将属于实体的逻辑放回实体并严格区分应用层和领域层。第一步让实体“富有行为”不要让User只是一个带id和name的struct或类。让它包含该对象固有的、通用的业务逻辑。// 错误做法贫血模型publicclassUser{privateStringname;privateStringstatus;// getters and setters...}// 正确做法富领域模型publicclassUser{privateStringname;privateStringstatus;// 构造函数确保对象创建时就合法publicUser(Stringname){if(namenull||name.length()2){thrownewIllegalArgumentException(用户名过短);}this.namename;this.statusACTIVE;}// 行为方法激活用户实体自己控制状态流转publicvoidactivate(){if(BLOCKED.equals(this.status)){thrownewIllegalStateException(已封禁用户无法激活);}this.statusACTIVE;}// 业务方法修改密码不仅仅是setPassword可能包含加密和校验publicvoidchangePassword(StringoldPassword,StringnewPassword,PasswordEncoderencoder){// 这里可以调用encoder但依赖是通过参数传入避免实体依赖基础设施// ...}// 可以不提供setter或者只提供受保护的setter给ORM框架}第二步明确Service的角色通常将提到的Service细分为两层应用服务Application Service【即用例层】职责 orchestrator编排者。它负责接收外部输入DTO验证权限决定调用哪个领域对象协调领域对象完成业务然后调用基础设施如Repository保存结果。特点 无业务逻辑只有任务编排。它不应该写if/else判断业务规则业务规则应该在领域对象内部。领域服务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 (真正读写数据库)总结改进后的分层调用关系Controller/Adapter 接收HTTP请求将DTO通过 DTO-Entity Mapper 转换成 领域对象或直接传参给应用服务调用 Application Service。Application Service (用例层)获取当前用户上下文权限判断。通过 Repository接口 从数据库加载 领域对象此时对象是活的带有行为。调用领域对象的业务方法如order.completePayment(amount)。再次通过 Repository接口 保存领域对象。领域层定义领域模型和领域服务执行核心业务逻辑维护自身状态的正确性。定义Repository接口获取和存储聚合根的契约。Infrastructure (含MyBatis Mapper/ORM)实现UserRepository接口。在实现类中调用 MyBatis 的 Mapper 或 JPA 将内存中的领域对象状态同步回数据库。注意如果使用JPA这类ORM实体本身可能带有注解这是为了映射表结构此时JPA注解可以视为基础设施的一部分但应尽量保持领域对象干净可以用XML配置或代码生成的方式隔离。通过这样分层的领域层就变得独立且可测试了不再依赖数据库和框架让技术和业务分离独自演化。愿我都能在各自的领域里不断成长勇敢追求梦想同时也保持对世界的好奇与善意!