第二阶段:Clean Arc重构

0 阅读16分钟

平台架构分析与重构指南

一、 现在存在的问题

1. Service 层过于臃肿

  • (1) 代码臃肿(Fat Service):所有的业务逻辑,如振动特征值计算、设备状态判定、告警等级推算、权限校验等等,全部塞在 Service 里。随着功能增加,后续单个 Service 文件体积继续扩大。
  • (2) 逻辑散落:原本属于设备本身的逻辑,却散落在 DeviceServiceDataService 等多个地方。
  • (3) 难以维护:修改一个小的业务规则,由于 Service 内部复杂的调用关系,往往会引起意想不到的连锁反应,导致维护困难。

2. 业务表达力缺失

  • (1) 数据对象只是死的数据容器:在现有架构中,Device 对象通常只有 get/set 方法,没有任何业务行为。
  • (2) 业务含义丢失:开发者在看代码时,看到的只是对数据库表的增删改查,而看不到真正的业务意图。

3. 技术实现与业务逻辑深度耦合

  • (1) 数据库驱动设计:现有开发流程通常是先设计表,再写代码。这导致业务逻辑被数据库结构绑架。
  • (2) 难以适配新技术:振动数据从 MySQL 扩展到时序数据库时,往往难以适应。在现有架构下,由于 Service 层直接调用了特定的 Mapper,想要更换底层存储或引入缓存,需要大规模重写业务代码,因为业务逻辑和持久化逻辑粘在一起了。

4. 应对复杂性的能力极弱

  • (1) 设备差异化处理困难:不同类型的设备的振动分析逻辑、采样频率、告警阈值计算方法完全不同。
  • (2) 硬编码风险:为了区分这些差异,Service 层会出现大量的 if-elseswitch-case。当接入多种设备时,代码将变得不可阅读且极易出错。
  • (3) 扩展性差:如果后续想引入一套新的预测性维护算法,由于现有系统缺乏清晰的边界,新算法很难平滑地插进现有流程中。

5. 数据一致性挑战

  • (1) 大事务问题:在 Service 层中,为了保证一致性,往往会开启一个大事务,涉及设备状态更新、告警记录生成、推送通知等多个操作。
  • (2) 性能瓶颈:在工业高频场景下,大事务会导致数据库锁竞争严重,系统并发能力受限,甚至因为一个小模块的延迟导致整个采集链路阻塞。

二、 架构更新带来的收益

1. 实现业务逻辑与技术实现的解耦

在传统架构中,业务代码往往被 SQL 或中间件 API 淹没。DDD 通过依赖倒置原则,将技术细节推向外层。

  • 优点:可以先编写纯净的业务算法,而不必关心数据具体实现。

2. 建立统一语言,消除沟通鸿沟

DDD 强调程序开发人员与非编码人员的专业名词统一化。

  • 优点:代码不再是冷冰冰的 updateStatus,而是 analyzeVibration()triggerAlarm()decommissionDevice()。极大的降低了业务理解的门槛,减少了需求传递过程中的偏差。

3. 高度内聚的聚合根保障数据一致性

在物联网系统中,设备状态、配置和传感器采样频率之间存在复杂的约束。

  • 优点:通过聚合根,所有对设备状态的修改都必须经过设备实体这个入口。确保了业务规则的强制执行。例如,只有在设备运行状态下才能调整采样频率,这一规则被锁死在聚合内部,避免了 Service 层由于漏写判断逻辑而导致的数据非法。

4. 轻松应对设备多样性的扩展

工业现场设备千差万别,风机、泵、压缩机的分析维度各不相同。

  • 优点:利用 DDD 的策略模式与限界上下文,您可以为不同类型的设备定义不同的领域服务。

5. 天然支持读写分离与高性能扩展

振动平台面临高频的波形数据采集上传与复杂看板分析的冲突。

  • 优点:DDD 与 CQRS(命令查询职责分离) 模式高度契合。命令端只负责处理业务规则和状态变更,保证绝对安全。查询端针对前端的大屏看板、多维报表,可以绕过复杂的领域模型,直接通过轻量级的 DTO 甚至宽表进行检索,极大提升了系统的响应速度。

三、 架构介绍

传统的三层架构

A. 架构层次:
  1. 表示层 / 用户接口层 (Presentation Layer)
    • 职责:负责与前端进行交互。接收用户的请求输入,进行简单的参数校验,然后调用下一层进行处理,最后将处理结果封装并格式化返回给前端。
    • 常见组件: 控制器 (Controllers)。
  2. 业务逻辑层 (Service Layer)
    • 职责:负责处理具体的业务规则、计算逻辑、流程编排以及事务管理。它承上启下,接收表示层传来的指令,并在需要读写数据时调用数据访问层。
    • 常见组件: 服务类 (Service Classes)。
  3. 数据访问层 (Data Access Layer / DAL / Repository Layer)
    • 职责: 专门负责与底层数据库或存储系统进行交互。封装了所有的 SQL 语句或数据库操作,向业务逻辑层提供简单的增删改查接口。
    • 常见组件:DAO (Data Access Object)、Mapper、ORM 实体。
