最近正好在整理工程代码,需要删除一大波废弃无用的代码,目前已进行了60%,粗估大概有10W行的删除量。删代码看似简单,过程中也是充满了坎坷,好几次因为改动量过大,不得不回退这一小时的工作,整个任务的时间也比预期多了不少。经过不断的尝试、调整,发现其实在项目工程中删代码也是一项技术活,也是有理可循的,正好借这段经历,把其中的经验教训总结一下。
量变引起质变
如果是删除1000行代码,那么不需要任何分析,直接删就完了;但如果删除的是1W行、10W行、100W行代码,那直接删除绝对会”头撞南墙”,”头破血流”。
任何事物都是一样,随着规模的增大,复杂度不是线性增长的,而是几何增长的,就像上云,如果你只管理几台服务器,那不用上云,手动操作就可以,但如果是管理几千台服务器,你就需要一套强大稳定的管理工具,而上云就是最好的解决方案。
删代码也一样,不要小看代码的删除,如果删得快还删得准,是一个技术活,你也不想删了一遍后,编译1W+的报错,这要修到何年何月;你也不想服务启动后,各种莫名奇炒的报错;你也不想误删了一行代码,导致后面某次外网版本出事故了。对于一次大规模的代码删除,其中依赖关系、涉及模块更是错综复杂,一味蛮干只能是事倍功半,用科学的方法指导,层层推进才是正确之道。
修改不可控的原因
你以为你是要从下图中剪去一根电线:

实际你是要从下图中找到那根电线并剪去:

不要怀疑工程项目的”屎山”程度,这是客观规律决定的,很多大型项目的代码逻辑和第二张图如出一辙,牵一发而动全身。
面对错综复杂的依赖关系,你准备删除A,然后发现B、C、D依赖A,你要先删除B、C、D,然后发现E、F依赖B,G、H依赖C….最后为了删除A,你需要删除几千行代码,这个复杂度就大大提升了。

项目中并非所有的代码都是低耦合的,不管是为了需求,还是为了赶进度,高耦合在业务代码中是常态,当你要删除大量代码时,你会面临大面高耦合的代码,所以,你最终面对的不是要删除的N行代码,而是N+N耦合的代码,至少会大一个数量级,这也是为什么一开始无脑删,越删越多、越删越不可控的原因其一。
原因其二是删除代码并不是纯粹的删除,往往还涉及到已有逻辑的修改,比如A模块用了B模块记录的数据来执行触发逻辑,现在要删除B模块,那么A模块的触发逻辑就需要重新设计了。
原因其三是删除代码并不是纯粹的你一个人的工作,比如要删除某个任务类型,那配置中相关的任务都需要删除,相关配置中引用了这些任务的都需要删除或修改,这又涉及到了策划和客户端的工作,它变成了一个团队协作任务。
删除原则
原则一:由外至内,抽丝剥茧
根据删除代码的调用关系,我们可以将代码分为几种类型:
- 闭包的代码,无外部调用
- 仅被待删除的代码调用
只被待删除代码集的其它代码调用 - 被外部代码集简单调用
调用删除对原有逻辑基本没有影响,比如被删的代码提供的是数据填充的功能,删除调用相当于不填充该数据,对原逻辑没有影响。
1 | // 待删代码 |
- 被外部代码集耦合调用
调用删除会影响原有逻辑,需要重写部分代码,比如原代码逻辑都是通过组队接口进入副本的,现在删除的组件模块,这里进入副本的流程就需要重新修改。
1 | // 原代码逻辑 |
- 被多个内、外部代码集调用

依据难易排序如下:

闭包代码自不必说,直接删除即可,不会对其它代码靠造成任何影响,是可以最优先处理的内容;
简单被调需要遵循由外及里的原则,先删除外层的调用,最后再删除代码;
内部被调略显复杂的是调用的代码,可能又别其它代码调用,所以要从依赖链的最外层开始删除代码。如图所示,待删除的是A,但因为依赖链的关系,需要先处理掉B、C、D。

