如何解决IT系统历史债务与保持后续质量
2018-08-24 / modified at 2025-10-04 / 3.6k words / 12 mins

在每个项目中,可能由于种种历史原因导致项目渐渐腐化陈旧,后续接手时很难维护,处理债务时除了进行代码整改,还要主动推动质量与流程的建设。

有哪些历史债务

当你满怀期待地(空降/交接)参与到一款项目的开发中,可能发现并没那么简单

  • 项目架构陈旧,存在技术债。比如:1. 代码文档老旧,频繁换人,导致难以传承;2. 线上故障较多,经常耗时间去定位问题;3. 缺乏安全设计,可能出现泄露等事故
  • 整个项目运作混乱不堪,比如缺少必要的验收,上线流程,甚至招聘都是拍脑袋做。
  • 整体开发人员士气差或者能力较弱。

上述场景最常见于

  • 接手祖传代码发生
  • 项目起初赶工,拿奖升级后,最后招一个新员工去接盘

对于上述问题,抱怨是没有用的,债务永远不会消失,只会转移。处理流程大致为IT审计-修改-维护大致如下

$2历史债务IT审计需求审计开发测试审计上线与变更审计债务改善一次性修复技术债安全与合规质量与流程建设研发运维流程项目管理流程招聘流程

准备工作

技术架构腐化一般是上层没有重视(比如压缩工期,频繁换人,需求敷衍等),下游码农趋利避害的产物,难点在于能拿到多少人力资源。

针对这些挑战,制定如下步骤

  • 定义目标:即外部合规,内控尚可,人员稳定。
  • 债务分析:基于IT审计(IT Audit )流程,将整个IT系统的从源码到发布全流程都进行分析。将过程中发现的问题整理为中立的技术报告,并初步估算时间。
  • 汇报与授权:针对审计产生的问题,汇报并获取人力投入授权。这一步可能会将领导架在火上烤,因此要按照优先级确保新需求的平衡性。
  • 债务改善:按照估算的时间进行修复,比如新特性开发与老代码修复分配为3:1的时间。
  • 质量与流程建设:其实就是通过行政手段引入更多检查点。

定义目标

我们进行债务处理时,最优先的并不是修复代码或者升级组件,而是确保存量系统的稳定性,如下

  • 外部合规:确保存量系统不要出现安全事故,在不大改的前提下进行安全加固。
  • 内部控制:对新代码合并与生产运维等变更流程,进行集权审批,确保不出现低级事故,后续质量跟上来后再放权。
  • 人员稳定:不要对存量人员进行施压或逼着加班改,而是给一个长期的时间表,这样好歹有个盼头。

没有彻底摸清楚全部流程,不要急着进行修改,有时很多看似明显的问题都是改不动导致的。

债务分析

正如开一家餐厅需要掌握制作过程,接手项目第一步最重要的就是审计代码如何流向生产环境。通过洞察生产过程,可以发现与业界的Gap,进而进行重构。

  • 存量的需求,设计与测试资料
  • 代码的分支管理与合并流程
    • 代码静态/人工检查
    • JUnit单元测试,冒烟测试,人工测试
    • 数据库设计
    • 业务本身的安全性分析
    • 开源软件清单
  • 相关CICD/DevOps部署流程
    • 组网图,上下游对接系统
    • 运维告警,特权账号,备份,版本等运维信息
    • 可回溯性
    • 安全评估

通过分析上述问题,一般有如下常见风险

  • 需求或设计文档不完善
  • 代码可读性差,也没有测试
  • 数据库设计混乱
  • 线上部署没有加固,第三方软件版本老旧

汇报与授权

