那天我面对一组自动化脚本,第一眼看到的是一个很容易让人紧张的数字:目录里有 72 个脚本,共 33,039 行,占用约 7.0MB。最长的一个脚本有 3,353 行,另一个也超过 2,500 行。只看这些数字,很容易得出一个看似专业的结论:代码太长了,应该马上拆。
但我没有立刻动手。因为“长”只是视觉上的问题,还没有说明它到底在哪里造成了损失。一个文件有 3,000 多行,并不代表这 3,000 多行会自动进入每次工作的上下文;真正昂贵的动作,是有人为了找一个函数,顺手把整个文件读了进去。于是这次的判断从“要不要拆文件”,转成了“什么东西真的在消耗注意力和预算”。
先确认长在哪里,别先动手术
我先盘点了脚本之间的关系。72 个脚本都有文档或引用关系,没有哪个是完全孤立、放着没人用的文件。重复度也没有想象中高,常见的连接操作只在少数位置出现,调试协议的辅助代码也没有大段复制粘贴。
这个结果很重要。它说明文件长,不等于职责混乱;文件多,也不等于存在一堆应该合并的重复实现。如果我只凭行数开始重构,可能是在解决一个没有被证明的病。更麻烦的是,命令入口、参数形式和已有测试都可能因此改变,最后得到一组“看起来更短”的文件,却丢掉了原来稳定的使用方式。
重构的起点不应该是文件超过了某个数字,而应该是已经确认它造成了具体的维护问题。
我后来把长脚本分成两个问题看。第一个问题是代码的组织:哪些职责互相依赖,哪些基础能力被多个地方重复实现,哪些部分可以独立测试。第二个问题是读取行为:调用方究竟是定位读取需要的片段,还是每次都整文件加载。这两个问题相关,但不是同一个问题,不能用拆文件一次性替代诊断。
真正昂贵的是整文件读取
代码本身是载荷,静静地放在脚本目录里,不会因为多了几百行就自动烧掉对话预算。只有当人或自动化工具把文件内容读进上下文时,它才会变成当下的成本。按当时的估算,最长脚本整文件读取一次,可能消耗约 40k 个上下文单位,足以吃掉相当大一部分工作预算。
这就解释了一个看似矛盾的现象:代码完全可以继续稳定运行,但协作过程已经变慢、变贵,也更容易让真正相关的内容被淹没。表面上是在抱怨脚本太长,实际上是在管理读取边界。
如果不处理这个边界,后面会出现一种很隐蔽的浪费:为了确认一个小函数,先加载几千行;加载之后又要在上下文里筛选相关部分;筛选过程中还可能把无关的实现误当成当前问题的前提。每次看起来只多做了一点,累积起来就会拖慢判断,还会增加误改风险。
所以我先设了一条简单的读取纪律:需要定位内容时用 grep 或 sed,需要了解命令能力时先看帮助信息,除非确实需要通读,否则不整文件读取。这不是限制阅读,而是把阅读从“先全部搬进来”改成“先问自己要找什么”。
用预算守卫代替行数焦虑
读取纪律解决的是当下的上下文成本,代码边界还需要一个长期的提醒。于是我加了一个文件大小预算:单个脚本默认不超过 2,000 行,超过预算就必须登记理由。当前确实有 2 个超预算文件,但它们都有明确的豁免说明,因此检查通过。
这里的关键不在于 2,000 这个数字多么神圣,而在于超出之后不能无声无息。一个文件变长,未必马上要拆;但它应该留下“为什么暂时不拆”的解释。这样,下一次有人准备继续往里加功能时,不必重新从感觉出发,也不会把历史上的临时决定误读成永久规则。
预算守卫不是强迫所有文件变短,而是让每一次变长都变成可解释的选择。
这和单纯的行数告警不同。告警只告诉我“超过了”;预算守卫还要求我回答“为什么超过、风险是什么、以后什么条件出现时再处理”。如果一个脚本拥有稳定入口、引用关系清楚、重复度低,并且当前没有明确的测试隔离收益,那么暂时保留长文件可以是理性的。反过来,如果它的职责已经无法描述,或者修改一处需要同时理解很多互不相关的状态,那就算只有 1,500 行,也可能值得拆。
最容易误判的错误捷径
最容易误判的错误捷径,就是看到“3,000 多行”就立即拆成很多薄包装。薄包装看起来很现代:一个命令一个文件,一个功能一个入口,好像以后增加功能会更快。但如果拆分只是把原来的编排逻辑搬来搬去,调用关系反而会变长,调试时要在更多文件之间跳转,原有命令契约和测试替身也可能失效。
我没有把主入口按命令硬切,是因为当时已有大量文档和测试在使用它,文件并不是无主的遗留物。后续确实发现了一些更适合独立成公共零件的能力,例如连接页面、打开数据库、读取会话记录、统一状态写法。这些能力被多个脚本借用,且部分调用方直接依赖了以下划线开头的内部名字。它们的问题不是“所在文件超过 3,000 行”,而是公共能力没有正式边界。
这也是判断拆分时最值得复用的一条线:如果代码是稳定的编排流程,就先保护入口;如果代码是多个调用方都需要、依赖方向单一、可以独立测试的叶子能力,就有理由抽出来。抽出来以后,正式公开名字可以承接新调用,旧名字保留兼容,避免一次重构让所有调用方同时迁移。
保留契约,拆出真正独立的零件
后续整理公共底座时,我更关注行为是否完全一致,而不是文件是否变短。行为一致的连接、数据库和状态处理可以合并;遇到异常兜底不同、环境变量为空时的回退规则不同,或者某个脚本还写死了旧数据目录,就先保留,不为了“统一”而强行抹平差异。
这里最重要的决策是:我选择保留稳定命令契约,是因为命令、参数和现有测试本身就是使用者已经依赖的接口。把内部实现移到公共底座,能够减少重复;把外部入口改掉,则可能把一次治理变成一轮迁移。两者的风险不在一个量级。
真正独立的模块还应该能被单独验证。后来统一自动化写入入口时,新增的写入闸门测试覆盖了新建、改名、改状态和软删等行为;改动前后,大部分测试结果保持一致,唯一持续失败的项目属于原有环境缺失,并非这次改动引入。这样的验证比“拆完以后目录看起来整齐”更能说明重构是否值得。
拆分的价值不在于把大文件切成小文件,而在于让一个边界清楚的能力可以独立调用、独立测试、独立承担变化。
如果暂时找不到这样的边界,就不要为了满足审美而拆。等下一次真的要修改某个叶子能力时,再沿着依赖关系移动它,通常比提前设计一套空壳目录更稳。代码结构应该回应真实的变化方向,而不是预演所有未来。
把读取、拆分和验证放进同一流程
这次之后,我给大型自动化代码形成了一套简单流程。第一步是盘点:看总量、最长文件、引用关系和重复实现,先判断问题是代码组织还是读取方式。第二步是分开管理:代码行数用预算守卫,协作上下文用定位读取,不能用一个指标代替另一个指标。
第三步是保护边界:保留现有命令、参数和被测试替身依赖的编排函数,先处理内部重复和不稳定的私有调用。第四步才是选择性拆分:只移动依赖单向、职责清楚、可以独立测试的叶子模块;行为不同的实现保留差异,并把暂不合并的原因写下来。第五步是验证:跑尺寸检查、公共入口测试和命令帮助检查,必要时用临时数据验证写入行为,确认真实数据没有被误改。
这套流程的好处是,拆不拆不再靠直觉投票。它把问题拆成了几个可以回答的小问题:谁在引用?重复在哪里?读取成本是多少?入口能不能保持?移动之后能不能单独证明行为没变?任何一个问题还没有答案,就说明现在更适合继续盘点,而不是急着切文件。
这件事让我重新意识到,维护大型脚本并不是和行数作战。行数只是地图上的等高线,真正需要治理的是信息如何被取用、能力如何被复用、变化如何被隔离。一个流程成熟的标志,不是目录看起来最整齐,而是下一次遇到具体问题时,能快速找到需要的那一小段,并且知道改动会影响到哪里。
下次我会怎么做
下次再遇到超过 3,000 行的自动化脚本,我会先停一下,不把“长”直接翻译成“该拆”。我会先查它被谁引用、有没有重复实现、整文件读取是否真的发生,以及当前最痛的维护问题是什么。如果只是阅读方式粗放,就先加读取纪律和预算守卫;如果是多个调用方依赖同一段基础能力,再考虑建立正式的公共入口。
我也会把“暂时不拆”写成有条件的判断,而不是一句含糊的保留意见:入口稳定、引用广泛、重复度低时先不动;职责独立、依赖单向、测试可隔离时再拆;行为不一致时不强行合并。这样,未来的维护者看到预算超标或公共底座时,能知道下一步该检查什么,而不是重新经历一遍争论。
最后,我会优先改善读取边界。先用定位工具找到函数、调用方和帮助信息,再决定是否需要阅读更大的范围。真正值得长期保留的,不是“这个大文件最后有没有被拆掉”,而是面对大文件时,我们已经有一套不靠焦虑做决定的方法。