整洁架构实践:契约层解耦与分层职责详解

224 阅读11分钟

工程结构图

image.png

说明:

  1. 从应用层中拆分出一个 contract (契约)层,用于解耦 infra (基础设施)层与 Application(应用) 层
  2. Application(应用)层只依赖 Domain(领域)层+contract(契约)层
  3. infra(基础设施)层只依赖 Domain(领域)层+contract(契约)层
  4. contract(契约)层是一组纯接口+数据结构+基础设施层对外暴露接口(从动)

各层级职责划分

层级职责
接口层(Interface)负责接收和处理外部请求
应用层(Application)负责业务流程的编排,事务控制、消费领域业务事件,外部服务的调用(如 MQ、RPC等)
契约层(contract)解耦应用层与基础设置层之间的依赖;从应用层拆解出来的,暴露外部接口和定义查询接口(纯查询)
领域层(domain)业务建模(聚合根/实体、值对象)、封装业务规则、领域服务、领域工厂、所需的仓储接口、不依赖外部技术(如数据库、MQ、RPC 等)
基础设施层(infrastructure)技术实现,如数据库访问、缓存、MQ、远程调用等
公共包(common)基础工具包,与业务逻辑毫无关系的通用类(如工具类、异常定义、常量)
api 包(api)接口定义、参数类、接口文档等;无实现

数据传递图

image.png

数据结构模型定义及及说明

层级名称用途说明命名示例
接口层Request接口入参用于接收前端参数LoginRequest
Response接口返回返回给前端的结构体LoginResponse
应用层DTO数据传输对象结构灵活,适合跨层;不包含业务逻辑;主要作为返回对象使用UserDTO
Command动作入参表示一种意图或动作CreateUserCommand
Query查询入参用于封装查询条件UserListQuery
Message消息体MQ 消息体(主要是生产,消费的可以在接口那层定义)OrderPayMessage
领域层Entity聚合根/领域实体有唯一标识,有生命周期(有状态、流程和规则限制的);无生命周期可简单建模(如字典)或不建模,通过事件在基础设施层监听消费(如日志)User,Order
Param领域层参数用于领域工厂构造复杂实体和业务规则校验
ID标识值对象用于标识实体,强类型替代 LongUserId、Email
Event领域事件说明“发生了某件事”UserLockedEvent
基础设施层PO数据库映射对象一般与表结构 1:1映射UserPO
ConditionDB 查询条件MyBatis 查询参数容器UserQueryCondition
BO多表联合查询的结果应用层的中间业务数据,为组装 DTO 对象提供数据支撑OrderDetailBO
API 包/基础设施层 rpc包ArgsRPC 请求参数对象API 包是为服务端的接口,基础设施层是客服端,无论服务端还是客户端都使用一样的后缀RoleArgs
ResultRPC 响应参数对象RoleResult

各层级编码规范

接口层

  1. 定义接口时,每个接口的 Request 对象应独立存在;除通用返回的 Response 外,每个接口的 Response 也应独立定义。

  2. Request 和 Response 不宜直接向下层传递,应做相应转换。

  3. Controller 仅负责简单的参数校验(通过注解实现)和请求转发,不处理业务逻辑。

  4. 返回数据应使用自定义的包装类封装,包含状态码(code)等信息,而非直接返回 Response、Object或者 ResponseEntity<Response/Object>。

  5. 当 Response 返回多层级数据时,采用内部类结构承载子数据,保证层次清晰。

  6. 统一接口日志拦截规范,确保请求和响应日志打印且脱敏:

    1. HandlerInterceptor:打印 URI、请求参数、traceId、耗时等。
    2. ResponseBodyAdvice:打印 JSON 响应体,支持脱敏和统一封装。
    3. ControllerAdvice:统一捕获并处理异常。

应用层

  1. 不处理核心业务逻辑(如状态机、复杂业务规则,例如订单超时自动取消等)。
  2. 对于读操作,调用查询和数据组装逻辑,不直接调用领域层,相关接口定义在契约层;应用层负责组装返回结果。
  3. 对于写操作,负责流程编排、事务管理及调用第三方服务,核心业务规则由领域服务实现,大部分业务代码写在领域层。
  4. 对于包含读写的行为操作,负责编排多步骤流程:调用外部服务、执行领域行为、保存数据。
  5. 对领域服务抛出的业务事件,在应用层进行监听和消费。

契约层

  1. 仅定义接口协议和数据结构,不包含任何业务逻辑,业务编排由应用层完成。
  2. 三方服务暴露出来的接口

领域层

  1. 领域层不负责所有查询,仅支持命令执行所需的必要查询。
  2. 实体按需加载,仅包含当前业务行为所必需的数据。
  3. 聚合间引用实体时,应仅引用对方实体的 ID,而非整个实体对象。
  4. 具有业务语义、不可拆分且需行为或校验的字段,应封装为值对象以提升复用性。
  5. 实体应定义行为,贯彻行为驱动设计。
  6. 领域层抛出的事件中,带有业务含义的事件由应用层监听消费;无业务含义的事件(如日志)由基础设施层消费。
  7. 简单实体可通过 Assembler 进行(Command → Entity)的映射转换;复杂建模或涉及业务规则时,应通过领域工厂(Factory)完成(Command → Param)的处理。
  8. 不直接调用第三方服务,所有外部访问通过端口(Port)实现。
  9. 限界上下文建设:当数据库单表承载多个业务领域状态与行为时,单一模型容易臃肿且职责模糊。为避免此类问题,应拆分为多个实体,分别归属不同聚合,实现职责清晰划分和领域隔离。(可选)
  10. 判断对象是否属于某个聚合根管理,关键在于对象生命周期是否依赖聚合根,且存在以聚合根为前提。