将上述问题整理为表格对领导进行汇报,然后找领导要人或者项目延期。这个时候作为决策层,在已经有明确的风险下,要有被架在火上烤的觉悟。

  • 如果决策层认可大部分问题
    • 并愿意投入部分时间。那么可喜可贺,按照时间表排期修复即可。
    • 但是不想投入太多时间。那么只能处理一些简单的债务,比如加更多的测试和监控,委婉地延长修复时间。
  • 如果决策层不认可相关问题,这里可能会出现割裂点,比如不做决策当鸵鸟或者想白嫖的“既要又要”,那么必然是渎职。此时你只有两个选择
    • 闭上嘴,从今往后仅对工时与流程负责,保留好相关记录,不对项目负责。
    • 早点跑路止损

债务改善

需求测试债务

持续补充更新存量的文档。

开发债务

常见的问题比如缩进,命名以及各种无法通过静态检测工具的场景,还有滥用抽象,滥用设计模式,滥用Map等检测不出来的坏习惯

使用服务化隔离烂代码

有些业务模块的确写的非常烂,而且重构成本的确比较高,修改任何一块砖都会崩塌,这时我们可以通过服务化将其封装为有状态的业务组件,新开发的组件通过RPC去调用它。这种操作并没有本质上解决遗留问题,但是新组件可以用标准的质量规范去管理开发流程,总体上分母变大了,相当于进行“隔离”操作。

原子性重构

对于技术架构,静态检查等问题,是可以慢慢改的,难点主要是在测试压力比较大。我个人的原子性提交流程如下

  • TOKEN: 首先对代码进行缩进,imports优化等Token级别的整改,这里不会改变代码编译结果。
  • AST: 接着通过inspect等静态检测工具对代码进行修复,比如空判断,for循环改Stream等技术上的整改,这一部分不会影响业务逻辑。
  • SEMANTIC: 业务中有互斥,依赖,Switch等代码,就算看不懂业务逻辑,人肉分析语义还是做的到。
  • BUSINESS: 针对业务进行具体整改,需要参考各种文档进行,这里没有通用办法。需要先补充测试用例。

上面步骤本质上就是实现了人工的编译器优化,每一次都是原子提交,而不要合并到一起。当运行测试用例报错时,很容易通过二分法迅速定位。

虽然上面只写了短短几行,具体代码改造需要依赖自己读过多少好代码了,这里可以看后文的参考文献

降低代码污染量

在很多项目中,我看到有许多人喜欢把文件按照后缀进行划分,比如service专门一个文件夹,controller一个文件夹,js一个文件夹…这样虽然看起来整洁,但是交接后再进行阅读时定位非常累,需要反复地进行JUMP。同时很多能力水平较低的开发,一不小心用了全局变量(比如给你整几个common.js),后期交接就炸了。对此我的建议如下

  • 一定要明确什么是公共,避免过早进行公共下沉。一般来说公共可以是技术上的(比如后端的JSON工具类,前端的Directive组件),还有可能是业务上的(比如数据字典,姓名联想),对此我建议将技术上的统称为Utils,而业务上的,无论前后台都叫做Service
  • 文件按照业务逻辑进行划分,比如js/css/html统一放到一个login业务的文件夹中,而不是分开放,这样就算代码写的烂,也能将污染降低到最低。

使用DSL&解释器代替生成器

在真实项目中,常见的生成器有Mybatis,SOAP等业务。很多人图方便喜欢用工具一键生成上百行的代码,但是也留下了上百行的维护代价,具体如下

  • Genretor生成的代码,后期不管是否用到,开发都不敢动,就算FindUsage搜索全局成本也很高。如果再经过多次VCS迁移,历史记录丢了就更难受了。
  • 由于生成器生成的代码质量较为一般,容易形成破窗效应,后期开发随意涂改。
  • 生成后的代码很多业务只是在进行繁琐的样板操作,比如HTTP请求中生成了一堆HttpClient连接Socket的代码。
  • 如果涉及到对接第三方系统,那么第三方变更可能导致这边代码需要重新生成,而那些开发对生成代码曾经进行过的小patch就丢了。

