1. Caveman 到底是什么:从一句玩笑到一种工程哲学
前阵子团队做代码评审,有位同事提交了一个状态管理方案,类名里带了三个设计模式,构造函数接了七个参数,还为了“将来可能的扩展”预留了两层抽象。我看完第一反应不是鼓掌,而是脑海里蹦出程序员圈子里那个经典比喻:如果一位穴居人(Caveman)路过看到这份代码,他能一眼看懂你在干什么吗?大概率不能。
这个比喻不是骂人,也不是调侃代码水平。它在圈子里流传了很久,演化出了“Caveman Debugging”“Caveman Code Review”这类说法,核心意思就一句话:代码复杂到一定程度,任何工具、文档、注释都救不了它,真正能救它的是让逻辑回到原始人也能看懂的简单状态。
我身边很多自嘲“老油条”的工程师,其实心里都有这套朴素的判断标准。代码审查时看一个函数,如果在毫无上下文的情况下、顺着函数体读一遍就能明白它做啥,那这代码大概率是好代码;要是读三遍还云里雾里,那问题往往不在你阅读理解能力上,而是在代码本身的复杂度上。
这篇文章就把我自己这些年践行“Caveman 标准”的经验摊开聊一聊。适合刚入行的新人理解什么叫“好的工程直觉”,也适合带团队的技术负责人拿去当评审的参考尺度。它会告诉你 Caveman 风格的调试、评审、重构具体怎么操作,还会聊聊这套标准在哪些地方管用、哪些地方千万别用。这不是让你退化成不用调试器、不写注释的野路子,恰恰相反,它是让你在眼花缭乱的技术方案里,找到最不容易出错的那一条路。
1.1 程序员嘴里的“Caveman”从来不是骂人
先把这个词的语境捋清楚。在英文技术社区里,“caveman”出现最多的两个场景:一个是Caveman Debugging,指的是不用调试器,只用打印语句(console.log / print / echo)逐段输出变量值,像原始人敲石头一样一点点排查问题;另一个是Caveman Code Review,指的是一种极其直接的评审态度——看不懂就说不懂,觉得绕就说绕,不跟你绕弯子。
这两种用法背后有一套共同价值观:相信直觉,相信直接的因果,警惕过度包装。打印调试虽然原始,但它把程序执行的每一步都暴露在你眼前,不存在断点没打对、条件没命中等干扰因素。直白评审虽然听着刺耳,但它在第一时间把“可读性风险”拉到了台面上,逼着作者重新审视自己的设计。
我在实际工作中发现,很多工程师对“caveman”的理解只停留在“低级”“原始”的贬义层面,这是没吃过复杂度的亏。一个系统在演进两年、接入十几个模块之后,你对“清晰”和“可维护”的判断就会越来越朴素——第一眼能看懂的东西,维护成本一定低;第一眼让你皱眉头的东西,以后一定会埋雷。这不是玄学,是有统计意义的工程经验。
1.2 三种最常见的“Caveman 语境”
把 Caveman 精神拆开看,日常工作中主要体现在三个层面,每个层面解决的问题不一样。
调试层面,指的是用最直白的方式定位问题。不搞复杂的监控埋点,不依赖花哨的调试工具,先打印关键节点的输入输出,用“控制变量法”缩小问题范围。等范围缩小到足够小,再决定要不要上断点调试。
评审层面,指的是用符号直觉做标准。看代码不是比谁的命名更优雅、谁的抽象更高级,而是看逻辑链是否短到“穴居人也能顺着读完”。评审者可以随时说“这地方我没看懂”,这句话不是能力问题,而是代码设计不合格的信号。
架构层面,指的是用“去掉能不能跑”来判断必要性。一个中间层、一个配置文件、一个抽象接口,如果去掉它系统照跑、需求照样能满足,那它现阶段就是多余的。Caveman 架构思想是“够用就行”,但不是“永远只做最简单的一版”,而是在复杂度真正出现之前不提前建设。
我把这三个层面整理成一个对照表,方便你快速抓住它们各自的职责:
| 场景 | Caveman 风格 | 核心理念 | 常见误区 |
|---|---|---|---|
| 调试 | 打印关键路径变量,逐步缩小范围 | 直击因果链 | 从头到尾无边界的打印 |
| 评审 | 看不懂就直接说看不懂 | 可读性是第一性 | 把“我没看懂”当耻辱 |
| 架构 | 能不加层就不加层,能不去抽象就不抽象 | 延迟复杂度 | 把不过度设计当成不设计 |
这套东西合起来,就是一套“用最早期的代价换取最大程度的清晰”的工程哲学。下面几节,我逐个讲实际操作,以及哪些坑我必须替你先踩一遍。
2. 为什么“越简单”在工程里反而越难做到
聊完了概念,说说更扎心的事。几乎所有人都认同“代码要简单清晰”,但真正写起来,大家还是不自觉地往复杂的方向走。这不是态度问题,是人性问题。
复杂度的第一来源是抽象冲动。程序员天然喜欢把相似的东西归成一个函数、抽出一个基类、封装成一个服务。这个冲动本身没错,错的是抽象的时机——很多抽象都是基于“将来可能有三种实现方式”的想象,而不是当前真实存在三种调用方。我见过太多团队,连需求都还没稳定,就已经把策略模式、工厂模式、适配器模式全部铺上了,结果未来没来,抽象先成了负担。
第二来源是对“专业感”的追求。说实话,写一个所有人都能看懂的 if-else,和写一个精心设计的策略模式,哪个会让你觉得更有成就感?我猜不少人会选后者。代码评审的时候,一份十行搞定问题的方案,和一份洋洋洒洒八十行的方案,后者往往更容易被表扬“有想法”。但真实情况是,那十行方案在三个月后依旧十行,那八十行方案可能已经成为团队里没人敢碰的“祖传代码”。
第三来源是盲目的“最佳实践”焦虑。技术社区每天都在生产新范式、新框架、新约定。今天说状态管理要用某种模式,明天说测试要全覆盖。这些建议本身没问题,但很多团队把这些“外部建议”直接套到完全不适配的项目里,产生一大堆为了满足规范而存在的样板代码,复杂度就这么堆上去了。
2.1 复杂度的代价比你想的更贵
这里说的复杂度不是指数学上的圈复杂度,而是指理解成本和维护成本。一份代码,写出来只花一天,但后面会被 20 个不同的人读、改、修 bug。这 20 个人每个人读一遍耗费的精力,最终都是要算进项目成本里的。
我用一个身边例子来说明。之前有个订单服务模块,为了“配置化”,把业务流程拆成了规则引擎、配置表和插件接口三层。初衷是业务调整时改配置就行,不用动代码。结果呢?第一个季度,业务调整确实只改了配置;到了第二个季度,业务需求表达不出来了,规则引擎表达能力有限,于是开始在配置里写脚本;第三个季度,配置内容比代码还难读,新来的同事接手时完全不知道一份订单是怎么流转的。后来我们做了一个大胆决定——把这三层拆掉,改回最原始的 if-else 状态机。代码量多了 60%,但每个人都能看懂,bug 修复时间直接降了一半。
这个案例不是反对配置化,而是提醒你:当你为“未来可能的变化”做设计时,必须先问一句——这个未来真的会来吗?如果不会,你为它付出的每一行设计,都是现在的债务。
2.2 新手的直觉是检验复杂度的最佳工具
我在团队里最常用的一招,是让刚入职的同事去读一段代码,然后看他能不能在半小时内说清楚这段代码的输入输出和大致流程。如果他说不清楚,我不是觉得他基础差,而是觉得代码复杂了。
这一招背后的道理很简单:新人的知识储备少,所以他能读懂,说明代码是符合人类直觉的;他读不懂,说明代码依赖了太多上下文、隐式约定或者抽象跳跃。有经验的工程师因为已经熟悉这套系统,反而读什么都觉得顺,慢慢就失去了对复杂度的敏感度。
所以我现在给自己定了一个规矩:写代码时假想读者是一个刚加入团队、完全不熟悉业务的新人。如果他在没有我口头讲解的情况下,按代码顺序读就能理解,那才允许提交。这比任何 lint 工具都管用。
2.3 简单和简陋的边界在哪里
Caveman 哲学最容易被误解的点,是把“简单”和“简陋”混为一谈。我见过有些人拿 Caveman 当借口,不写注释、不设错误处理、不做任何结构设计,出了问题就说“要实事求是、要原始直接”。这是把人家的底线当成了天花板。
我的判断标准是:简单是指“没有不必要的复杂度”,简陋是指“缺少必要的保障”。一个表单提交接口,直接拼接参数发请求,这叫简陋——没有错误处理、没有超时控制、没有状态反馈;而用十行代码写完校验、提交、异常提示,没有引入任何状态管理库,这就叫简单。
区分两者的试金石是“异常时刻的表现”。代码在正常流程上再怎么花哨,都不如它在出错时能不能快速暴露问题更能说明设计质量。Caveman 标准从来不是不处理异常,而是让异常处理路径和主流程一样直接、可读。
3. Caveman 调试实操:从打印到定位的完整思路
这一节说调试。为什么先从调试讲起?因为 Caveman 哲学最早就是从调试场景里长出来的,而且调试技巧是所有工程师每天都要用的,讲起来最有实感。
我先把态度摆出来:我不是反对调试器,我是反对“一上来就开调试器”。调试器是有成本的电钻,打印是免费的肉眼检查。很多问题根本不需要钻墙,你趴在墙上看一眼裂缝走向就知道问题在哪了。用调试器很容易陷入“单步跟踪两小时,最后发现是变量名拼错了”的搞笑局面。
3.1 打印位置的挑选技巧
常见错误是到处打印,print 打满全屏,然后看着一堆日志发呆。正确的做法是先缩小范围,再打印关键节点。
我通常的定位思路分三步:
- 确定“问题表现层”。界面报错了?接口返回值不对?还是数据持久化之后丢了?先明确你观察到的结果属于哪一层。
- 倒推“数据链条”。从问题表现层往回推,数据从哪个函数来,经过哪些变换,最终写到了哪里。每次倒推一个环节,就是一次二分。
- 在链条的分叉点打印。不要在每一行都打印,只在数据流分叉的地方——比如 if/else 的两条分支、函数调用前后、循环的首尾——打印输入和输出。
举个例子,有一次前端同事反馈说某个表单提交后,后端收到的是乱码。我没有跑接口调试,直接在接收请求的 Controller 入口打印原始字符串,发现数据是对的;再在过滤器和转换器后面打印,发现第二步之后字符串就变了;两步定位到编码转换写死成了某一种字符集。整个排查过程五分钟,全是因为打印位置选得好。
3.2 用“控制变量法”替代“猜”
调试时最容易犯的错是“看着代码猜”。看到某个变量可疑,就顺手改一下试试;发现问题还在,再改下一个。这不是调试,是占卜。
控制变量法的核心是一次只验证一条因果。你说这个函数返回值不对,那先验证它收到的参数到底对不对;参数没问题,再验证它内部的中间计算结果;内部计算也没问题,再往后看调用方是否错误使用了返回值。每一步都只动一个变量,不做跳跃式假设。
Caveman 调试其实一点都不原始,它是严格的科学方法,只是工具用了打印这种“原始”方式。很多时候,console.log 比 debugger 更能逼着你把因果链捋清楚,因为你是在用肉眼做证据链推理,而不是机械地单步执行。
3.3 什么时候必须切到调试器
打印不是万能的。我总结了几种打印解决不了、必须老老实实上调试器的场景:
- 循环中的复杂状态变化。循环几百次,每次状态都在变,打印会刷屏,你根本看不出规律,这时候用调试器的条件断点,在某个状态值满足条件时中断,效率高十倍。
- 并发/异步竞态问题。多个回调交叉执行,打印的顺序和实际执行顺序不一定一致,你需要断点在特定时序上观察调用栈。
- 原生层或第三方库内部问题。你怀疑问题出在某个你控制不了的库里面,这时候一步步入库内调试是最实在的。
我的建议是两条腿走路:先用几处打印把问题范围固定下来,再在嫌疑最大的那一段用调试器看细节。结合起来,比任何单一路线都快。
4. Caveman 评审实战:让“看不懂”成为标准
接下来这部分,是我最希望每个团队都能认真对待的——Caveman Code Review。
先描述一个理想场景:你在 GitHub/GitLab 上打开一个 Merge Request,不需要跑起来,不需要看设计文档,顺着代码列表从上往下读,每个文件的改动你能马上说出“这段是加校验、那段是改文案、这个函数是为了处理某种边界”。你看完第一遍,基本就能判断这个 MR 整体逻辑是否合理。这种阅读体验,就是 Caveman 评审追求的标准。
如果反过来了——你读了三遍还搞不清作者为什么要加这个文件、那个函数什么时候被调用、状态到底怎么流转的——那问题通常不是你的问题,而是这个 MR 本身的表达出了问题。
4.1 评审时的“三步提问法”
在评审时面对一份看不懂的 MR,我一般会按顺序问自己三个问题:
- 如果我是第一次见到这段代码,我能猜到它是干嘛的吗?这一步考察命名和结构是否直观。
- 这个 MR 的核心改动是什么?去掉那些辅助的封装和抽象,主路径你看得清吗?这一步考察是否有过度设计。
- 新来的同事照着这个 MR 能顺利完成开发吗?他需要问多少问题才能补齐上下文?这一步考察隐性知识的依赖程度。
这三个问题只要有任意一个回答不了,我的评审意见就会倾向于让作者重构,而不是让他补充文档。文档能补的是知识,补不了理解阻力。
这里要补充一个很重要的实操心得:评审时别急着去评论“命名不好”“缺少测试”这种表面问题,先往最核心的那个判断走——代码能不能被快速读懂。读不懂是根问题,命名、测试、注释都是派生问题。根问题不解决,派生问题修得再多,读起来还是费劲。
4.2 如何用“原语化”标准审视抽象
我经常在评审中问作者一句话:“这个抽象/服务/类,目前真的只有一种用法吗?”
如果答案是从头到尾只有一处调用,那就没有抽象的必要。抽象是有成本的,它把直接的逻辑变得间接,把明确的调用变得需要查找。如果一个抽象只服务一个场景,那么这个间接层带来的只有障碍。
但这里要小心,我不是说不能为未来做准备。我的建议是把“未来”写清楚,而不是用代码挡住现在。如果作者说“我预计将来必须支持两种渠道”,那就把这个预期写进 MR 描述里,而不是在当前代码里铺开一大堆通道接口。将来真的来了,再把接口拆出来也不迟,而且到那时你对实际需求的理解会更准确,设计出的接口也更贴切。
4.3 评审是给作者看的,不是给未来的自己看的
这一点特别想强调。评审意见的最终价值,是让作者在接下来的修改里得到成长,而不是让这份 MR 看起来像一篇论文。所以我的评审风格一贯是“直接说”的:哪里看不懂直接指出,哪里绕直接点破。不寒暄、不铺垫、不绕弯子。
很多年轻工程师刚开始不适应,觉得这种评审太凶。但实际上,这是对彼此时间最大的尊重——作者不需要猜“评审者到底啥意思”,评审者也不需要躲在礼貌用语背后。我的经验是,Caveman 风格的评审意见,沟通成本最低,修改到位的速度最快。前提是意见本身有依据,而不是情绪化吐槽。
5. 架构层面的 Caveman:延迟复杂度的时机与方法
从调试和评审往前再走一步,就是架构层面的决策。架构层面最容易出现“提前复杂度”,所以要单独拿出来说。
5.1 判断“有没有必要加层”的三个条件
我写代码之前会问自己三个问题,全都能给出肯定答案,才允许自己往架构上加层:
- 当前是否有两个或以上的真实调用方?只有一个调用方,就别抽象成接口。真实调用方远胜于想象中的调用方。
- 是否需要支持一种当前还不存在的扩展方式?如果业务上完全说不清未来要扩展成什么样,那这个扩展方式就是幻觉。幻觉支撑起来的架构就是空中楼阁。
- 去掉这层之后,需求是否能继续满足?如果能,说明这层是“锦上添花”,那就先别加,等需求真的缺它再补。
这三个条件看起来特别简单,但执行起来极其反直觉。因为大多数工程师加层的时候,不是为了当前需求加,而是为了“以后方便”。只要把这个冲动按住,架构就自然简单了。
5.2 技术栈和中间件同样适用“用得上再上”
架构复杂度不完全在代码层,更多时候在基础设施层。缓存、消息队列、NoSQL、监控系统、容器编排……每一个中间件,都是复杂度源。
我用一个很现实的场景说明:某个功能查数据比较慢,第一反应是上 Redis 缓存。但数据慢的原因还没查,Cache Aside 模式、缓存过期策略、穿透问题,直接给自己封装了三层。后来发现慢是因为 SQL 少了一个索引,加个索引 5 毫秒搞定,Redis 那套设计和部署浪费了一周。
正确的思路是:先用最直接的手段解决当前的问题(加索引、优化查询、合并请求),让系统继续跑;当最直接的方案已经解决不了性能瓶颈时,再引入中间件。这样既能最快见效,也避免了一堆未来才用得上的复杂度。
5.3 大系统里如何保持“局部 Caveman”
有人会说,你说的这套在大系统里根本走不通。我一个微服务有几十个依赖,怎么 Caveman?
我的回答是:Caveman 标准本来就该在局部执行,不是全局。一个系统整体复杂是正常的,但任何一个局部单元——一个模块、一个服务、一个函数——都可以做到足够直白。复杂度应该被控制在边界内部,而不是在每个单元内部都均匀铺开。
比如网关层可以做复杂的路由转发、鉴权过滤,但这不影响背后某个业务模块保持简单的 if-else。高复杂度中间件和低复杂度业务代码可以共存,关键是让每个单元的“内聚”和“单一职责”足够清晰。你可以把家里的水电管道修得错综复杂,但每个水龙头打开都必须出水——一个道理。
6. 四个最容易翻车的“伪 Caveman”坑
这几年我见过太多 Caveman 哲学被滥用,所以专门整理一节翻车记录,希望你不要在同一个地方摔跤。
6.1 把“不用调试器”当成信仰
第一坑。有人听完“打印调试”,回去就以后再也不开调试器了,遇到问题就打日志。我再说一遍:Caveman 不是信仰,打印是手段不是目的。你真正要的是快速定位问题的因果链,用什么工具取决于问题形态。该用调试器的时候吝啬,和该用打印的地方炫耀调试器一样,都是工具论上的偏执。
6.2 把“不写注释”当成极简
第二个坑很隐蔽:有人觉得 Caveman 风格就是代码越少越好、注释能省则省,然后把函数名写得天花乱坠来代替注释。结果就是:读代码像读外文小说,每个词都认识,但句子组合起来的逻辑就是不懂。
Caveman 精神是减少无关的复杂度,不代表连必要的说明都要省。好的注释不是解释“代码在做什么”(代码本身就说明了),而是说明“为什么这样做”,尤其是那些有明显备选方案、但选了这条特别路线的场景。这种注释恰恰是 Caveman 精神的体现——因为你代替未来的读者省下了查历史记录的时间。
6.3 为了原始而牺牲性能与正确性
第三个坑是拿简单当借口,放弃性能优化和边界处理。我用一个案例说明:某段逻辑通过双层循环暴力求解,几百条数据跑起来没问题,但上线到了十万条,直接超时。这时候最简单的代码就变成了性能事故。
正确的做法是:逻辑表达上保持简单,但性能瓶颈处要有意识地处理。简单和性能不冲突——你完全可以用一个清晰的哈希表替代双层循环,代码依然简单,复杂度却降下来了。简单不是傻,不是蛮干。
6.4 拒绝学习新工具的“伪 Caveman”
第四坑,也是我觉得最值得警惕的:把“不学新东西”包装成 Caveman。
有些人会拿“保持简单”当挡箭牌,拒绝学习容器、拒绝 CI/CD、拒绝新的语言特性。这不是 Caveman,这是僵化。真正的 Caveman 是判断清楚工具和问题是否匹配,而不是拒绝一切工具。如果一个工具能显著降低某类问题的复杂度,那学会它就是简单化的一部分。
这几类伪 Caveman 让人最头疼的地方在于,他们很难被说服,因为他们每个行为都披着“极简主义”的外衣。我的应对方式是:直接用结果验证。对比一下同样需求下不同方案的实际维护成本,三周后回访,谁是真简单谁是假装简单,一目了然。
7. 到底什么时候该“Caveman”,什么时候必须上工程化
聊到最后,必须把适用范围说清楚。没有任何一种方法论是万能药,Caveman 标准也不例外。
7.1 适合 Caveman 的场景
- 小型工具脚本、一次性迁移、原型验证:这些场景的生命周期短,或者受众极小,维护成本天然低,过度设计是纯浪费。
- 业务核心路径的代码:订单流程、支付流程、权限校验,这些地方的正确性比“优雅”重要一万倍。让核心路径保持直白,比任何模式都重要。
- 需要多人长期维护的公共函数/组件:公共代码的读者是最多的,可读性优先级最高。
7.2 必须适度使用工程化的场景
- 多人并行开发的超大型系统:强约定、强规范、强分层不是过度设计,而是协调成本的一部分。
- 涉及资金、合规、安全的领域:审计、追踪、精细的权限模型都是必要的复杂度,这时候为了 Caveman 而砍掉保障,等于裸奔。
- 高频变化的业务域:如果业务平均每两周变一次方向,那么适度的配置化、模块化就是合理的,因为它是为了“当前正在发生”的变化,不是为了想象中的变化。
关键在于区分“必要的保障”和“过度的预备”。判断依据我已经重复过无数次了——看看这个复杂度是为当前真实的约束服务,还是为想象中的未来服务。
7.3 如何温和地在一个团队里推行 Caveman 标准
我最后分享一点推动团队向 Caveman 靠拢的经验。
第一步,先建立“理解门槛”的共识。别急着定一堆代码规范,先让全组认同一个原则:代码的第一读者不是机器,而是三个月后的自己和新同事。这个共识建立起来以后,很多具体决策就简单了。
第二步,用正反案例做评审培训。我在组里组织过两次“最差代码大赛”,专门把那些过度设计的代码翻出来,当众讲它为什么难懂、怎么样改才更直接。这种做法比抽象地说“要写简单代码”有效得多。
第三步,评审时态度要坚决,但说话要具体。不说“这样写太复杂”,要说“这个抽象只有一个调用方,把它改成直接调用能减少至少三十行的间接层”。给具体的操作方向,别让作者自己猜。
最后分享一点个人体会
这几年执行 Caveman 标准,最大的变化反而不是代码风格,而是我在技术方案面前越来越有耐心。遇到新的框架、新的范式,第一反应从“好高级,我们也要上”变成了“它能解决我们哪个具体问题,值不值得为此改变现在的写法”。技术选型的决策速度快了很多,因为判断标准变清晰了:不解决当前真实问题的复杂度,统统不要。
我组里的新人仍然会花很长时间去适应这种风格。一开始他们觉得“这样写会不会太简单了?会不会显得我很不专业?”后来他们上了线,看了维护成本,就再也回不去了——明明几处普通函数能解决的问题,非要在那里凭空设计一个抽象层,不仅累,而且没必要。
如果你也想在团队里试试这套标准,我个人最推荐的做法是:下一次评审轮到自己的代码时,先按照“假设自己完全忘了这段逻辑,还能不能顺着读下来”的标准重构一遍。你会发现,这比让别人来指出问题要轻松得多。原始的评判标准不吵也不闹,它就静静地待在那里,等着验收每一行代码。