B. 依赖关系:

依赖是严格单向且自顶向下的: Web 层直接依赖 Service 层,Service 层直接依赖 DAO 层。

image.png

Figure 1 传统三层架构依赖关系

C. 所存在的问题:

1. 数据库驱动

  • (1) 设计本末倒置:开发新功能时,开发者的第一反应往往是“如何建表?”然后再根据表结构生成实体类,最后在 Service 层写业务逻辑。这种思维是数据驱动而非业务驱动。
  • (2) 贫血模型:实体类退化成了只有 gettersetter 的数据载体,没有任何业务行为。
  • (3) 面条式代码:所有的业务规则、状态流转、计算逻辑全部堆积在 Service 层的方法里。随着业务发展,Service 类会迅速膨胀,代码量激增,极难维护。

2. 业务逻辑与底层技术强耦合

  • (1) 违背依赖倒置Service 层直接依赖了 DAO 层。
  • (2) 业务逻辑与技术强耦合: 如果想要更换数据库或者更换 ORM 框架,由于 Service 层深层绑定了 DAO 层的具体实现或数据结构,核心业务代码往往也需要大面积重构。

3. 业务逻辑泄露与碎片化 由于缺乏严格的业务边界,业务逻辑常常散落在系统的各个角落:

  • (1) 向上泄露到 Controller:开发者可能会在 Controller 层直接写一些复杂的参数校验、权限判断甚至核心业务分发逻辑。
  • (2) 向下沉没到 DAO:开发者经常把复杂的业务规则写成几百行的超长 SQL 语句。这导致数据库承担了业务计算的职责,一旦业务规则变化,调试和修改 SQL 极其痛苦。

这是一份继续为您整理并排版好的 Markdown 格式文档,对领域驱动设计(DDD)的架构原理以及两个核心模块的重构实践进行了清晰的层级划分、加粗和代码高亮,保持了与上文一致的阅读体验:


四、 领域驱动设计 (DDD) 架构解析

A. DDD 四层架构

  1. 用户接口层 (Presentation Layer):负责向前端展示信息和解释用户指令。
  2. 应用层 (Application Layer):很薄的一层,不包含业务逻辑。它主要负责编排领域对象、处理事务、安全认证和发送领域事件。
  3. 领域层 (Domain Layer):系统的核心。包含所有的业务逻辑、领域模型、领域服务和仓储接口。它完全不依赖于具体的底层技术。
  4. 基础设施层 (Infrastructure Layer):提供技术支撑,包括数据库持久化、消息队列连接、第三方 API 调用等。

B. 依赖关系

为了保护领域层,现代的 DDD 四层架构应用了依赖倒置原则。在这个架构中,Domain 处于最中心位置,它不依赖于任何其他层。所有其他层的依赖箭头最终都指向领域层。

  • (1) 领域层 (Domain Layer) —— 零依赖
    • 依赖项:无。绝不引入任何基础设施层的代码或框架。
    • 职责:它只包含纯粹的面向对象代码,以及接口定义。
  • (2) 基础设施层 (Infrastructure Layer) —— 向内依赖 Domain 层
    • 依赖项:Domain 层(有时也依赖 Application 层)。
    • 职责:基础设施层负责实现领域层定义的接口。
  • (3) 应用层 (Application Layer) —— 向内依赖 Domain 层
    • 依赖项:Domain 层。
    • 职责:应用层通过调用领域层中的领域对象和领域服务来编排业务用例。它同样是通过接口与基础设施层交互。
  • (4) 用户接口层 (User Interface Layer) —— 向内依赖 Application 层
    • 依赖项:Application 层(通常不直接跨层依赖 Domain 层)。
    • 职责:接收用户请求,将其转换为应用层可以理解的指令,然后调用应用层的服务。

image.png

Figure 2 领域驱动依赖关系

C. 相比较与传统架构的优势

1. 业务与代码的高度一致性

  • 通用语言:在传统开发中,业务人员说“业务话”,开发人员写“技术代码”,中间存在巨大的翻译损耗。DDD 强制要求团队建立一套通用语言。
  • 代码即文档:领域模型中的类名、方法名直接反映了业务动作。这使得新加入的开发人员可以通过阅读核心领域层的代码,直接理解业务流程,降低了维护成本。

