代码腐化的五个信号:我从这个项目里看到的
老炮踩坑录 · V03 · 老炮视野系列
基于「企业融合评估平台」真实源码,跟着一个"半天能做完"的小需求走完全程,看五个腐化信号如何在改动现场挨个露面
关键词:额外修改成本 · 重复方法 · 超大文件 · 废弃的注释代码 · 模板占位符
👋 欢迎阅读
🏠个人主页: 知守观
📘我的专栏: 老炮踩坑录
💻当前内容:代码腐化
引子
2022 年春天有个新需求,我到现在还记得自己当时的工作量预估:只要半天时间。
产品要的东西不复杂,几个申报列表上加一个时间提示,用来显示这条申报距离今天过了多久。列表查询本来就有,日期字段本来就有,取出来算一下,展示,完事。我给产品回的话是"明天给你上"。
实际上我花了三天才完成。
这三天里我没有解决任何技术难题,没有设计任何新模式,大部分时间在做两件事:找代码,以及判断找到的代码还作不作数。后来我复盘这三天的时间去向,发现五个熟悉的信号全在现场。这篇就按那天的真实路径走一遍,带你看看"一个半天的需求怎么变成三天的"。
先交代一句,当时的对话和具体钟点我凭记忆还原,但代码、行号、数字全部来自现在的代码仓库,2026-10 实跑可查。这个仓库快照里 git 历史只有二十几个提交,都是近年的,原始开发记录不在里面,所以没法按提交时间复原当年的过程,靠的是代码里留着的版本标记,下面会讲到。
先说清楚,这篇不是吐槽。这些代码里,有不少是我自己经手留下的。写它是因为那三天让我想明白了一件事:腐化不是某个人某次不负责任的产物,是一连串"当下合理"的决定长期累积的结果。
信号一:还没动手,先要在一千五百行里找到位置
接到需求,我先找项目申报列表在哪。
IDE 里打开 EnterpriseRegistController.java,1600 行。跟它配套的 ApplyInfoServiceImpl.java,1796 行,34 个方法。整个项目里 800 行以上的文件有 9 个:
yaml
1796 ApplyInfoServiceImpl 34 个方法
1545 EnterpriseRegistController 25 个方法
1488 ApplyElecInfoServiceImpl
1048 EnterpriseRegistServiceImpl
903 ApplyInfoDeptServiceImpl
871 FileController
866 HuaweiyunFileServiceImpl
866 ApplyTypeInfoServiceImpl
812 HttpUtil
EnterpriseRegistController 名义上管企业注册,25 个方法里躺着四个报表导出。exportPlustekDiagnosis 从 737 行铺到 1109 行,373 行的一个方法,清洗数据、算表头、渲染样式、写输出全在里面。我要找的"申报列表"埋在这些东西中间,滚动条拖了几个来回才定位到。
文件长本身不会直接产生 bug,它先产生的是成本:你读代码的每一分钟,都在为它的体积付费。那天上午的头一个小时就这么没了,一行代码还没改。
信号二:找到一个列表,发现它有三个兄弟
定位到方法,准备动手,发现事情不对。同一个区县申报列表,项目里有三个方法:
yaml
countyApplyInfoList 1276 行
countyApplyInfoListForQuxianOnly 1427 行
countyApplyInfoListZhuanjia 1605 行
三个方法从 1276 行排到 1716 行,骨架一模一样:分页、查列表、拼字典、算诊断状态、组装返回。差异在查询条件和少数字段,占不到十分之一篇幅,但"加时间提示"这件事,我得在三个地方各做一遍。
文件里这种带编号的重复方法还有更直白的。FileController 里紧挨着:
java
public Result uploadDiagnosis(MultipartFile[] files, ...) // 60 行
public Result uploadDiagnosis1(MultipartFile[] files, ...) // 51 行
public Result deleteDiagnosis(...)
public Result deleteDiagnosis1(...)
"1"这个后缀讲了个很短的故事:旧方法线上有页面在调,不敢动,新需求压下来,复制一份改改最快。
重复方法真正的问题在以后。哪天文件名校验要改,你搜到两个上传方法,没法确定哪个还在使用------实际情况是两个都有页面在调。改一个漏一个,一个月后另一个页面出问题,排查的人完全想不到两者同源。那天我对着三个方法逐行比对差异,又是半天。
信号三:改完一测,页面不对劲,但什么错都没报
三个方法都加上时间计算,本地跑,页面显示不对劲。没有异常弹窗,没有红色日志,就是某一列数据不对。
追到头,问题在时间解析的兜底上。项目里这种写法遍布各处,以 ApplyInfoServiceImpl 1295 行为例:
ini
try {
date = simpleDateFormat.parse(createTime);
} catch (ParseException e1) {
e1.printStackTrace();
}
String dateDep = DateUtils.formatFromTodayDep(date);
parse 失败,堆栈拍向控制台,date 保持 null,下一行照走,页面拿到一个奇怪的结果。整个项目里 printStackTrace 有 117 处,26 个文件,HttpUtil 一家 27 处。我搜了一下空 catch,12 处,HttpUtil 第 29 行那个 static 初始化块,异常连控制台都不上:
php
static {
try {
trustAllHttpsCertificates();
HttpsURLConnection.setDefaultHostnameVerifier((urlHostName, session) -> true);
} catch (Exception e) {
}
}
测试时我的 parse 为什么失败,控制台其实打了,但那堆输出混在百来行历史堆栈里,眼睛直接跳过。异常处理烂到一定程度后,连真的异常也混在大量无效输出里。这一段调试耗掉我第二天的大半个下午。
信号四:翻参考实现,翻到一个完整的废弃方法
改的过程中我想确认下"时间提示"老版本有没有类似实现,顺着相关的类翻,在 WjxServiceImpl.java 第 48 行看到一大片注释:
ini
// @Override
// public Result addApplicationCompany(JSONObject json) {
// String companyName = json.getString("q1");
// String companyLegal = json.getString("q2");
// ...
// String a24 = json.getString("q33");
// ApplicationCompany applicationCompany = new ApplicationCompany();
76 行,一个完整的废弃方法。FileController 第 419 行还有 65 行,旧上传接口的完整实现,Javadoc 齐全。全项目疑似注释掉的代码行有 456 行,连续块超过 20 行的有一大批。
这段废弃代码差点坑了我。我第一眼以为那是某一版的参考逻辑,下意识读了下去,读到一半才反应过来它已经废弃------它引用的字段结构跟现在对不上。如果当时是个新人接手,照着它补新方法,半小时就白费,还可能引入已经作废的业务规则。
注释代码给每一个读文件的人增加一道额外工作:停下来,判断它是否还在使用,没有注释说明为什么保留,全靠猜。
信号五:想搜前人留的说明,135 条全是模板占位符
我没死心,想着这么关键的列表,万一有前人留过 TODO 说明。
bash
grep -rn "TODO" src/main/java | wc -l
# 135,分布在 51 个文件
翻了二十几条,没有一条是真的:
kotlin
/**
* @Description: TODO(这里用一句话描述这个类的作用)
*/
public class SysIndustry {
代码生成器自带的模板,文件生成那天起就留在类头,再没被人碰过。真手写的待办混在里面,比如 ApplyElecController 里的 //TODO xmlu add,但我已经没有耐心从 135 条里把它们挑出来了。标记一旦失去区分度,跟没有标记没有区别。
占位符的同类是半成品。仓库里的 AbstractFileService 模板方法搭得不错,md5 去重都写了,全项目只有 HuaweiyunFileServiceImpl 一个子类接过去,其余上传入口照旧各写各的。重构进行到一半人被拉走,抽象留下,后来者还容易产生"这事好像有人做过了"的错觉。
五个信号如何叠加增加成本
单个信号看着都不致命,麻烦在它们互相影响。
复制粘贴攒出重复方法,重复方法多了没人敢动,文件体积失控;文件越长,找代码越依赖搜索,注释掉的废弃代码和模板 TODO 就混进搜索结果形成干扰;异常被 printStackTrace 淹没后,真问题排查更慢,大家更倾向"先复制一份保底"。一个需求三天,其中半天是干活,两天半是在这套循环里打转。
代码里留着一个特殊的记号能看清这件事。V3.5 是项目中期的一次大版本,我数了,"V3.5" 标记 36 处,分布在 13 个文件。我这次改的时间提示,就是 V3.5 的需求,它落地的痕迹现在还在三个列表方法里:
dart
// V3.5 添加提示时间 start
String createTime = String.valueOf(e.get("sbsj"));
...
e.remove("sbsj");
// V3.5 添加提示时间 end
ApplyInfoServiceImpl 的 1291、1471 行各有一块,BackDeclareListServiceImpl 129 行还有一块------三个方法里各插了一份,连标记都复制了三份。这类 start/end 块当年是为了好定位改了哪,几年之后,没人知道这些 V3.5 兼容分支哪些数据还在走,哪些可以删。历史遗留代码就这么一层层累积起来。
在你的项目里重走这一遍
五个信号的统计命令,跟那天的排查路径同序:
| 信号 | 怎么搜 | 该警觉的量级 |
|---|---|---|
| 文件体积 | 按行数排序,再数单类方法数 | 单文件 800+、单方法 200+ |
| 重复方法 | 搜带数字编号的方法名,同名方法比对方法体 | 出现 1/2 编号 = 已经在复制 |
| 异常被吞 | grep -r printStackTrace src/,再搜空 catch |
健康项目趋近零,按文件密度看 |
| 废弃的注释代码 | 脚本扫连续 // 开头且像代码的行 |
连续超 20 行的块必须处理 |
| 占位符 | grep -rn TODO src/,区分模板和手写 |
模板占位跨版本还在 = 没人管 |
补一句实话,单看某个信号可能冤枉人------printStackTrace 最多的也许恰好是没人动的工具类。五个体征在同一个模块里同时抬头,再下判断。
老炮点评
这篇文章里批评的东西,有不少是我自己经手留下的。那三个"V3.5 添加提示时间"块,时间提示这个功能,就是我做的。
当时没人觉得有问题。需求压着,三个重复方法是历史遗留,我做的是"按现有风格把功能加上",一天干完,产品满意,测试通过。我甚至还规规矩矩打了 V3.5 标记,方便以后定位。站在当时看,这活干得没毛病。
腐化最吊诡的地方就在这儿:它从来不是某个人某次不负责任的产物,是一连串"当下合理"的决定长期累积的结果。我写这篇不是站在旁观者的位置指责当年的开发者------这些代码里也有我的一份。
那三天教会我的东西也简单:判断一个项目健不健康,别看架构图,找一个真实的小需求,计时。改动顺畅、涉及面一目了然、异常有据可查,项目就还活着。一个半天的需求要三天,这个项目就在为过去的每一个"先这样吧"付出代价,而这些成本全部由现在改代码的人承担。
延伸阅读 :
如果你还没看过这个项目的重构方案,可以翻一下 《# 老项目重构实战》。
下期预告:《老炮的代码审查清单:我 Review 代码时看什么》
格式问题交给规约插件,我 review 时看的是异常流向、事务边界、空值路径、并发里共享了什么。下期把清单摊开,每一条配这个项目里的真实反例。
如果本文对你有帮助,欢迎:
👍点赞 | ⭐收藏 | 👤关注 | 💬留言
你的每一次互动都是我继续更新的动力,我们下一篇见!🚀
我是老炮,18 年 Java 老兵,仍在一线。关注「Java老炮踩坑录」,不错过每一篇真实案例,少踩坑。