1. 代码为什么总是很快变脏:四个来源和一次清扫契机
先说个真实场景。我维护一个小型服务端项目,功能迭代不大,但代码量从八千行涨到两万行只用了四个月。涨得快不是因为业务复杂,而是因为我每次在紧急需求里都会顺手留下点东西:一个测试用的print、一段被注释掉的旧逻辑、一个改了三次最终没用的临时变量。等到功能稳定想回头整理时,已经没有勇气去动那些文件了——我根本分不清哪些代码还有用,哪些是垃圾。
那段时间我也观察了一下周围同事的代码,发现"脏"并不是我一个人的毛病。脏代码的来源基本逃不出四种:
- 调试残留:为了定位问题加的日志、断点、临时分支,问题解决之后原样躺在代码里。
- 边写边改的阴阳代码:一种新方案写了一半,旧方案没删,两套逻辑互相掩护。
- 复制粘贴的衍生品:从别的模块粘过来改了一半的函数,字段和命名全都不对味。
- "以后可能用到"的防御性代码:当前根本没有调用方,但就是舍不得删的抽象和工具函数。
这些东西单看都不致命,但累积起来非常麻烦。我在一次重构中想加一个字段,结果全局搜索关联变量时,被十几个无用的引用干扰,差点改错了地方。那一刻我意识到:代码卫生不是一个"有空再做"的加分项,而是直接影响后续开发效率的必做事。
今年年初我开始尝试把"代码清扫"这件事系统化。一开始是给自己列清单,后来发现清单每次执行都凭记忆、全凭手感,不同时间、不同项目里的执行尺度还不一样。于是就有了 Hygiene-sweep 这个 skill:把一套六步清扫流程固化下来,让代码检查从"想起来才做"变成"按流程执行"。这套流程的核心思路是:不是靠自律去保持代码整洁,而是靠一套固定的、可执行的、有先后顺序的清扫程序。
在往下拆解这套流程之前,先说明一下这个 skill 的使用场景。Hygiene-sweep 的定位不是给代码库做架构级重构,它的目标很明确:在一个任务或一个迭代结束之后,对变更范围内的代码做一次卫生检查,扫掉那些妨碍后续阅读和修改的杂物。它适合单人项目,也适合多人协作中每次合入 MR 前的自检。它不替代 Code Review,它是在 Code Review 之前先让自己这关。
2. Hygiene-sweep 六步清扫清单:每一步的逻辑、粒度与误伤边界
六步清扫不是随便排的六个动作,它们之间有严格的先后依赖。大体逻辑是:先建立安全基线,再清理最容易误伤的"死东西",然后在活代码里做减法,最后处理格式和隐患。每走一步,代码的"可读信号"就会干净一截,而每一步都有具体的判定标准和误伤边界。下面逐个说。
2.1 第一步:先建立基线,再动手删东西
很多开发者在清代码时有个致命习惯:上来就删,删完再跑编译。这个顺序是错的。
我在 Hygiene-sweep 里规定的第一步是建立清扫基线,也就是在动任何一行代码之前,先确认三件事:
- 当前分支上的代码可以正常构建。
- 测试套件(如果有)是通过的。
- 你手上有这次清扫的变更范围清单,知道自己要碰哪些文件。
这个步骤看起来没有技术含量,但它决定了后面五步的可靠性。原因很简单:清扫本身就是一种重构,重构的前提是"运行状态可回滚"。如果你不知道清扫前的代码是绿的,清扫后出现问题时你就无法判断到底是清扫弄坏了什么,还是代码本来就有问题。
我在实际执行中会把清扫前的结果记录到本地备注里,包括构建输出和测试通过的报告摘要。这不是形式主义,我踩过这个坑:有一次清理一个模块后,接口超时明显变慢,排查了半天发现模块里有一段"看似是死代码"的逻辑——实际上它在一个回调里被间接调用了。如果没有基线记录,我很难快速意识到自己碰过的范围到底有多广。
提示:这一步还有一个容易被忽略的动作——查看当前的 Git 状态。如果工作区里同时有好几个改动点混在一起,清扫完之后你会很难区分哪些改动属于卫生清扫,哪些属于功能开发。我通常会在清扫前先 commit 或 stash 掉不相关改动,保持清扫的独立性。
2.2 第二步:扫死代码,先解决"没人要的东西"
基线立好之后,第二步是扫死代码。这里说的死代码,定义很严格:在当前代码库内,没有任何调用方或引用方,且不属于对外暴露接口(API、配置项、动态注册机制)的代码。
具体扫这三类东西:
- 未被引用的函数和类:用 IDE 的 Find Usages 功能逐一确认,或者用 grep 全库搜索标识符,确认没有任何引用点。
- 无效的条件分支:比如
if (false)、永远为true的常量判断、无法到达的return之后的代码。 - 注释掉的代码块:这是我的 TOW(容忍上限)原则——如果一段代码被注释掉,且没有任何说明它为什么被保留,删除。如果注释里写明了"暂时保留,等某功能完成后启用",那么检查这个"某功能"是否还处于活动状态,如果已经放弃,也删。
这个环节最容易误伤的,是"看似没有引用、实际通过字符串或反射调用"的代码。我见过很多 Python 项目里用getattr(obj, method_name)动态调用函数的,也见过 Java 项目里通过 Spring 的 bean name 注入的。所以在删除任何"零引用"标识符之前,我会额外做一次字符串全文搜索,确认方法名不是以字符串形式出现在配置文件或调用注册表中。
第二步执行完毕后,我会顺手把一些只在一个地方被用到的工具函数做内联处理,或者移到调用方附近。这不是必须的,但能让代码归属更清晰。
2.3 第三步:扫调试残留,把"检查用的"和"生产要用的"分开
死代码清完,接下来处理的是"活着但属于干扰项"的调试残留。这一步的典型对象是:
- 调试日志:
console.log、print、System.out、logging.debug这类临时输出。 - 临时断点和条件断点:IDE 里可以清,但代码里
debugger语句是要删的。 - 测试用的假数据分支:比如写死的
if user_id == 123: return mock_result。 - 临时代码块:比如为了验证某个算法手写的循环,功能完成后再也没用上。
这里有一个边界问题:不是所有日志都属于调试残留。生产环境需要的审计日志、请求日志、错误日志不在这一步的清扫范围内。我的判定标准是:看这个日志语句是否记录了业务状态语义。如果一条日志是为了记录"用户下单成功、订单号是多少"这样的业务事件,它属于功能的一部分,不能动。如果它是为了在开发时看"走到这个分支没有"这种流程确认,它就应该被清掉。
从实际操作角度,我不建议用暴力搜索一次全删。更稳的方式是:先搜索出所有疑似残留的日志点,快速扫一遍每一条的上下文,判断它是为了什么目的存在的。然后按两种方式处理:确认没用的直接删除;暂时不能确定的,先加一行说明性注释,让代码保留"开发时为什么这里会有输出"的上下文,等版本稳定后再删。
注意:不要低估调试残留的隐藏风险。有些
console.log每次请求都打印了一次,代价是几十毫秒。清扫调试代码不是洁癖,是性能账。
2.4 第四步:扫"絮状物":导入、依赖和名字的乱麻
到这一步,剩下的代码基本都是"在用的",但往往还缠着一层絮状物——导入关系混乱、依赖声明多余、命名失序。我把它分成三类:
- 多余的导入/依赖:代码里已经不再使用但没清理的
import语句、require表达式、include指令。几乎每个项目在重构后都会留下一批"不再被引用的导入",它们不报错、不影响运行,但增加了编译/加载时间,更重要的是干扰读者对模块依赖的判断。 - 重复的依赖声明:同一个依赖在
requirements.txt中出现多个版本、在package.json的 dependencies 和 devDependencies 里同时出现。这类问题在多人协作中特别常见。 - 命名失序:名字和实际用途不一致的变量、函数、类。比如一个函数叫
get_user_info,实际干的是查询订单;一个变量叫temp,结果在两百行作用域里被反复赋值,每次含义都不同。
处理导入问题,我习惯先让 IDE 的自动优化功能跑一遍,然后手动复查被删掉的项,确认没有反射、动态 import 之类的特殊场景。处理命名问题,则要克制——我只重命名那些明显误导的标识符,不会为了"更优雅"而进行大规模改名,因为改名本身会引入 diff 噪音,增加 Code Review 成本。
说实话,这一步在"卫生清扫"里优先级可以排低一点,因为它的收益不像清死代码那么立竿见影。但它有一个长期价值:每个模块的依赖关系一旦变得透明,后续做模块拆分和架构演进时,你能清楚地知道这个模块到底和谁有耦合。我建议至少做到"让 IDE 的依赖分析结果和实际代码保持一致"。
2.5 第五步:扫格式漂移和结构漏气
第五步聚焦的是"一眼看上去不够整齐"的地方。代码本身没有技术错误,但会让后来者阅读时额外消耗注意力。
具体包括:
- 同一层级里两种缩进风格并存。这种情况多发生在不同编辑器和不同成员贡献的代码混合在一起后。
- 命名风格不统一:同一个项目里,有人用下划线命名,有人用驼峰命名,而且互相同时出现在相近作用域。
- 空行和分组逻辑混乱:函数内的变量声明、业务逻辑、后续处理没有清晰分组,所有代码堆在一起。
- 过长的参数列表:超过四五个参数的函数,调用处读起来非常痛苦。
- 魔法数字和魔法字符串:代码里直接出现的
3000、"paid",没有命名常量解释含义。
这一步也是最容易引发争议的一步,因为"格式好不好看"是主观的。我的处理原则是:如果项目里已经有 linter / formatter 配置,就交给它们跑,并且只接受 autofix 结果;如果项目没有配置,我会最小干预——只处理与当前改动直接相关的行,不碰无关的老代码。这个原则很重要,它把格式化从"个人审美"变成"工程约定",避免清扫演变成一场风格战争。
顺手补充一个结构层面的检查点:方法内缩进过深。如果一个函数里出现了超过五层的嵌套(循环套条件套循环),这不是格式问题,是结构问题。遇到这种情况,我不会在清扫流程里去重构成多个函数,但我会在清扫报告里标记出来,作为后续重构的建议项。原因是清扫的目标是"让现状清晰",不是"推翻现状"。
2.6 第六步:扫隐患,把"看不见的坏味道"暴露出来
最后一步我称它为"隐患扫描",它不再关注代码长得好不好看,而是关注在执行边界上是否存在可见的缺陷风险。这些风险不在编译器的报错范围内,但在运行时可能变成事故:
- 空 catch / 空处理块:异常被捕获后什么都不做,静默吞掉的错误会在几个小时后变成诡异的问题。至少要有日志。
- 过度宽泛的异常捕获:
except Exception甚至except:没有区分错误类型,导致真正需要特殊处理的异常被统一吞掉。 - 非空断言和强制转型:
!(Kotlin)、as!(Swift)、assert类写法,一步断言失手就崩在运行时。 - 资源句柄未关闭:文件、游标、请求句柄,在 Python 里只看
open()和close()的配对,在 Go 里看defer的使用。 - 竞态迹象:共享变量在多个线程/协程里被无锁修改,或使用了不保证原子性的"检查后动作"模式。
第六步的产出不是一个修复动作,而是一个风险列表。我的建议是:能当场修的当场修(比如缺日志的空 catch),需要更大改动来根治的写进清扫报告,标注严重级别。这么做是因为"隐患"往往是结构性的,强行在一个卫生清扫任务里深挖重构,反而会扩大改动范围,违背清扫的初衷。
六步走完之后,跑一次完整构建和测试,对比第一步记录的基线,确认代码状态没有变差。然后把清扫过程中实际删除的内容、修改的位置整理成一段简短的清扫摘要,提交时一并写入 commit message。这样,代码卫生这件事就有了完整的痕迹,后续任何协作者都能清楚地看到这次清扫做了什么。
3. 把六步流程固化成一个可复用 skill:核心设计与触发规则
六步清单在纸面上已经成立,但它距离"一个 skill"还差得远。一个真正的 skill,至少要满足两个条件:任何人(或者任何 AI 代理)拿到它,都能按同样的标准执行同一套流程;不同时间执行时,输出格式和判定尺度保持一致。我设计 Hygiene-sweep 时,重点做了这几件事。
3.1 元信息与触发条件:让"什么时候该清扫"不再模糊
Skill 的元信息里,name和description决定了它何时被选中。我见过很多 skill 写不好 description,导致明明应该触发的场景里没有被触发,或者在无关场景里被强行套用。
我给 Hygiene-sweep 写 description 时,刻意描述了触发信号而不是功能说明。比如它会这样描述:"当代码变更完成、准备提交前;当调试日志和临时变量已无法靠肉眼追踪;当一个任务分支即将合入主分支却未做过清理,可用此 skill 执行六步代码卫生清扫。"这段描述没有使用"帮助开发者"这类空泛话术,而是列出了具体的场景状态,这样无论是我自己使用还是在 AI 代理环境中调度,都能精确匹配到正确的时机。
3.2 每一步的"清扫算子"标准化
六步流程不是六个模糊的目标,而是六组可执行的检查项。我把每一步都做成了"检查算子"——一个算子包含三要素:检查什么、怎么判定、误伤时怎么办。
举例来说,"回头扫死代码"这个目标本身没法执行,但"扫描当前变更文件范围中,所有未被引用的函数定义并全文搜索确认引用点"就是一个可执行的算子。每个算子我在 skill 里都写明了判定规则,避免执行时过度依赖个人直觉。
这里有价值的一点是:算子的粒度不是固定不变的。我在实际使用中发现,对不同语言、不同规模的项目,某些算子应该被启用或停用。比如一个动态语言项目,死代码判定的可信度低于静态语言,因为字符串动态调用的概率更高。所以我在 skill 里给每个算子加了"启用级别"——有些属于必扫项,有些属于建议扫项。这样遇到不同类型项目时,执行标准不会僵硬。
3.3 输出格式与清扫报告:让结果可验收
一个 skill 如果没有明确的输出格式,它的执行结果就很难验证。Hygiene-sweep 的输出不是"清扫完成"这四个字,而是一份清扫报告,每条报告项包含四个部分:
- 检查项所属步骤(对应于六步中的哪一步)。
- 扫描范围(涉及的代码文件或模块)。
- 发现的问题和判定依据。
- 处理建议(立即删除、标记待定、建议重构)。
这个设计对实际使用很有帮助。特别是当清扫范围比较大时,报告让我能快速区分"必须手动确认的事项"和"可以直接执行的事项"。比如一个空 catch 的修复建议是"补充日志或明确文档",而一段注释代码的修复建议是"删除",两者可信度完全不同。
另外,我也在 skill 里写了一条输出规范:清扫报告里必须列明所有被修改的文件。这是防止清扫扩散的关键——如果某个文件不在最初设定的变更范围内,但清扫过程中被改动了,报告必须明确指出,让我回去确认这次改动是否必要。
3.4 适配不同代码库的"配置面"
最后,我保留了 skill 的灵活性。不是每个项目都需要全部六步。我在 skill 里设计了一个简单的配置列表,核心是三个选项:
- 语言/框架类型(影响死代码判定和动态调用误伤概率)。
- 是否启用格式检查(无 lint 配置的项目默认停用)。
- 清扫范围(当前分支 diff、指定目录、全库)。
Hygiene-sweep 里写死的不是"清扫这个问题",而是"如何按照这个流程清扫"。措辞上我也刻意避开了"你应该做代码卫生"这类说服性语言,全部改成命令式的操作指令。毕竟这个 skill 的使用场景是"已经决定要清扫",而不是"要不要清扫"。
4. 实测记录:误删、误伤和被忽略的三条边界
Hygiene-sweep 从 1 月运行到现在,清扫的大小项目加起来超过了二十次。整体流程稳定,但过程中踩过几个印象深刻的坑,正好可以当成边界案例来说。
4.1 动态调用的误删:一个注释代码块引发的宕机
我第一次在真实项目里跑 Hygiene-sweep 时,清掉了一段注释掉的代码。那段代码看起来是旧的鉴权逻辑,注释里没有说明。我确认了一下当期分支上已经没有调用,就顺手删了。
结果当天晚上线上服务报了一个运行时错误,查了很久才发现,一个后台管理接口通过配置中心动态加载了一个处理器,处理器类名在配置里写死了,而那个类被引用的私有方法正好包含了一段解析逻辑——它不是直接调用,而是通过字符串拼接出的方法名在运行时触发。这个逻辑在代码库里没有任何直接调用关系,注释掉的部分恰好是那个解析方法的一个分支。
这个事故之后,我调整了第二步的执行规则:所有"注释掉的代码",如果没有清晰的保留原因,一律不是直接删除,而是先移动到单独的归档文件里,保留一个 commit 再决定删不删。后来发现这个规则也带来了一个隐藏收益——动手删除的"心理门槛"降低了,因为你知道随时可以恢复,所以清扫时反而更果断。
提示:对于间接引用(字符串调用、反射、SPI 机制、配置中心加载类),单靠代码库内的静态引用搜索判断"死代码"是不可靠的。涉及配置文件的标识符,我建议每一处都做一次"配置内容 vs 代码标识符"的交叉比对。
4.2 日志不是越少越好:把关键路径日志也扫掉的教训
有一段时间我把"扫日志"执行得太激进,把一些业务语义日志也删掉了。删的时候觉得"反正这段流程已经很稳定了"。结果后面排查一个订单状态不一致的问题时,我翻遍了所有日志都找不到当时这笔订单走到了哪个分支,最后只能加回日志重新复现,浪费了整整一个下午。
这件事让我给第三步加了一条明确的护栏:判定调试残留日志,不看日志级别,只看它是否承载业务状态语义。举个例子,如果一行日志是为了说明"订单从待支付变更到了已支付,操作者是某个用户,变更原因是某个回调",这行日志就属于业务日志——它是数个小时后排查问题的唯一线索。如果一行日志只是为了确认"代码执行到了第 87 行",这才是要清的调试残留。
4.3 格式化扩散:谈一谈"顺手改了一下"有多坑
代码卫生清扫里最难控制的不是删了什么,而是"顺手改了"什么。有一次我用 gofmt 跑完整个目录,发现改动了几十个文件,而真正的清扫对象只有三个文件。那次 PR 的 review 体验非常差——reviewer 根本分不清哪些改动是我有意为之,哪些只是格式化差异。
从此之后,我在 skill 里加了一条硬性规则:格式自动化工具只允许全库执行一次,且执行结果单独成一个 commit。在提交信息里写明"chore: 格式化调整",和功能清扫隔离开。这样如果后续 review 发现格式改动混进了功能 diff,可以快速 revert 掉那个格式 commit。
还有一个执行层的小坑:我在本地做清扫时,IDE 经常会因为自动 import 优化把一些原本正确的导入顺序打乱。这本身不是大问题,但会让 diff 变大。我的建议是清扫时临时关掉 IDE 的自动格式化选项,在做完所有修改之后手动触发格式化,不要让格式化在每次保存时自动进行。
4.4 清扫与测试的时序:先绿后扫,扫完再绿
我在多个项目上验证了一个稳定的顺序:先让测试基线变绿,再做清扫,清扫完成后立刻回归。如果在清扫中发现测试变红,先停下来判断是清扫改坏的还是基线本来就红。这个判断如果没有基线参考,会很耗时。我在第一次跑 Hygiene-sweep 时,就因为基线当时是红的而清扫后又出现了新的报错,花了不少时间才理清哪个失败是原有的、哪个是清扫导致的。
所以,基线记录不仅仅是"第一步"的仪式感,它是整个清扫流程的锚点。我甚至在 skill 里写死了一个规则:基线不绿,不进入第二步。如果一个项目目前处于构建失败的状态,"清扫"这件事应该等到项目恢复稳定后再做,否则清扫带来的改动会和已有问题纠缠在一起,谁也说不清。
5. 从清扫到防御:怎么把 Hygiene-sweep 塞进日常开发流程
单独跑一次 Hygiene-sweep 只是一个起点,真正受益的是把它变成日常开发的固定环节。我实践下来有三个比较自然的切入场景。
第一个场景是PR/MR 提交前的自检。在我自己提交代码之前,先在本地跑一遍 Hygiene-sweep,带着清扫报告去提交,可以避免很多 review 阶段的来回扯皮。尤其是调试日志和注释代码这类问题,如果等到 reviewer 提出来再改,会给人一种"你对代码不上心"的观感。及时把卫生处理好,review 的重点会自然聚焦在设计和逻辑上。
第二个场景是任务切换时的"桌面清理"。开发者的注意力是有限资源。当一个功能分支暂时挂起,另一个更紧急的需求插进来时,我会在挂起之前快速跑一遍 Hygiene-sweep。这样做有两个好处:一是回来时面对的是一个干净、可继续的状态,不需要重新理解自己之前写过的半成品;二是分支上残留的调试日志不会意外地进入合入流程,因为被紧急需求打断后,你很容易忘记之前为了排查问题加了一些临时输出。
第三个场景是CI 配合轻量卫生检查。不是所有代码卫生都可以交给自动化,但有一部分可以:死代码扫描、未使用依赖检查、调试日志检测,这些都可以通过现成的开源工具做成流水线的一部分。Hygiene-sweep 这个 skill 的价值不在于替代这些工具,而在于补充它们覆盖不了的部分——比如命名失序的识别、隐患风险的分级、清扫报告的组织。我目前在 CI 里加了两个轻量检查:一个检查是不允许新增console.log(测试文件除外),一个是不允许出现注释掉的代码块。这两条规则执行成本极低,却挡住了大量"顺手留下"的卫生问题。
说到底,我把 Hygiene-sweep 定位成了一套个人或团队内部的代码卫生协议。它不评判你写代码的水平如何,它只要求一件事:在代码从一个阶段流向下一个阶段之前,按固定的步骤让状态变得清晰可见。有了这套协议之后,我对"代码整洁"的理解也变了——不再是"漂亮的代码"这种模糊印象,而是"可快速理解、可放心修改、变更范围可追踪"的具体状态。