多个模块依赖和上面同理,遵循由外及里的原则,只是涉及的模块会更多。
耦合调用涉及原有代码逻辑的修改,需要放在最后来做,一是修改代码本身就是高风险的,会引入逻辑bug,二是修改代码本身就是高复杂度的,在前期做会增加复杂度。
原则二:单一原则,不耦合其它职能团队
被删除的代码可能还会涉及配置、DB、协议、通信的修改。
配置修改:比如删除了一种任务类型,那相关的配置也要进行清理,这里会涉及策划、客户端的工作
DB:比如删除了一张表,这里会涉及DBA的操作
协议:比如删除了一个字段,那相关使用用这个字段的代码都会进行清理,这里会涉及客户端的工作
通信:比如协议改为了udp、监控端口发生了变化等等,都统一归到通信,可以理解为外部调用的方式发生了变化,这里会涉及外部访问端的工作

团队协作一定是低效的,随着项目规模增长,这个效应会激增,比如这里删除代码的工作,其它职能团队的优先级会很低,那么你一旦依赖他的推进,你可能永远也推进不了,而且一旦需要多团队配合,调试、合入成本都是急剧升高的。
那么,对于这种依赖,我们最好的解法就是维护其兼容性。举例来说,对外暴露的地址发生了变化,最好的做法是加一个转发服,将旧地址转发到新地址,这样,你可以不依赖外部迁移完就可以开展工作;再比如删除某种类型的任务,可以保留任务类型的定义,将任务类型实现置空。
总之,花一点时间写一些兼容代码,或保留一些残留代码,都是性价比极高的,如果它能让你避免给其它团队派活。
原则三:大任务拆小,一步一个脚印
大范围删除涉及的文件量庞大,可能一次批量替换就小汲上百个文件,如果叠加几次操作,然后发现方法不可行,这时你想只回退某几步操作就不可能了,你只能无赖全部回退,白费了半天功夫。
所以,对于大范围的删除,一定要做任务拆解,按原则一拆分成多个删除目标,每次只达成一个目标,每个目标达成后,都要进行验收(如编译、测试通过),并进行合入。
拆分目标后,每个目标也要做到勤提交,多编译,避免大规模回退,避免积累大规模问题。
要知道编译在删除代码中占用的时间是最多的,一个是编译的时间长,一个是编译的次数多,如果积累了N次提交再一起编译,如果遇到一个莫名奇怪的编译错误,百思不得其解时,你就需要逐个回退来定位问题了。同样,如果积累了上千行的变更再一起编译,那可能要解决的编译错误是100+,靠编译逐步去解决需要耗费大量的时间。
这就是在任务拆小,一个一个脚印的意义,每次只解决一个或两个问题,可以大大降低整个问题的复杂度。

原则四:明确验收标准
“工欲善其事,必先利其器”,在开展工作前,需要花一些时间建立验收标准,比如写一些冒烟用例,或者手动跑的测试用例,每达到一个里程碑,都需要花一些时间进行验收,修复验收过程中的bug,达到可用性的标准,除一些极边界的情况外,不要耦合进bug。
原则五:适当保留,不要有洁癖
大范围删除中,肯定会遇到一些难度很大的删除,可能是依赖它的模块过多,可能是耦合的逻辑过于复杂,只要不影响后续删除进行的,可以先放一放,后面再来做,这样可以大大降低这次删除工作的复杂度,也会大大缩短工期。而且残留些许代码并不会影响程序的运行,也不会造成编译速度降低、包体变大,写好注释即可。
原则六:聚焦,不要发散
删除就仅做删除的事情,即使看到可以顺手优化的点,也不要手痒改了,可以记录在todo list,等删除工作完了再来,原则就是修改越多,越不可控,你已经是在做一件极其复杂的”手术”,不要节外生枝。