作者简介
Mega,携程国际机票工程师,关注Java、devops领域。
一、前言
大多数的技术研发都对重构有所了解,而每个研发又都有自己的理解。从代码重构到架构重构,我参与了携程大型全链路重构项目,积累了一点经验心得,在此抛砖引玉和大家分享。重构是指在不改变外部行为的情况下,改进其内部结构的软件系统更改过程。目前携程大部分业务场景都使用了微服务架构,要求服务应该封装单一的责任或单一的能力,以形成松散耦合的服务架构。根据著名的康威定律,保证一个团队可以独立工作、快速交付变更、尽可能消除团队之间协作和协调的费力度。所以当组织架构因为业务发展需要做相应调整时,决定了服务的架构也会需要相应的重构。重构遗留代码的目的,通常是使系统其更易于维护,减少维护系统所需的工作量,释放出研发资源处理更高价值的任务。即使是在项目成功落地之后,仍然需要持续的系统迭代,支持新的业务需求。迭代过程引入新的缺陷也是难以避免的。即使是最好的团队也可能会提交不成熟的代码,这往往会导致代码复杂性让系统变得难以维护,长期以往系统会越来越难以维护,影响研发效能。越来越长的开发周期,导致业务需求发布被推迟。
技术债是很多研发团队为了追求交互的速度,倾向于选择更简单但不太可靠的方案所付出的代价。短期内为了更快地交付做出的妥协都会在未来带来更多的工作量。遗留代码可能无法利用现代 DevOps 集成来加速 CI/CD。遗留代码需要改造成无状态服务,这样可以适配云上的弹性伸缩,优化成本。从重构的影响面可以分为最高层次的架构重构和最低层次的代码重构。代码重构是在修改代码后不会改变对外的行为,主要目的是提高整个软件的质量。一个重构通常是一个小任务,但是多个重构应用于代码可以显著提高其质量。架构重构主要是将现有代码重新组织成新的层级,改变代码在逻辑层次中的组织方式。代码重构通常在一个开发团队内部达成一致,影响的范围容易控制和验证,风险较低。架构重构必须要多个团队甚至是技术委员会达成一致,影响范围大,风险和成本较高。通常在做架构重构前尝试用可量化的指标来确定投入产出比,决策是否值得重构。此外架构重构相比代码重构难度更高,对研发的专业知识要求也会更高,项目组里至少需要有一个熟悉链路上所有服务的资深研发,负责重构决策(特别是服务和服务边界处)。
在开始重构编码之前,对现有系统深入分析,评估哪些部分应该被重构以及重构的优先级,有助于列出重构计划。全链路上的大规模重构往往都是长期项目,初期制定的计划也肯定会有变化调整,特别是重构过程中会有新的业务需求进入,需要调整优先级,所以计划要分成不同的阶段性里程碑目标。重构期间需要和负责业务需求的研发团队同步协调计划,优先对新需求部分重构,避免新功能完成后立即又成为重构任务。例如,重构后的版本在下周就会发布运行,有些业务需求计划可以直接在新版本上实现,这样既避免了业务研发被技术债务困扰,也减少了重构项目组的工作量。分层设计即定义服务之间、模块之间的边界,是完成重构的关键,让代码之间的依赖关系变得更清晰,减少耦合性。根据关注点分离原则,把现有的功能分类,将对应的代码移动到合适的层级和该层级对应的模块。前提是需要理解每个类的功能是什么,当面对实现多个功能的类,必须要拆分成多个只服务于一个目的的类。单一职责的类更具备可测试性,依赖更少的外部功能,减少测试用例里 Mock 的工作量。单一职责的类也更容易被复用,更容易维护。大型重构项目会有许多工作要做,分层后的设计就像地图,避免研发迷失方向。
在分阶段重构的过程中,引入抽象层(例如面向接口编程),同一个接口有新老两套实现。抽象层的引入允许显示系统里新老版本多个实现的共存。
解决循环依赖
解决 Cyclomatic Complexity
控制每个类的方法数
控制每个方法的行数
- 解决 warning(删除不使用的代码,也意味着减少重构的工作量)
自动化测试是重构最重要的基础,它能确保系统的行为不会因为重构而改变。在重构开始优先保证测试的覆盖率,所以我们会在重构前优先使用集成测试来验证重构结果:设计集成测试需要减少对其他系统(尤其是和外部第三方交互)的依赖,通过一些 Mock 工具提供稳定的测试数据,让测试结果更稳定可靠,比如使用缓存保证相同的请求能拿到相同的结果,这也能帮助后续新老版本的比对回归验证。借助 CI 持续迭代,每一次变更提交会触发自动化测试回归验证,在早期开发阶段快速暴露问题。同时也鼓励研发多次提交变更,避免一次提交包含太多的功能点,减少测试不通过排查问题的复杂度。在整个链路上的服务完成一次里程碑后,会做链路上的端到端的自动化测试,比对新老系统结果,验证整条链路。
a. 线上版本链路入口在收到业务真实请求后,会把请求和响应推送到消息队列里。
b. 比对工具 SmartDiff 订阅消息队列,复制请求重构后的版本链路入口,拿到结果后和线上版本结果比对
c. SmartDiff 把比对结果写入 ES 日志,研发分析日志排查比对差异原因,修复问题。
很多时候,软件架构容易被作为一个学术概念。即使是在一些成熟的研发团队,团队成员能够描述系统架构应该实现哪些重要的非业务功能需求(吞吐量、耗时、稳定性等等),但很难证明系统确实做到了这些。尤其是系统经过长期多次迭代后,当前的架构实现是否还遵循原先的架构设计呢?如果无法验证,研发对架构也会失去信心。所以我们需要对系统定义多个非功能性指标并将其可视化,线上持续地观察和确认是否和我们当初设计期望的指标一致。比如读写缓存接口响应 P90 保持在 100ms 以内,假设后续为了提升缓存命中率,架构重构引入了多个缓存,P90 线 100ms 就可能会不达标。指标可视化帮助我们快速发现新的设计带来的问题并做相应调整。这些非功能性指标就如同测试用例一样,快速发现问题、增强研发对架构重构的信心。