穴居人代码自救指南:从命名到重构的工程实践
发布时间:2026/10/29 15:30:43 锦皓数字建站

你有没有见过这种代码变量名全是一两个字母判断条件里躺着好几个光秃秃的数字if 套 if 三层往上改一个分支都要顺着逻辑捋半天。如果有恭喜你遇到了传说中的 caveman 代码——业内常叫它穴居人代码。这篇东西就想正经聊聊 caveman 代码长什么样、为什么大家都会不知不觉写成这样、以及怎么把自己和团队的代码一点点从“洞里”搬出来。适合被自己三个月前代码吓到的朋友也适合带新人、想给团队立规范的同学。1. caveman 式编程的常见画像1.1 第一眼就知道这段代码有穴居人味道caveman 代码最典型的特征是“命名丢失”。变量名喜欢用a、b、tmp、flag、data2函数名动不动就是handle、doSomething、check。你光看函数签名完全猜不到它负责什么必须点进去逐行读才能懂。更难受的是读懂了也不敢改因为你不知道哪些状态逻辑是在这里耦合的。第二个典型特征是“魔法数字满天飞”。比如状态判断直接写if (status 3)错误返回用return -99。这种代码写的时候当然没问题因为写代码的人脑子里知道 3 代表“已退款”、-99 代表“参数错误”。但这份记忆只存在于那一刻。三个月后回头再看谁都得对着数字猜半天。我当时接手过一套老系统里面1在不同模块里分别代表“启用”“已支付”“管理员权限”三种意思排查问题全靠上下文强行推理。第三个特征不太容易描述但我相信所有人都有体感结构上“一锅粥”。一个函数动辄一两百行支付、退款、库存判断、消息推送全部揉在一起循环里嵌套判断、判断里再套循环。想单独测试其中某一段做不到因为所有依赖都硬编码在函数内部。说句公道话caveman 代码和代码水平的关系没大家想象得那么大。我见过写了十年业务的老手顺手写出来的 util 函数照样是getData()配return obj结尾。这不是能力问题是状态问题。1.2 为什么会退化成 caveman 风格没人天生想写烂代码caveman 风格的形成基本逃不过这么几个原因。排第一的是长期赶排期。需求一个接一个产品经理站在背后催大脑自然而然切进“先跑通再说”的模式。浏览器里能出结果了就觉得任务完成了。至于变量命名清不清楚、分支结构好不好改那是“以后”的事。问题是这个“以后”基本不会来于是代码就这么沉淀下来。单兵作战也是重要催化因素。没有 Code Review 压力的时候人很容易对自己写出的一坨逻辑产生一种“我懂就行”的错觉。写给自己看的代码确实没必要绕那些弯子——但问题是你不可能永远是这份代码唯一的读者。团队里任何一个人接手这套代码都要为你的“顺手”买单。还有一种情况是复制粘贴式扩散。一个模块用一个不规范的写法实现了功能下一个人看图模仿下一个模块同样风格再后来整片代码库都是同一个调调。最要命的是这些坏味道还会传染新人新同事入职后看老代码都是这样写的自然以为这就是团队约定俗成的标准。1.3 别急着否定caveman 阶段其实是必经之路聊到这儿我得给 caveman 代码正个名。对于刚入行或者刚接触一段新业务的人来说能把功能写出来跑通本身就是一种能力。认知负荷是有限的新手阶段脑子里塞满了语法、框架、接口文档确实分配不出多余的精力去打磨代码结构。我经常和团队里的小朋友说一句话第一次写出 caveman 代码不是什么丢人的事丢人的是写完之后觉得“挺好”然后心安理得把它留在那。换句话说关键转折点是意识到“代码能跑”和“代码可以维护”是两码事。很多资深工程师回头看自己早期代码都会脸红但正是曾经脸红过才说明后来在进步。2. 从 caveman 到现代开发者基本功重建2.1 命名变量名就是最便宜的文档想摆脱 caveman 状态第一步永远是命名。别小看这件事把a改成order、把flag改成hasValidCoupon读代码的体验会有质的改变。改名的成本极低收益却立竿见影尤其适合作为重构练习的起点。命名有几个很实用的原则。布尔变量用is、has、can开头一看就知道是状态位。数量词放进名字里比如userCount而不是num。动词打头的函数名代表动作比如buildOrderPayload、validateStock。如果名字里需要写注释来解释那说明这个名字没起好。我见过很多人给变量取名result然后在不同的代码块里反复复用。这里其实藏着隐患变量复用通常会掩盖中间状态的含义调试时都不知道这个result到底属于哪一步。给它拆成stockResult、priceResult不光是可读性变好出错率也会降下来。2.2 函数把长流程拆成看得懂的故事caveman 代码的核心病灶在函数长度上。一本正经的建议是单个函数控制在 20 到 30 行以内做得住一件事。这个数字不是拍脑袋定的人是没法在大脑里同时装太多上下文的函数一旦超过一屏阅读它就需要来回滚动心智能量全耗在“刚才那个变量是什么”上了。拆函数我推荐一个技巧把代码当成一个故事来分段落。先看整段逻辑有哪几个“环节”每个环节抽出来就是一个子函数。比如“提交订单”这个动作背后其实是“校验商品库存”“计算最终价格”“生成支付单”“更新订单状态”四个环节每个环节独立一个函数主流程就变成了一行一行念下来很通顺的步骤。拆函数最大的阻碍其实是心理上的总觉得多搞出一个函数会不会影响性能、代码会不会变太多。实际上现代编译器对函数调用的优化已经很成熟更重要的是拆出来的函数可以被单独测试、单独理解调试效率翻倍。2.3 注释写为什么不写是什么很多初学者写注释喜欢描述“做了什么”比如// 循环遍历商品列表。这种注释纯粹浪费字符因为代码本身已经说明了一切。真正有价值的注释只有一种就是解释“代码这么写背后的动机和限制”。比如// 这里不能直接删库存必须等支付回调通知防止支付失败后库存被扣就是一条好注释。它告诉未来的维护者这个看起来奇怪的顺序其实是业务约束千万别随手“优化”掉。我有个习惯先尝试用代码表达表达不清楚的才写注释。如果代码写得很自明注释就完全没必要存在。反过来如果发现了为什么要这么写的原因优先考虑用代码或命名传达实在传达不了再落成注释。2.4 从“看着能跑”到“测着能跑”防御式编程caveman 代码往往建立在“用户不会乱来”的假设上。比如直接o.user.vip完全不考虑user可能是空的情况再比如外部传入的数组不做判空上来就遍历。真实线上环境永远比想象中残酷用户操作、上游接口、配置变更都可能带来意想不到的输入。防御式编程不是让你每个地方都写十层判空而是对“外部边界”保持警惕。数据从外部接口来该做的非空校验、类型校验、边界值校验都要有。但数据经过内部函数传递只要在入口处校验过一次内部就可以放心使用。这一点对重构旧代码尤其重要。很多人不敢重构就是因为老代码里没有边界保护改一处动不动就炸。补上防御式编程这一课之后代码的可维护性会肉眼可见地提升因为你敢改了。3. 出洞之后工程实践帮你守住下限3.1 格式化与 Lint机器先替你把关靠自觉维持代码风格基本等于靠意志力跑步减肥短期有效长期白搭。真正可靠的做法是把检查交给机器。格式化工具负责统一风格Lint 工具负责识别“危险代码”。它们不用讨论、不用评审提交代码的瞬间就能给出硬性反馈。前端圈里最常见的组合是 ESLint 加 PrettierJava 那边有 Checkstyle 加 SpotBugsPython 世界是 Ruff 加 mypy选你自己语言生态里最流行那套就行。框架代码里加一个npm run lint配合 Git 的 pre-commit 钩子代码提交前先跑一轮检查有红线的直接挡住。这类工具落地的时候有一个特别影响体验的点老项目首次接入会爆出一堆历史问题。我的建议是分步走第一轮先把问题逐一清理成绿灯状态然后把检查配置合并进主分支。从那之后每一天的代码都保持绿灯别喊着“这次先跳过下次再改”因为没有下次。3.2 单元测试给重构系上安全带没有测试的重构就是高空作业不系安全绳。你改完代码发现功能还在就敢合代码这不叫胆大这叫赌运气。尤其面对老代码测试的意义不在于覆盖率数字而在于给你一个稳定的“行为基线”。我的实操经验是重构前先写行为测试。不了解内部实现没关系只测输入输出、正常流程和异常流程。把当前代码的行为“钉死”之后再做结构调整跑一遍测试如果全绿说明行为没变重构大概率没引入大问题。这个过程叫“特征测试”是改造老代码的第一板斧。补测试这件事最大障碍是旧代码耦合太深根本没办法单独实例化。我的办法是老项目里先用“冒烟测试”把主流程盖住再慢慢把大的函数拆开拆到可以逐层注入依赖、写细粒度单元测试的程度。能测一点是一点测过的代码就是敢动的代码。3.3 Code Review让另一个人类当你的“镜子”机器能检查风格问题但检查不了“这个函数是不是太长”“这两个模块的职责是不是糊在一起”这类抽象判断能干的只有人。Code Review 是摆脱 caveman 状态最有效的团队机制本质上是让另一个人来读你的代码帮你看见自己看不见的坏习惯。一份有效的 Review 清单通常包括几项命名是否表意准确函数是否过长、能否继续拆分有没有新增魔法数字边界和异常分支有没有覆盖有没有明显的重复代码测试用例是否覆盖关键路径。做 Review 最怕流于形式。别在群里甩一句“LGTM”就算完事也别逐案辩论命名风格浪费时间。重点放在“这段代码三个月后回头看我们还能不能秒懂”上。刚开始推行时大家可能不习惯但坚持一段时间团队平均代码质量会被拉高一大截。3.4 渐进式重构老项目里怎么出洞手里维护一个老系统想一次性推倒重来基本不现实。渐进式重构是更务实的路线原则是“每次动一点动完就能跑随时能上线”。我把这个过程总结成四步选中目标模块、用测试保护行为、结构拆分、观察结果。先挑一个小而有代表性的模块下手别上来就碰最复杂的支付中枢。第一步在模块边界写测试第二步在绿灯下把长函数拆短、把命名改清楚、把魔法常量提进枚举第三步持续跑测试确保每一步都没有破坏原行为。这样一个小模块打完样其他模块照方抓药风险低、效果好。实际操作中我还会特别在意一件事每个重构步骤尽量独立提交。这样某个步骤出了问题git bisect能快速锁定是哪一次改动引入的。把格式化、改名、行为修改混在一个 commit 里出问题想回退都很难办。4. 实操演示我亲手重构一段 caveman 代码4.1 原代码以及我列出的问题清单先给大家看一段从老项目里简化出来的典型 caveman 代码业务背景是电商订单模块的支付和退款流程function handle(o, a) { if (o.status 0 a 1) { if (o.price 0 o.stock 0) { let t o.price; if (o.user.vip true) { t o.price * 0.8; } o.total t; o.status 1; return t; } else { return -1; } } if (o.status 1 a 2) { o.status 3; return 0; } return -99; }这段代码我拿到手第一反应就是头疼。问题清单列出来更头疼handle看不出在干嘛o、a不解释谁知道一个是订单一个是动作状态和动作全部用魔法数字1 到底是“已支付”还是“支付动作”全靠上下文猜折扣计算、价格计算揉在流程里o.user.vip存在空指针隐患返回值 -1、-99 跟密码似的正常人看不懂。4.2 第一步用常量替代数字让状态开口说话重构的第一步不是改结构而是先给所有数字一个名字。这里定义的常量就很直观const ORDER_STATUS { PENDING: 0, // 待支付 PAID: 1, // 已支付 REFUNDING: 2, // 退款中 REFUNDED: 3 // 已退款 }; const ORDER_ACTION { PAY: 1, REFUND: 2 }; const ORDER_ERROR { GOODS_UNAVAILABLE: -1, INVALID_REQUEST: -99 };这一步不改变任何执行逻辑只把“语义”还给了代码。跑一遍测试全绿说明数字被换成常量名之后一切照旧。千万别觉得这一步多此一举后面所有重构都要建立在对语义清晰理解的基础上。4.3 第二步拆函数把职责分清楚原函数最大的问题是一个函数同时处理支付和退款两条业务线。把它们拆成两个独立函数主流程直接清楚了一半。拆的时候我用了一个习惯动作先给支付流程写行为测试覆盖正常支付、VIP 折扣、库存不足、状态不正确四种情况然后开始拆。动作对应的代码被搬到独立的payOrder函数里退款挪到refundOrder。拆分后的两个函数在语义上各自独立以后要加“订单取消”之类的新动作也不用再往这个越来越臃肿的大函数里塞逻辑。4.4 第三步卫语句替换嵌套逻辑线摊平原来的代码里第一层 if 把整体包住里面还有第二层嵌套阅读的时候必须匹配一对又一对大括号。我用卫语句把非核心分支提前返回把真正的主流程留到最后平铺出来function payOrder(order) { if (order.status ! ORDER_STATUS.PENDING) { return ORDER_ERROR.INVALID_REQUEST; } if (order.price 0 || order.stock 0) { return ORDER_ERROR.GOODS_UNAVAILABLE; } const finalPrice calculateFinalPrice(order); order.total finalPrice; order.status ORDER_STATUS.PAID; return finalPrice; }卫语句思路其实很像生活里的“排雷”先把所有不满足条件的请求挡掉剩下能走完全流程的一定是合法请求。主流程的每一行代码都是有效动作没有嵌套不需要大脑去模拟“这个 else 到底跟哪个 if”。4.5 第四步抽出小函数补上空值保护价格计算这件事我抽成了独立的calculateFinalPrice同时把潜在的空引用问题修掉function calculateFinalPrice(order) { const isVip order.user ! null order.user.vip true; return isVip ? order.price * 0.8 : order.price; } function refundOrder(order) { if (order.status ! ORDER_STATUS.PAID) { return ORDER_ERROR.INVALID_REQUEST; } order.status ORDER_STATUS.REFUNDED; return 0; }走到这一步整个重构后的代码已经面目一新。同样的业务逻辑原来的版本需要读三遍才能明白现在扫一眼就知道先检查状态再检查商品可用性然后算价格最后改状态。每一行都在说人话。我要特意强调一个原则重构时尽量不顺手改行为。原代码o.user.vip没判空新代码补了判空这属于 bug 修复严格来说和“结构调整”不是一回事。我个人的处理方式是重构期间只做结构调整发现明显 bug 就单独记下来、单独提交别和重构混在一起。这样出了问题回溯起来极其方便。5. 常见问题与排查技巧实录5.1 我实践中遇到的典型问题速查表光讲方法不讲坑等于菜谱里不放盐。把我在各种新旧项目中反复遇到的问题整理一份速查表都是真实踩过的。症状核心触因我的应对思路看到老代码完全不敢动没有测试积累改错没人兜底先写行为测试钉住当前表现再小步重构团队没人重视代码规范缺少共识和强力工具支撑拿一个重构成样板用结果说话而非讲道理重构到一半被新需求打断一次改动范围太大停不下来拆成多个可独立提交的小步骤随时能安全切换用工具全库批量格式化想省人力但对 diff 造成灾难分模块做每个模块的改动独立合并一补测试就发现旧代码全是 bug旧逻辑本身就不可靠分清“既有行为”和“正确行为”先锁后改关于最后一条我多说两句。补测试时发现旧代码的行为本身就有问题这时候先别手痒去改。先把当前行为用测试记录成“既有行为”单独写一张清单标记可疑 bug等结构重构完毕再决定要不要修。如果你重构一开始就顺手改逻辑测试一旦失败根本分不清是改坏了还是原来就错。5.2 踩过几次坑之后我总结的避坑经验第一格式化跟重构不要混在同一个提交里。我刚带团队的时候有同事图省事把代码格式化、变量重命名、逻辑调整全部塞进一次提交review 的时候看 diff 看了整整四十多分钟因为格式化造成的改动淹没了真正的逻辑变化。从那以后我立了个规矩每个改动只做一类事提交信息里写清楚“refactor: extract calculatePrice”和“style: format with prettier”是两种不同类型的变更。第二变量改名一定用 IDE 的重命名功能别用全局搜索替换。手工搜替换极容易漏掉重名变量尤其当项目里有同名不同作用域的变量时很容易改出一个静默 bug。工具重构的好处是它能识别变量的作用域边界该改则改不该改的不碰。第三用大模型辅助重构效率高但必须人工 review 逻辑等价性。我自己现在也会让 AI 帮我做初步拆分但每一步都会重新读一遍确认没有把某个分支条件悄悄吃掉。AI 生成的分支合并、条件简化挺聪明但聪明的东西有时候是最危险的因为它看起来天衣无缝。第四重构不要顺手“加功能”。重构期间最怕的事就是有人路过说“顺便帮我加一个优惠券字段”。每加一个需求你还要兼顾新旧逻辑共存重构的每一步都必须重新验证很容易顾此失彼。我一般挂在嘴边的一句话是重构归重构需求归需求混在一起出事了没人分得清责任。结尾一个老程序员对 caveman 的最后一点唠叨说到这我还真想分享最后一次翻车经历。前年我接手一个老项目随手改了一行“看起来没毛病”的判断条件结果因为上游把状态值含义换过导致线上支付订单全部卡在中间状态。那次事故之后我的习惯就变成了铁律任何老代码的行为测试永远优先于任何重构冲动。如果你现在手里就有一堆让自己头疼的 caveman 代码我的建议不是马上开干而是先挑一个最边缘的小模块给它写行为测试、改命名、拆函数把整套流程走一遍。等你尝到“重构完能秒懂”的甜头自然会想碰下一个模块。我记得自己是从一个二十行的工具函数开始的那时候只是想把tmp改成有意义的名字没想到后来把整片核心业务都翻新了一遍。最后再送一个小习惯以后每次提交代码先花三十秒看一遍自己的 diff想象一下十天后的自己看到这段代码什么感受。被别人 review 之前先自我 review这个习惯能过滤掉至少一半的 caveman 味。能跑不是终点能看懂才是。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。