基础设施层

  1. 实现 Repository 和 QueryService 接口,负责数据查询,确保仅做简单数据判断,不承担数据组装任务。

  2. 提供技术服务实现,如数据库连接、消息队列(MQ)、RPC 调用、缓存等。

  3. 监听和消费领域层抛出的非业务性事件(如日志)

  4. SQL 日志管理:

    1. 打印 SQL 执行时间、traceId、接口路径、方法名、数据库名、表名等元数据。
    2. 生产环境不打印完整 SQL,开发和测试环境可打印完整 SQL 用于调试。

其他

  1. 锁的范围和生命周期应覆盖整个事务过程,可适当提前加锁或延后释放,避免并发冲突及数据不一致。
  2. 锁 Key 应基于业务唯一键设计,同一业务不同操作共享同一把锁,保障并发安全。
  3. 大多数情况下,锁加在接口层,事务管理放在应用层;批量处理场景可能需多把锁保证粒度和效率。
  4. 发送业务消息(如 MQ)应采用事件注册机制,确保消息仅在事务成功提交后发送,保障数据一致性。
  5. 各层保持清晰解耦,避免跨层调用,保证高内聚低耦合。
  6. 关键业务操作推荐设计幂等机制,防止重复执行导致数据异常。

从传统架构转整洁架构中疑问答疑

问题解决方案说明
依赖倒置后, PO 类不能暴露出去,应用层查询接口要查一些数据,但是没有接收类怎么办全部定义 DTO 来接收DDD/整洁架构中,PO 只属于基础设施层。不能跨层暴露给应用层或接口层。使用专门定义的 DTO 显式承载要展示的数据内容,是职责边界清晰、可维护、可扩展的设计方式。
我能不能直接调用领域层的查询接口,返回 Entity?不推荐;即使满足需求也不该直接用 EntityEntity 是领域内部的行为载体,不该作为传输载体暴露出去。否则违反“面向接口编程”和“依赖倒置原则”,会让外层代码对内部结构产生强耦合。
DTO 太多太杂,看着爆炸怎么办?组织结构、命名规范、复用结构、统一转换DTO 爆炸是“清晰职责划分”的自然产物。不是问题,只是组织问题。可以通过:1、场景命名:ListDTO / DetailDTO / ExportDTO 2、抽取通用字段:BaseXxxDTO 3、Assembler 封装统一转换实现结构清晰、职责分明。
DTO 应该复用吗?尽量不共用(保持语义清晰);可结构复用(字段)各个使用场景的 DTO 应该独立定义(即使字段一样),避免语义冲突和耦合问题。但可以通过字段提取成公共结构类 BaseDTO 来避免冗余。
DTO 怎么转换?封装进 Assembler 类比如 OrderDetailAssembler.toDTO(order)。Assembler 是解耦转换逻辑的重要手段,减少重复代码和脏耦合。
如果领域服务中需要调用外部服务(如发短信、 MQ RPC )怎么办?使用事件机制解耦领域层不允许直接依赖外部服务。业务事件:发布后在应用层消费(如“用户注册成功后发券”)技术事件:基础设施层消费(如日志、MQ)
如果领域服务内部要直接依赖远程 RPC 并处理返回逻辑呢?定义接口 CreditRpc 放在领域层,基础设施层实现它这种情况确实存在,尤其在微服务中。当RPC 的结果是领域判断所必需的信息时,应将其作为“领域所依赖的能力”建模成接口放在领域层(Port),由 infra 层提供具体实现(Adapter)。
事件机制会不会破坏事务的一致性?Spring 默认事件是同步执行、共享主事务使用 @EventListener,事件会立即同步执行、和主流程共享事务。能读到主事务待提交的数据,并统一回滚。本质就像“把流程拆成模块”,而不是异步。
如果我用 @TransactionalEventListener(phase = AFTER_COMMIT) 呢?会延迟执行,不能回滚主事务AFTER_COMMIT 意味着事务已经提交,监听器才触发。适合做“后置副作用”(如发 MQ、写日志、发邮件等)。但监听器内部失败,不会影响主流程,需手动监控、补偿、幂等处理。
如果我要让事件异步执行(如发 MQ ),怎么办?必须自己建立一致性机制一旦事件与主事务解耦,就必须处理一致性:① 落库保证可靠投递(Outbox 模式)② 消费幂等③ 消息失败补偿 / 监控告警④ 避免副作用影响主流程这会让你的事件机制复杂度上升很多。
事件发布和调用服务代码有啥区别?默认事件机制就像“正常流程代码拆分”使用 Spring 同步事件的效果就像是“把流程代码拆成多个模块”,职责更清晰,但仍然在一个事务中同步执行。只是逻辑分布更清楚,不影响执行顺序和原子性。

总结

本文系统地阐述了基于整洁架构的多层设计理念,重点围绕 CQRS、事件驱动和依赖倒置原则展开,明确了各层职责和边界,实现了业务逻辑与技术实现的高内聚低耦合。

通过拆分契约层,有效解耦了应用层与基础设施层的依赖,提升了系统的可维护性和扩展性。CQRS 模式的应用,使得读写职责清晰分离,进一步优化了系统的性能和复杂度管理。

事件驱动机制不仅保证了业务流程的松耦合,还增强了事务一致性与异步处理能力,为复杂业务场景提供了可靠的支持。依赖倒置原则的贯彻,确保了系统模块间的灵活替换和解耦,促进了高质量代码的持续演进。

综合来看,该架构设计既兼顾了理论先进性,又具备良好的实用性和工程落地能力,是打造健壮、可扩展企业级应用的有效范式。未来可在此基础上,结合具体业务特点,进一步深化异步一致性方案和领域事件设计,推动系统架构不断优化升级。