2. 天然契合微服务架构

  • 限界上下文:传统架构容易演变成“大单体”和“大泥球”。DDD 在逻辑上划分出不同的限界上下文。
  • 微服务拆分指南:一个限界上下文通常就是一个天然的微服务边界。为微服务的拆分提供了强有力的业务理论支撑,避免了“为了拆分而拆分”导致的服务间耦合灾难。

3. 告别贫血模型,实现代码高内聚

  • 充血模型:针对传统架构中 Service 层臃肿、Entity 只是数据壳子的问题,DDD 将业务规则、状态变更逻辑全部内聚到实体和聚合根中。
  • 保证业务规则的完整性:外部代码不能随意修改对象的状态,只能通过聚合根暴露的特定业务方法来进行。减少了脏数据的产生,保证了业务规则的严密性。

4. 核心业务逻辑与技术细节的解耦

  • 依赖倒置与纯粹的领域层:在 DDD 中,领域层不依赖任何外部框架、数据库或中间件。它完全由纯净的面向对象代码组成。
  • 更适应技术演进:如果未来需要把 MySQL 换成 MongoDB,或者把单体拆成微服务引入 Kafka,只需要修改最外层的基础设施层。核心的业务逻辑(领域层)一行代码都不用改。这极大地延长了软件的生命周期。

五、 架构重构落地实践

重构目标:模块按 DDD 分层拆解,引入领域对象、领域服务、应用服务、基础设施适配四层,使业务规则内聚于领域层,并保持外部接口不变。

一、 登录模块重构

A. 分层结构
  • (1) 接口层/controller/LoginController.java
    • 核心组件LoginController
    • 职责定位:负责接收前端 HTTP 请求、基础的参数校验、路由分发以及返回统一的响应体。
    • 设计特征:该层变薄。不再包含任何业务逻辑,仅负责将前端传入的 UserLoginDTO 转发给下游,并将下游返回的 UserLoginVO 包装后返回给前端。
  • (2) 应用层/application/UserApplicationService.java
    • 核心组件UserApplicationService
    • 职责定位:负责业务用例的编排与协调。它本身不包含核心业务规则,只负责串联各个领域组件和基础设施。
    • 设计特征:在登录流程中负责调用领域服务进行业务校验,校验通过后组装并签发 JWT 令牌,最后构建返回 UserLoginVO;在注册流程中,负责调用领域实体的工厂方法创建新用户,并协调仓储进行持久化。
  • (3) 领域层/domain/user/User.java, /domain/user/service/UserDomainService.java, /domain/user/UserRepository.java
    • 聚合根 (User):摒弃贫血模型,转变为充血模型,封装了与用户身份相关的核心业务行为。
    • 领域服务 (UserDomainService):负责处理跨实体或不适合放入单一实体的业务逻辑;封装了完整的登录校验规则。校验失败时抛出明确的领域业务异常 LoginFailedExceptionUserDisabledException
    • 领域仓储接口 (UserRepository):利用依赖倒置原则,仅定义数据访问接口,不关心具体实现。
  • (4) 基础设施层/infrastructure/persistence/UserRepositoryImpl.java, UserDOMapper.java, UserConverter.java
    • 核心组件UserRepositoryImplUserDOMapperUserDOUserConverter
    • 职责定位:为领域层提供技术实现支持。
    • 设计特征:引入 MyBatis-Plus 完成物理表的 CRUD。引入 UserConverter 严格区分领域对象 User 与数据库对象 UserDO,确保数据库表结构变更不会污染领域层。

image.png

Figure 3 登录模块重构后的结构目录

B. 重构步骤
  1. 新建领域层:在原模块下创建 domain/user/ 包,包含 User 聚合根(封装 verifyPasswordisEnabled 方法)、UserStatus 值对象枚举、UserRepository 仓储接口。
  2. 新建领域服务:在 domain/user/service/ 下创建 UserDomainService,封装登录校验规则。
  3. 新建应用服务层:在 application/ 下创建 UserApplicationService。登录负责调领域服务和发 Token;注册负责参数组装和调仓储。
  4. 新建基础设施层:在 infrastructure/ 下创建 UserRepositoryImpl,内部依赖 UserMapper,并使用 UserConverter 负责实体互转。
  5. 重构 ControllerLoginController 改为只依赖 UserApplicationService。原 UserService / UserServiceImpl 保留用于用户管理功能,后续统一迁移。
  6. 新建 VO 与异常:新增 UserLoginVOLoginFailedExceptionUserDisabledException,配合全局异常处理器。
