如何解决项目历史债务与保持后续质量
2018-08-24 / modified at 2024-04-07 / 4k words / 13 mins

在每个项目中,可能由于种种历史原因导致项目渐渐腐化陈旧,后续接手时很难维护,作为码农除了进行代码整改,还要主动推动开发流程的优化。

有哪些历史债务

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

  • 项目架构陈旧,比如仍然运行着漏洞百出的Struts与Java6,我见过最老的项目是使用Java4的某垄断项目。
  • 代码技术上很差,比如缩进,命名以及各种无法通过静态检测工具的场景,还有滥用抽象,滥用设计模式,滥用Map等检测不出来的坏习惯。
  • 代码逻辑质量差(写代码不用脑子),连基本的互斥,依赖,循环等逻辑都搞不定
  • 交接基本等于没有,平时没有Wiki/文档意识

上述场景最常见于

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

对于上述问题,抱怨是没有用的,债务永远不会消失,只会转移。只能说明你找工作不慎重。

非技术手段解决方法

为什么要把非技术放在前面呢,因为技术架构腐化是上层没有重视(比如压缩工期,频繁换人,需求敷衍等),下游码农趋利避害的产物。这个话题其实有点宽泛,这里主要从这几个方法

价值分析

假设你作为新社招员工,对目前项目改造有很多想法,其实问题很复杂

  • 技术架构的改进在强KPI考核下吃亏,国内文化更趋向于奖励救火的“神医”
  • 周边保守同事不配合(二八定律),项目运行好好的为啥要改;还有同事认为你在抢坑,实际上自己根本不想接这种盐碱地。
  • 公司当初管理欠下的债务,不应该你投入时间来还

针对这些挑战

  • 明确自己有本事能够搞定问题并能攥在手里
  • 自己能带来小跟班或者有权取得支持调动小弟
  • 代码未动,胶片先行: 针对项目中种种遗留问题,仔细解释当前债务,规划改造思路并指出整改成本与收益,并汇聚为PPT向领导汇报。这一方面可以获得领导的共识与推动,也相当打了一个时间戳,防止自己的苦劳被南郭先生收作业。
  • 自己可以优先进行一个小模块的改造实现POC,改造成功后进行项目培训,告诉同事改造有如何收益。千万不要自己嘴上方法论却不实战写代码,同事会暗中鄙视你的。
  • 定下代码质量规范,为长期维护提供基础。

三十六计:识别拒绝&跑路止损

历史债务大多数是盐碱地,就是连殖民地都不如,更不用谈黑土地了。

对于这种炸弹,如果自己没有信心,还是建议拖着并及时暴露风险。当然这种方法与印钱还债一样,如果债务多了,最终还是可能崩盘滴。如果发现项目实在带不动,时薪太低,没有成就感,领导不认可这种维护工作,没有指挥或方案权,那么还是早点跑路止损吧。

识别与开除

虽然这类工作一般是PL来做,如果你想发展为TechLeader的,必须要做到把每个人对项目的态度搞明确。很多项目花了很多钱,换了一波波中层,为啥做不起来?每个人的态度太重要了,举个例子,我在项目中遇到的各种人

  • 社招能力完全不行,培训成本比应届生高的人
  • 与世隔绝,不听指挥,二传手的人
  • 迷迷糊糊,每天干很多工作,也加班,却不知道在干啥
  • 坚持工作,长期对项目热情的人

我这里并不是为资本家说话,而是想表达拿钱就要出力的观点,任何公司都没有铁饭碗。为什么很多领导空降后,都是自带团队,并进行中层换血呢?原因就是这个,空降带人,别人又不吊你是很受气的。你需要为PL等管理者提供建议,为真正热情的人加工资发掘潜力,开掉能力弱的人。

作为普通员工,要明确自己的定位,要么兢兢业业按照要求完成固定的分工,要么个性能力强一人全部放心包办。不能既不担责,又不服管,那么就只能送一个“Ad hoc motion”大礼包了

人员招聘

虽然有很多人将格子间中的码农比作十九世纪的纺织工,但是编码工作本质仍然不是流水线的工作。因此招人就显得非常重要了,详见我以前写的文章。

特别注意的是,如果人员不合适(包括代码,认知,言行等),一定要及时在试用期把人给换掉。这个流程是长期的,带有主观偏见的,比如我以前鄙视HR的唯学历论,认为学历不能代表能力,但是现实屡屡打脸:三年经验的三本同时与一本的应届生接手新技术/新项目,学习与输出能力就是差很多。

这里也侧面说明了应届生找一家靠谱的公司多么重要,差公司可以让应届生在三年内养成了一堆坏习惯

代码检视与静态检查

虽然这个是代码相关,但是代码检视本质是通过行政手段干预开发,这类先进的检测工具虽然很多,但是耗费人力比较大,说的不好听就是靠强制力推动,最低要求如下

  • 墨菲定律:只要没检查到了,一定会出问题,所以要渐进提高检视覆盖量
  • 各种IDE右边自带的WARNING要清空;FindBugs的静态扫描也要清空
  • 推荐使用IDEA的UpSource工具进行手动阅读
  • 可以把统计报表(比如SonarQube)中的烂码率纳入KPI(但是如果你是冒尖的,那么就危险了)
  • 使用Jenkins提高项目自动化(本文重点是改造遗留项目代码,这里不详谈团队效率改进)

文档管理

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

我的建议: 实现关键词秒查解决

  • 搭建ElasticSearch/Tika附件索引服务
  • 使用Docfectch等工具建立本地索引
  • 推广GitBook等先进Wiki工具

至于如何推动所有人写文档,这个比较难,但是要勇于深入群众,服务群众。

不能剥夺弱者的尊严

当你看到某个烂代码时,可能大声叫喊着“谁写的烂代码”,这种行为虽然可以理解,但是我个人建议一定要克制。

  • 不要居高临下: 当你看某人不顺眼,觉得对方能力坑,你可以通过正式渠道直接通知领导,让领导去决策是加强培训还是走HR流程,但是不要在公开场合直接指出不足(对事不对人),或者表达出让其离开的观点。如果这么做了,那么和北京驱赶低端人口并没有什么不同。
  • 发掘成就价值: 有人可能代码能力差,但是耐心好,可以分配拉通对接第三方,支撑运维的任务;有人喜欢研究新技术,但是代码有坑,可以分配独立微服务让其自己成长;有人前端切图好,但是JS不行,就不要强行安排框架的活;有人没法搞定模糊的任务(比如新员工),可以分配明确而且繁琐重复的搬砖任务(比如代码重构,测试用例)。总之,不要老是抱怨同事代码质量差,任何人都不完美

长远管理

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

技术手段解决方法

掌握生产过程

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

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

使用服务化隔离烂代码

有些业务模块的确写的非常烂,而且重构成本的确比较高,修改任何一块砖都会崩塌,这时我们可以通过服务化将其封装为有状态的业务组件,新开发的组件通过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) 提高代码可读性。

培训一览

下面是可能需要进行培训的知识点一览

研发过程规范

  • 敏捷开发流程入门
  • SonarQube使用
  • 在线Wiki编写与流程图绘制

编码培训

  • null,注解,if使用培训
  • Java8与函数编程培训

技术培训

  • 取决于项目中使用的技术

参考书籍