代码可读性修炼:为什么说学好语文才能写好代码,从命名到注释的工程实践
代码写得多了我越发认同一个看起来有点“反直觉”的说法学好语文才能写好代码。我第一次听到这话是在某家大型支付平台的一次内部技术分享上讲这句话的是一位研究员级别的老前辈。当时台下不少人当段子听我倒是越想越觉得有道理。事情得从一段让我刻骨铭心的代码说起。我转正之后接手的第一个模块来自一位已经离职的同事。代码里大量出现a1、b2、tmp、data_list这类命名注释只有三行其中两行写着“这里有问题”和“别动”。我花了整整两周才勉强搞清楚里面一个核心函数到底在干什么。那段时间我每天都在问同一个问题代码写出来到底是给机器看的还是给人看的这个问题想通了很多东西就顺了。1. 代码的第一读者不是机器是人——聊聊“学好语文”和“写好代码”的关系1.1 编译器只检查语法人检查“语义”很多新手有一个误解觉得代码只要编译通过、运行结果正确就算写完了。这句话对了一半。机器只关心语法和逻辑但代码真正的第一读者永远是人。一个函数机器会老老实实执行它但团队里的其他人、三个月后的你自己需要读懂它、维护它、扩展它。编译器能抓出括号不匹配、类型错误却抓不出“这个函数叫do_sth它到底做了什么”这种致命问题。工程代码的平均寿命远远超出很多人的预期。一段业务代码往往要经历需求迭代、bug 修复、人员交接、线上复盘读它的次数可能比写它的次数多几十倍。代码的维护成本几乎全部发生在“人读代码”这个环节而“人读代码”靠的不是语法分析是阅读理解能力。我后来带团队时经常跟新人说一句话写代码要当心三个月后的自己。那时候你会像一个陌生人一样对你的代码一无所知。如果你自己都看不懂自己写的代码你要么是在写代码要么是在制造灾难。1.2 “语文”到底指的是什么能力这里说的“语文”不是背古诗词、写议论文那个语文。它在编程语境下拆开来是四种能力精确理解他人表达的能力看不懂需求文档、听不懂产品经理的话代码写得再花哨也跑偏。清晰组织文字的能力变量名、注释、文档、提交信息全是文字表达。抽象概括的能力把复杂逻辑提炼成几句话把散乱的模块梳理出清晰的边界。用类比把复杂变简单的能力给非技术背景的人讲清楚技术方案靠的正是语文里的比喻和类比。写代码本质上是一连串“翻译”动作把业务需求翻译成技术语言把技术语言翻译成机器指令再把代码意图翻译回文字告诉下一个读者“这段代码为什么存在、为什么这么写”。任何一个翻译环节没过关信息就会折损折损的代价就是返工和加班。有个生活化的类比我一直很喜欢代码就像是菜谱。菜谱只是给厨师看的机器会做的只是把菜谱“流式执行”一遍但其他厨师能不能照着你写的菜谱做出同款菜完全取决于你的措辞是否清楚。食材清单都列全了、步骤都对但“适量盐”“小火慢炖”这种模糊表达就足够让厨房翻车。1.3 技术债里最隐蔽的一种是“表达债”技术圈爱谈技术债什么设计债、测试债、架构债都有人研究。但有一种债几乎没人提我管它叫表达债——代码逻辑其实不算乱但你根本看不懂它想表达什么。逻辑混乱的代码好歹还能靠重构救一救表达债却会让你连重构的勇气都没有。我见过一个典型的场景某模块的旧负责人离职后留下一仓库的缩写命名函数职责边界模糊核心逻辑散落在一个 500 行的函数里。新团队花了整整两周审阅才敢对一行代码动手——不是因为那行代码多难而是因为没人看得懂它的依赖关系和前置条件。后来团队做了一个很朴素的调整重命名变量、拆分函数、补上“为什么”注释。代码逻辑几乎没有任何变化但可维护性完全不一样了。那次之后我是彻底信了这句话语文能力的短板会在长期维护里以“隐性工时”的方式反复向你收费。2. 命名、注释与提交信息——程序员每天“写作文”的三个考场2.1 变量命名的灾难现场与自救先列几个我在 code review 里经常看到的命名每个都是活生生的反面教材常见反例问题出在哪推荐改法a1、a2完全没有语义只能靠上下文猜expire_time、create_timetmp指代不明不同位置的tmp含义完全不同reduced_price、pending_countdo_sth、deal动词含糊看不出职责apply_discount、calculate_total_amountres到底是 result 还是 resource前后矛盾query_result、order_responsedata2比data多了个 2但这根本不是版本号need_billing_data或filtered_rows命名的基本原则其实就一句话变量名是职责的缩写函数名是行为的说明书。一个名字如果说不清楚往往不是因为你的词汇量不够而是因为你在动手前就没想清楚它到底负责什么。我印象很深的一次重构发生在上面提到的那个“离职同事遗留模块”上。当时我硬着头皮花了一周把a1、b2、tmp全部替换成可读的名字原代码一行逻辑都没改。奇迹发生了原本团队讨论要三小时才能理清的流程重命名后只花了一小时十分钟。代码还是那段代码只是“人话”变多了。这个操作后来成了我给新人做培训时的保留题目。我会随手选一段代码让大家说说每个变量名表达了什么。说不上来的基本都不是英语问题是设计问题。2.2 注释的三种层次翻译、意图与契约注释是“语文”在代码里最集中的体现但也是最容易写烂的地方。我把注释分成三个层次第一层翻译注释彻底没用。比如# 把 a 和 b 加起来 result a b这种注释的唯一贡献是制造噪音。代码本身已经说了它要做什么你加上去反而干扰阅读。第二层意图注释这才是注释的核心价值。代码能告诉你“做了什么”但通常说不清“为什么这么做”。比如# 缓存不能加在这里因为订单过期时间由下游配置决定 # 一旦缓存会读到脏数据读穿透的成本在可接受范围内 order query_order(order_id)如果没有这段注释后来的人很可能会“好心”地在这里加一个 Redis 缓存然后踩进一个线上事故里。意图注释回答的是这段代码为什么长这样为什么不那样写。第三层契约注释用来交代代码之外的约束条件。比如# 调用前必须持有全局锁且并发线程数不允许超过 4否则会触发死锁 def sync_xxx(): pass这种信息藏在代码里完全看不出来但写出来就能救你一命。我自己给团队定的注释纪律就一条注释里只写代码自己不能表达的信息。能用代码说清楚的不要用注释注释要优先写“为什么”而不是“是什么”。2.3 commit message 和 PR 描述被忽略的“职场作文”如果说命名和注释是代码内部的语文那么 commit message 和 PR 描述就是代码外部的语文。说实话我见过不少代码写得挺干净的程序员一到写提交信息就原形毕露。最常见的反例就是这几个fix bugupdate修改代码优化这种提交信息等于没写。三个月后你翻 git log看到的是一堆“fix bug”“update”完全无法恢复当时的上下文。更可怕的是线上出故障时值班同事只能对着这种历史空转。我后来在团队里强制要求提交信息必须说清楚三件事改了什么、为什么改、影响范围。主题行写成“动词 对象 原因”的格式正文可以短但必须给出背景。比如这样修复订单导出时间窗口问题 背景业务方反馈跨时区订单经常漏导。 原因之前按自然日 0-24 点筛选而业务实际需要按 “从当前时刻往前滚动 24 小时”计算。 改动将筛选逻辑替换为滚动时间窗口。 影响仅影响导出查询不涉及其他模块。 验证已用跨时区模拟数据跑通抽样比对无漏单。这个习惯一开始确实痛苦大家会发现很多提交根本写不出“为什么”四个字。但只要坚持两周所有人都会发现一个神奇的变化查 bug 比以前快多了。因为 git log 真的变成了一本可以翻阅的项目史而不是一堆无法解码的暗号。3. 从需求到落地的“翻译链”——语文不好代码从一开始就跑偏3.1 需求一句话代码跑偏一百里程序员每天干的第一件事其实是“阅读”。读需求文档、读原型图、开会听产品经理说话。这一关没过后面全是白干。业务语言天然充满歧义。同一个词在业务侧、技术侧、用户侧可能有完全不同的含义。我经历过一次典型的翻车需求方提了一句“导出今天的订单”开发人员很自然地按“自然日 0 点到 24 点”筛选数据。结果开发完了业务方说不对他们真正要的是“从当前时刻往前推 24 小时”的滚动窗口因为运营同学会在凌晨导出前一天的订单按自然日算会漏单。一句话的歧义浪费了一整个迭代的开发量。语文能力强的人会本能地做两件事复述和反问。接到需求时多嘴问一句“你的意思是以支付成功时间为准从昨天上午 10 点到今天上午 10 点对吗”这种习惯能帮你挡掉八成以上的返工。它不是需求分析师的专利是每个会写代码的人都应该具备的基本功。3.2 技术方案的本质是“书面沟通”写技术方案这件事很多程序员把它当成交差背景复制粘贴核心设计写一大段晦涩的架构图收尾来个“综上所述”。这种方案发出来评审者要么看不懂被退回要么没人真看懂却直接通过了——后者更危险。方案的本质是书面沟通它是让评审者在最短时间内对你建立的系统建立正确心智模型的工具。表达混乱的方案大概率意味着思路本身还不清晰。我建议方案至少包含四件套背景为什么要做这件事不做会怎样。方案你打算怎么做核心链路是什么。取舍为什么不用其他备选方案这个方案牺牲了什么。风险哪里可能翻车怎么兜底。大部分人只写第二件导致评审变成答疑大会一个个来回盘问。如果你在写方案时发现“取舍”和“风险”写不出来那不是文笔问题是设计还没有闭环。写不清楚就是没想清楚这个判断标准比任何 review 都严格。3.3 评审和跨部门沟通中的“语言组织能力”代码评审里有一种熟悉的味道讲的人很激动听的人很迷茫。问题的核心往往出在组织语言时完全没考虑听众。有一次我们跨部门评审一个订单状态机的设计一个同事准备得非常充分时序图画了五张讲得满头大汗但对面业务方全程表情空白。后来另一个同事站起来说“你就把订单状态当成洗衣机。待机、进水、洗涤、排水、脱水、完成任何一个环节出问题就进故障检修修好了继续跑修不好就走报废流程。”全场三秒就懂了。这个例子说明的是语文里的“类比”能力。在写代码时类比能力的价值同样是核弹级的设计系统时如果你能用一句话比喻讲清楚核心数据流说明你已经先在心里验证了一遍这个设计。讲不清楚大概率说明某些环节自己也没完全捋直。这种能力是可以练的。每次给自己两分钟把刚做完的模块用一句话讲给一个非技术背景的人听。讲得通你才真正理解了它讲不通回去再看一遍自己的代码。4. 把“练语文”落到日常我拆出的四个可执行动作光谈观念没用我会说一些真正落地的动作。这些年我靠这几个习惯明显感觉自己写代码和带团队都顺了不少。4.1 动作一给每个命名写“一句话理由”这个动作来自一次 code review。当时我盯着一行代码的变量data看了半天问作者“你这个 data 到底存的是什么”他想了半天说“就是数据啊”。这句话一出口我们俩都愣住了——一个连作者都说不清语义的变量名放在线上跑了三个月。从那以后我给自己定了个规矩任何命名如果能用一句话解释它在系统里的职责就成立解释不了就说明它不该存在或者名字该换。具体操作是定期对自己负责的模块做一次“命名复盘”把模糊的命名列出来逐一替换。你试试就知道给名字找理由的过程往往会倒逼出重构。因为有些函数和变量你压根解释不清它们就是历史包袱不删留着过年。4.2 动作二注释先写“为什么”再写“是什么”写注释的正确姿势不是对着代码逐行翻译而是先回答一个问题如果这段代码没有任何注释三个月后的我会在哪个环节卡住把答案写下来这就是一条合格的意图注释。比如# 这里不用 Redis 缓存订单过期时间由 MQ 异步更新 # 缓存一致性维护成本高于收益当前读 QPS 完全扛得住 def get_order(order_id): ...它回答了三个问题为什么不用更“聪明”的方案、有什么代价、为什么接受这个代价。后来的人看到这段注释就不太可能手贱改掉你的设计。团队纪律就一条禁止翻译注释禁止“这里有问题”“别动”这种没有信息量的废话。注释是为了填补代码表达的盲区不是为了凑行数。4.3 动作三把 commit message 当“工作日志”写前面已经讲了 commit message 的格式这里补充一个操作层面的建议主题行一定不要超过一句话正文用列表列清楚关键信息。我个人的习惯模板是这样的第一行动词 对象 原因比如“缩短订单查询超时时间消除上游慢查询阻塞”空一行后写正文背景为什么要做这个改动改动改了什么模块、什么核心逻辑影响会影响哪些调用方、需要注意什么验证怎么自测的、结果如何这个格式并不复杂但它能让你在三个月后线上出问题时只需要一条git log就能恢复所有上下文。有一次我们在追查一个生产环境的数据异常靠的是一条半年前写的 commit message里面恰好备注了“此逻辑依赖上游的时区配置改动前务必确认”。这句话帮我们排除了一个重大嫌疑方向节省了至少两小时排查时间。这就是语文能力在“运维”场景里的真金白银。4.4 动作四每周输出一篇“人话版”技术笔记最后一个动作跟代码无关但效果极其显著每周选一个本周解决过的问题用不超过五百字的篇幅把它讲给一个完全没有技术背景的人听。这不是为了传播而是为了倒逼自己梳理模糊地带。你会发现一个很有意思的现象好多事你觉得自己懂了一写就卡壳。真到“写出来”这一步那些含糊的、没想透的地方全都会浮现出来。我坚持写技术笔记已经很多年。最大的受益根本不在“分享出去”而在“写之前被迫把脑子里那团模糊的毛线团拆成一根根清晰的线头”。如果你觉得“明明会做但讲不出来”那九成不是表达天赋问题而是你对这件事的理解还停留在“能做”的层面没到“能讲清”的层面。我这些年面试过不少人也带过不少新人发现一个很朴素的规律代码命名清楚、注释准确、提交信息可读的人写技术文档、发协作消息、讲方案通常也不差。反过来代码乱成一团的人让他口头讲讲自己的设计往往也讲不出完整的因果链。这不是玄学而是同一种能力的两面——能不能在脑子里把事想清楚并通过语言精确地传递出去。“学好语文才能写好代码”这句话看似鸡汤其实是一条被反复验证过的工程经验。如果你也正在被“写得出来却说不清楚”困扰不妨从给一个变量起好名字开始试试。