C. 逻辑实现
  • (1) 登录流程 (POST /login)
    • 入口:LoginController.login() -> UserApplicationService.login()
    • 应用层委托领域层:UserDomainService.login()
      1. 按账号查用户(不存在抛 LoginFailedException
      2. 验密码 user.verifyPassword(...)
      3. 验状态 user.isEnabled()
    • 校验通过后,应用层生成 JWT,返回 UserLoginVO
  • (2) 注册流程 (POST /login/register)
    • 应用层调用聚合根工厂方法创建用户:User.create(...)
    • 在领域对象内部完成密码 MD5、状态初始化、时间初始化。
    • 通过仓储保存:UserRepository.save(...) -> UserRepositoryImpl.save(...)
    • 拦截重复键异常(Duplicate entry)并返回友好提示。
  • (3) JWT 鉴权接入
    • 拦截器从请求头取 JWT,解析成功后把账号状态放入 ThreadLocal (BaseContext)。
    • 全局拦截,放行 /login、文档、静态资源等。
D. 当前实现特点
  1. 登录业务规则已经从传统 Service 下沉到领域模型,职责更清晰。
  2. 接口层不再直接处理密码/状态规则,异常语义化。
  3. 持久化对象 UserDO 与领域对象 User 通过 converter 解耦,避免数据库结构污染领域。
  4. 项目里仍保留旧风格的 UserService,处于新旧并存的渐进重构阶段。

二、 设备管理模块重构

A. 分层结构
  • (1) 接口层system/controller/DeviceController.java
    • 设计特征:极度变薄。不包含事务控制和默认值处理,仅负责参数转发,并将结果包装为 DevicesResultVO 返回。
  • (2) 应用层system/application/DeviceApplicationService.java
    • 设计特征:负责设备用例编排(查询、新增、修改、删除及图片上传)。调用聚合根生成对象并协调仓储持久化;在查询时负责归一化查询参数 normalizeQuery
  • (3) 领域层system/domain/device/Device.java, DeviceRepository.java
    • 聚合根 (Device):充血模型,封装状态合法性校验 normalizeStatus,以及设备创建 create 和更新 markUpdated 的时间戳维护规则。
    • 领域服务:暂无(业务校验完全内聚于聚合根内部)。
    • 领域仓储接口 (DeviceRepository):定义数据访问契约 findAllpageQuerysave
  • (4) 基础设施层system/infrastructure/persistence/DeviceRepositoryImpl.java, DeviceConverter.java
    • 设计特征:引入 MyBatis-Plus(DeviceMapper)。通过 DeviceConverter 区分聚合根 Device 与持久化实体 DeviceEntity,保障领域层纯洁性。

image.png

Figure 4 设备模块重构后的结构目录

B. 重构步骤
  1. 新建领域层:创建 domain/device/ 包,包含 Device 聚合根、DeviceStatus 枚举、DeviceRepository 接口、分页查询对象 DeviceQueryDevicePage
  2. 新建应用服务层:创建 DeviceApplicationService,对接表现层,编排条件查询、设备保存、本地与云端 OSS 图片上传。
  3. 新建基础设施层:创建 DeviceRepositoryImpl 实现仓储接口,并增加 DeviceConverter 处理实体转换。
  4. 新建异常机制:新增 DeviceValidationExceptionDeviceOperationException,并配套全局异常捕获器 DeviceExceptionHandler
  5. 重构 Controller:全面删除对原有贫血模型 DeviceService 的调用。原 DeviceServiceImpl 增加 @Deprecated 注解标记废弃,平滑过渡。
C. 逻辑实现
  • (1) 分页查询流程 (POST /devices/PageConditional)
    • 应用层通过 normalizeQuery() 处理空值默认化,生成领域查询对象 DeviceQuery
    • 委托仓储层 DeviceRepository.pageQuery(query),底层使用 PageHelper 结合 Mapper 执行物理分页。
    • 将结果转换为 DevicesPageResultDTO,在 Controller 组装返回。
  • (2) 新增与修改流程 (POST /devices, PUT /devices)
    • 新增:应用层调聚合根工厂 Device.create(...),内部完成状态校验防腐与时间初始化。
    • 修改:应用层通过 Device.reconstitute(...) 恢复对象,调用 markUpdated() 更新时间戳。
    • 最终均通过仓储 save(...) 存入数据库。
  • (3) 异常处理流程
    • 底层校验失败(如非法状态、图片上传失败)直接抛出领域异常。
    • DeviceExceptionHandler 全局接管,转为 Result.error() 输出,Controller 彻底告别 try-catch
D. 当前实现特点
  1. 业务规则内聚:状态校验收敛到 Device 聚合根内部,解决数据被随意篡改的隐患。
  2. 强大的防腐隔离:利用 DeviceConverter 彻底斩断了数据实体对核心业务层的侵入。
  3. 异常语义化与表现层减负:依靠自定义异常与 RestControllerAdvice 结合,剥离了 Controller 层错综复杂的逻辑判断。
  4. 安全过渡架构:旧版 Service 使用 @Deprecated 标识,接口向下兼容,保障了稳健的工程化改造。