针对这些质量问题,我建议如下

  • 使用更高级的DSL或者“纯函数”来描述业务,比如通过Retrofit/Feign的注解描述请求(比如Swagger生成Feign代码),这类生成后的代码就是一个注解描述的接口,开发者直接@Autowired即可注入,内部请求细节由Feign解释器进行处理。
  • 使用Inteceptor插件分离副作用,比如Mabatis的通用分页/通用Mapper可以解决所有单表查询问题,而与业务没有关系;还有Feign中可以定制拦截器来进行实现BasicAuth鉴权等业务无关的功能。
  • 自己写生成DSL的高级生成器,比如通过模版引擎,入参是一个数据库的table,能够生成所有的Controller/js/html,模版是自己手写的,当然定制性比开源更强。

上面的缺点就是很多新人不会使用,这里就要完善文档,降低学习与沟通成本

使用函数式编程降低副作用

纵观很多烂代码,偌大的for循环中无非就是为了实现一个GroupBy,或者为了去重而写了一堆HashSet,或者一堆continue,这类问题本质上是“做什么”与“如何迭代”强耦合导致。通过培训Stream技术可以降低代码中的for/continue等冗余代码,而且通过Clousure可以强制降低代码副作用,建议

  • 开展Java/JS(ES6)中Stream与Lambda表达式培训
  • 对部分重点代码进行Stream重构,简化逻辑

针对超级复杂的逻辑业务,我个人一般使用GroupBy与模式匹配(Pattern Match) 提高代码可读性。

安全加固

对存量系统来说,即使比较烂,大家可能都能忍,但是千万不能在你的手上出现低级的安全问题。

这里需要引入安全建模流程(Threat Modeling Process) 进行分析,比如 STRIDE 模型,常见的容易修改的问题如下

  • 明文的敏感信息存放于代码/数据库/配置中心/日志等
  • 上传下载的入口判断,比如XSS/CSRF
  • 业务权限的SOD,比如不能让人同时管预算审批与拨款
  • 开源软件的定期升级

质量与流程建设

质量与流程本质是通过集权的行政手段干预开发,这类管理工具的加入,必然会降低现有的开发速度,但是也需要投入建设,先固化后改进。

研发代码检视与静态检查

  • 引入自动化检测工具,比如Sonarqube,以及IDE自带的工具。
  • 代码合并尽可能还是走人工二次审批,但是可以通过AI检测降低相关成本。

文档管理

项目中虽然有各种各样的文档,但是大部分公司可能还是用SVN,邮件甚至Word来管理文档,而不是使用Wiki/markdown等工具,导致搜索时比较心累,作为开发者应该如何解决呢?

此刻需搭建集中式的先进Wiki工具,比如Azure DevOps

运维流程

同样进行先集权固化后再放权。

  • 集权阶段,确保接手的任何变化,都需要至少邮件的审批,不要直接进行无授权变更。
  • 放权阶段,等项目质量稳定后,允许相关人员先斩后奏,以提高效率。

项目管理

很多项目组中通常是一人兼任需求分析与人员管理,这种场景下将导致需求长期排满,导致没有挤出时间进行基础功能的优化,进而导致人员,代码的进一步恶化。针对这种情况,说白了还是人手不够,那么就更加需要自动化工具的帮助,但是加班代价是不可避免的。

  • 引入科学管理模型,先达到富士康的模式,尽可能提高搬砖的熟练度
  • 熟练度提高后,再进行多迭代/下层需求等”敏捷“流程,删掉无用的管控流程

人员管理

当前承担开发工作的项目人员,针对项目的无底洞想必也心知肚明,因此可能士气低下。

  • 针对无穷无尽的深坑,需要给出改进点的排期,确保开发人员能够认为“撑过今年就行了”
  • 对于能力较弱的开发人员,及时替换

总结

项目债务处理并非单独的代码债务处理,而是一个复杂的IT审计流程。作为项目管理者,最重要是如下的决策

  • 存量系统维护的投入占比
  • 集权与放权的平衡

参考书籍