从代码轮椅化到清晰架构:基于DDD与单一职责原则的重构实战
最近在重构一个遗留系统时深刻体会到“牵一发而动全身”的困境。原有的代码结构耦合严重新增一个简单的查询功能却需要修改五六个看似不相关的模块测试回归工作量巨大整个团队的开发效率明显“轮椅化”——进展缓慢步履维艰。本文将以一个典型的业务模块“毕安卡井一”Bianca Well One 此处为化名代表一个核心数据服务的重构实战为例系统性地拆解如何将一团乱麻的代码重构成清晰、可维护、可扩展的架构。无论你是正在面对历史包袱的团队骨干还是希望提升代码设计能力的中高级开发者这套从问题诊断到方案落地的完整流程都能为你提供直接的参考和可复用的代码模板。1. 背景与核心概念什么是“代码轮椅化”在开始具体重构之前我们首先要明确问题。“事情开始变得轮椅起来了”是一种形象的比喻形容代码库在经历多次快速迭代、多人维护后逐渐变得难以理解和修改的状态。其典型特征包括高耦合度模块间依赖关系复杂修改A模块必然影响B、C、D模块。就像轮椅的轮子、扶手、座椅被焊死在一起想调整座椅高度却不得不动轮子。低内聚性单个模块或类的职责不单一混杂了多种业务逻辑。例如一个OrderService既处理订单创建又负责发送邮件、更新库存、计算积分。重复代码相同的业务逻辑散落在多个地方一旦业务规则变更需要多处修改极易遗漏。难以测试由于依赖复杂为单个方法编写单元测试需要搭建庞大的“脚手架”测试成本极高。可读性差命名随意结构混乱新成员需要很长时间才能理解代码意图。本次重构的目标模块“毕安卡井一”正是一个典型的“轮椅代码”。它原本是一个数据查询服务但随着业务发展逐渐掺入了数据校验、格式转换、缓存逻辑、甚至部分业务规则判断变成了一个超过2000行的“上帝类”God Class任何改动都风险极高。2. 环境准备与版本说明本次重构不依赖于特定框架的奇技淫巧核心是面向对象设计原则和重构手法的应用。为了演示的通用性我们使用Java语言并采用最常见的Spring Boot技术栈作为承载。你可以很容易地将思路迁移到C#、Go、Python等其他语言项目中。JDK版本 11 或以上推荐11/17 LTS版本构建工具 Maven 3.6 或 Gradle 7.x项目管理框架 Spring Boot 2.7.x 本文示例基于2.7.18单元测试框架 JUnit 5 MockitoIDE IntelliJ IDEA 或 Eclipse 具备重构功能即可示例项目结构bianca-well-refactor/ ├── src/main/java/com/example/bianca/ │ ├── domain/ # 领域模型 │ ├── application/ # 应用服务用例/事务脚本 │ ├── infrastructure/ # 基础设施持久化、外部API等 │ └── interfaces/ # 接口层Controller, DTO等 ├── src/test/java/ # 测试代码 └── pom.xml重要提示 版本号请根据你的实际项目调整。本文重点在于展示重构的思路、步骤和代码形态而非绑定特定版本。3. 核心重构原则与模式拆解在动手之前我们需要武装一些理论武器。以下是本次重构中会高频使用的原则和模式3.1 单一职责原则SRP一个类或模块应该有且仅有一个引起它变化的原因。这是破解“上帝类”最核心的原则。重构时要不断问自己“这个类当前承担的责任是什么能否再拆分”3.2 依赖倒置原则DIP高层模块不应依赖低层模块二者都应依赖其抽象。这意味着我们应该依赖接口Interface而非具体实现Concrete Class。这是实现模块间解耦的关键。3.3 领域驱动设计DDD分层概念虽然不是完全实施DDD但其分层思想极具指导意义领域层 封装核心业务逻辑和规则是系统的“心脏”应保持纯净不依赖任何外部框架。应用层 协调领域对象完成一个特定的用例用户故事负责事务、权限校验等。基础设施层 实现领域层定义的接口提供技术细节如数据库访问、消息发送。接口层 处理外部请求HTTP API, RPC等完成数据的输入输出转换。3.4 重构手法提取类、提取方法、引入参数对象这些是IDE支持的基础重构操作是消除重复、简化代码结构的利器。4. 完整实战案例重构“毕安卡井一”数据服务假设原BiancaWellService类主要职责是根据复杂条件查询油井数据并返回特定格式的报告。问题代码片段示例如下// 重构前的庞杂服务类 (问题代码示例) Service public class BiancaWellService { Autowired private WellDataDao wellDataDao; Autowired private RedisTemplateString, String redisTemplate; Autowired private EmailSender emailSender; public ReportDTO generateWellReport(ReportRequest request) { // 1. 参数校验 (混杂了业务规则) if (request.getStartDate() null || request.getEndDate() null) { throw new IllegalArgumentException(日期不能为空); } if (request.getWellId() 0) { throw new IllegalArgumentException(井ID无效); } // ... 更多琐碎的校验 // 2. 尝试从缓存获取 String cacheKey well_report: request.getWellId() : request.getStartDate(); String cachedReport redisTemplate.opsForValue().get(cacheKey); if (cachedReport ! null) { return JSON.parseObject(cachedReport, ReportDTO.class); // 直接返回缓存逻辑与业务耦合 } // 3. 构建复杂查询条件 (SQL逻辑泄露到服务层) MapString, Object queryParams new HashMap(); queryParams.put(wellId, request.getWellId()); queryParams.put(startDate, request.getStartDate()); queryParams.put(endDate, request.getEndDate()); if (HIGH.equals(request.getPressureLevel())) { queryParams.put(minPressure, 100.0); } // ... 大量条件组装 // 4. 执行查询 ListWellData rawDataList wellDataDao.findByComplexConditions(queryParams); // 5. 数据加工与计算 (核心业务逻辑淹没在细节中) ReportDTO report new ReportDTO(); double totalOutput 0; for (WellData data : rawDataList) { // 计算逻辑... totalOutput data.getDailyOutput(); // 某些特殊井有特殊计算规则 (if-else 硬编码) if (data.getWellType().equals(SPECIAL)) { totalOutput * 1.05; } } report.setTotalOutput(totalOutput); // ... 更多字段计算和设置 // 6. 格式转换和补充信息 report.setFormattedDate(LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)); // ... // 7. 写入缓存 redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(report), 1, TimeUnit.HOURS); // 8. 如果产量异常发送警报邮件 (另一个职责) if (totalOutput 10000) { emailSender.sendAlert(井产量异常告警, 井 request.getWellId() 产量过高); } return report; } }这个方法的长度和承担的责任显然已经失控。接下来我们分步骤进行重构。4.1 第一步代码分析与职责拆分首先通过注释梳理出generateWellReport方法中的所有职责输入校验缓存管理查询条件组装数据获取核心业务计算报告格式组装缓存写入告警通知我们的目标是将这些职责分配到不同的类中。4.2 第二步创建领域模型与值对象首先将核心的业务概念抽象出来。创建WellData的聚合根和相关的值对象。// 领域层值对象封装查询条件 package com.example.bianca.domain.vo; import lombok.Value; import java.time.LocalDate; Value // 使用Lombok生成不可变对象 public class WellQueryCondition { Long wellId; LocalDate startDate; LocalDate endDate; PressureLevel pressureLevel; // 枚举 // ... 其他条件 // 可以在此处封装一些简单的自校验逻辑 public void validate() { if (wellId null || wellId 0) { throw new IllegalArgumentException(无效的井ID); } if (startDate null || endDate null || startDate.isAfter(endDate)) { throw new IllegalArgumentException(日期范围无效); } } }// 领域层领域服务接口定义核心业务能力 package com.example.bianca.domain.service; import com.example.bianca.domain.vo.WellQueryCondition; import com.example.bianca.domain.model.WellReport; public interface WellReportService { /** * 生成油井报告 - 纯粹的领域业务逻辑 */ WellReport generateReport(WellQueryCondition condition); }4.3 第三步实现领域服务与分离基础设施创建领域服务的实现它只关心业务计算不关心数据从哪里来依赖倒置。// 领域层领域服务实现 package com.example.bianca.domain.service.impl; import com.example.bianca.domain.model.WellData; import com.example.bianca.domain.model.WellReport; import com.example.bianca.domain.service.WellReportService; import com.example.bianca.domain.vo.WellQueryCondition; import com.example.bianca.domain.repository.WellDataRepository; // 依赖抽象接口 import org.springframework.stereotype.Service; import java.util.List; Service public class WellReportServiceImpl implements WellReportService { private final WellDataRepository wellDataRepository; // 通过构造函数注入 public WellReportServiceImpl(WellDataRepository wellDataRepository) { this.wellDataRepository wellDataRepository; } Override public WellReport generateReport(WellQueryCondition condition) { // 1. 参数校验 (可移至应用层或值对象自身) condition.validate(); // 2. 通过接口获取数据不关心具体是MyBatis还是JPA ListWellData wellDataList wellDataRepository.findByCondition(condition); // 3. 纯粹的核心业务计算 double totalOutput calculateTotalOutput(wellDataList); double avgPressure calculateAveragePressure(wellDataList); // ... 其他计算 // 4. 组装领域对象并返回 return WellReport.builder() .wellId(condition.getWellId()) .totalOutput(totalOutput) .averagePressure(avgPressure) .dataList(wellDataList) .generatedTime(java.time.LocalDateTime.now()) .build(); } // 私有方法封装具体计算规则 private double calculateTotalOutput(ListWellData dataList) { return dataList.stream() .mapToDouble(data - { double output data.getDailyOutput(); // 特殊井类型的业务规则内聚在此处 if (data.getWellType().equals(WellType.SPECIAL)) { output * 1.05; } return output; }) .sum(); } private double calculateAveragePressure(ListWellData dataList) { ... } }关键点 现在的WellReportServiceImpl只专注于“计算报告”这一件事。它依赖一个抽象的WellDataRepository来获取数据。4.4 第四步实现基础设施层基础设施层负责实现领域层定义的接口处理技术细节。// 基础设施层仓储接口实现 (使用MyBatis为例) package com.example.bianca.infrastructure.persistence.mybatis; import com.example.bianca.domain.model.WellData; import com.example.bianca.domain.repository.WellDataRepository; import com.example.bianca.domain.vo.WellQueryCondition; import com.example.bianca.infrastructure.persistence.mapper.WellDataMapper; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import java.util.List; Repository RequiredArgsConstructor public class WellDataRepositoryImpl implements WellDataRepository { private final WellDataMapper wellDataMapper; // MyBatis Mapper Override public ListWellData findByCondition(WellQueryCondition condition) { // 在这里将领域层的查询条件对象转换为基础设施层如MyBatis所需的参数 // 例如可以创建一个专门的Query对象或直接使用Map return wellDataMapper.selectByCondition(condition); } }// MyBatis Mapper接口 package com.example.bianca.infrastructure.persistence.mapper; import com.example.bianca.domain.vo.WellQueryCondition; import com.example.bianca.infrastructure.persistence.po.WellDataPO; // 持久化对象 import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.List; Mapper public interface WellDataMapper { ListWellDataPO selectByCondition(Param(condition) WellQueryCondition condition); }4.5 第五步创建应用服务协调横切关注点应用服务或用例负责协调领域服务、处理缓存、发送通知等横切关注点并管理事务边界。// 应用层应用服务 package com.example.bianca.application; import com.example.bianca.domain.service.WellReportService; import com.example.bianca.domain.vo.WellQueryCondition; import com.example.bianca.interfaces.dto.ReportDTO; import com.example.bianca.interfaces.dto.ReportRequest; import com.example.bianca.infrastructure.cache.ReportCacheService; import com.example.bianca.infrastructure.notification.AlertService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; Service Slf4j RequiredArgsConstructor public class WellReportApplicationService { private final WellReportService wellReportService; // 领域服务 private final ReportCacheService reportCacheService; // 缓存基础设施 private final AlertService alertService; // 通知基础设施 private final ReportAssembler reportAssembler; // DTO组装器 Transactional(readOnly true) public ReportDTO generateWellReport(ReportRequest request) { // 1. 参数转换与校验 (可借助Validation注解) WellQueryCondition condition reportAssembler.toQueryCondition(request); // 2. 缓存读取 (基础设施职责) ReportDTO cachedReport reportCacheService.getFromCache(condition); if (cachedReport ! null) { log.info(缓存命中 for condition: {}, condition); return cachedReport; } // 3. 调用领域服务获取纯粹的领域模型 var domainReport wellReportService.generateReport(condition); // 4. 将领域模型转换为对外输出的DTO ReportDTO reportDTO reportAssembler.toDTO(domainReport); // 5. 写入缓存 (基础设施职责) reportCacheService.putIntoCache(condition, reportDTO); // 6. 根据业务规则触发告警 (另一个应用逻辑) if (domainReport.requiresAlert()) { alertService.sendWellOutputAlert(domainReport); } return reportDTO; } }4.6 第六步组装与运行最后我们需要一个入口如Controller来接收请求调用应用服务。// 接口层Controller package com.example.bianca.interfaces.web; import com.example.bianca.application.WellReportApplicationService; import com.example.bianca.interfaces.dto.ReportDTO; import com.example.bianca.interfaces.dto.ReportRequest; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; RestController RequestMapping(/api/wells) RequiredArgsConstructor public class WellReportController { private final WellReportApplicationService reportAppService; PostMapping(/report) public ReportDTO generateReport(Valid RequestBody ReportRequest request) { // Controller保持轻薄只负责参数校验和协议转换 return reportAppService.generateWellReport(request); } }至此我们完成了从“轮椅代码”到清晰分层架构的重构。新的结构如下图所示概念图HTTP Request | v [Controller] (接口层) | (调用) v [Application Service] (应用层) —— [Cache Service] | (调用) —— [Alert Service] v [Domain Service] (领域层) | (依赖抽象) v [Repository Interface] | v [Repository Impl] (基础设施层) —— [Database]5. 常见问题与排查思路在重构过程中你可能会遇到以下典型问题问题现象常见原因解决思路编译错误找不到Bean新的分层后组件扫描路径未覆盖。检查SpringBootApplication主类所在的包位置确保其是所有子包的父包。或使用ComponentScan显式指定包路径。空指针异常NPE依赖注入失败或领域对象在转换过程中为null。1. 检查各层组件的Service,Repository注解。2. 在DTO组装器Assembler中增加空值判断。3. 使用 Optional 进行安全访问。事务不生效事务注解Transactional加在了接口、私有方法或未被Spring代理的方法上。确保Transactional加在应用服务Application Service的public方法上并且该类是由Spring容器管理的Bean。缓存与数据库数据不一致应用服务中先写缓存再更新数据库失败或缓存更新策略有误。1. 考虑“先更新数据库再删除缓存”的策略。2. 对于复杂场景引入分布式锁或使用数据库事务消息确保最终一致性。3. 评估缓存是否必要或设置较短的过期时间。单元测试难以编写重构后类之间的依赖通过接口连接更容易进行Mock。使用Mockito等框架在测试应用服务时Mock掉ReportCacheService和AlertService在测试领域服务时Mock掉WellDataRepository。测试变得聚焦且简单。感觉更复杂了类变多了这是重构初期的正常感受。衡量标准不是类的数量而是修改一个功能时需要动的文件数和理解代码的认知负荷。新架构下修改计算规则只需改领域服务修改缓存策略只需改缓存服务关注点分离长期维护成本大幅降低。6. 最佳实践与工程建议小步快跑安全重构不要试图一次性重构整个巨型类。使用IDE的重构工具如“提取方法”、“提取类”、“内联”每次只做一小步修改并立即运行现有测试确保没有破坏原有功能。测试护航在重构开始前确保为待重构的类和方法编写了足够的单元测试和集成测试。这些测试是你的“安全网”。如果原代码没有测试先尝试为最核心、最稳定的部分补写测试。依赖注入面向接口坚决使用构造函数注入并依赖于接口。这不仅能解耦还能让单元测试中的Mock变得异常简单。领域层保持纯净领域模型和领域服务中不要出现任何Spring注解如Autowired、JPA注解如Entity或JSON序列化注解如JsonProperty。它们只应包含业务逻辑和数据。技术细节通过适配器模式在基础设施层实现。使用DTO进行层间通信避免将领域对象直接暴露给接口层。通过Assembler或Mapper在领域对象与DTO之间进行转换这保护了领域模型的完整性也避免了API变动导致领域模型被迫修改。日志与监控在应用层和基础设施层的关键节点添加日志记录特别是缓存命中/未命中、外部调用、耗时操作等。这为后续性能优化和问题排查提供依据。代码审查将重构后的代码提交给团队进行审查。这不仅是为了发现潜在问题也是一个极好的知识共享和统一团队设计思想的机会。重构不是一次性的活动而应成为开发中的持续实践。每当发现代码有“轮椅化”的苗头例如一个方法超过50行一个类职责开始模糊就应及时运用这些原则进行微重构避免技术债务的累积。最终你会得到一个像精密仪器一样各司其职、易于组合和调试的系统开发体验将从“推轮椅”变为“开跑车”。