
写代码这件事很多刚入行的朋友会下意识理解成“让程序跑起来”。但我做了几年工程之后才发现真正拉开差距的是让代码被人读懂的能力。这段认知是在一次次接手别人代码、又被别人接手自己代码的过程中磨出来的。很多时候一段代码功能没有问题可只要第二个人需要看它、改它成本就会成倍往上翻——这其实就是大家常说的“技术债”而里面占大头的往往不是架构选型错而是可读性欠账。这篇文章不聊空泛的原则我会从命名、函数拆分、注释、格式、实战重构五个角度配合正反两面的代码案例把我觉得最实用的规范技巧一条条讲透。你可以直接把它当成一份“代码可读性自查清单”来用哪怕你写码经验只有半年照着一项项调整也能在短时间内明显改善代码的观感和后续维护成本。1. 先聊聊可读性差的代码到底在消耗什么1.1 一个真实得不能再真实的维护场景几年前我还在做后端开发时接过一个运营天天催的报表模块。前任同事离职前留下一个函数参数名是a和b函数体里全是裸奔的数字返回值像打了一堆补丁。我当时不敢改因为动任何一个数字都可能让某个隐藏分支出问题。后来因为业务规则要调整我硬着头皮读了两个小时才勉强猜出其逻辑a是购买件数b是原始金额整体逻辑是根据件数分档打折、再算税费、最后判断是否满足满减。那段代码本身并不复杂按今天眼光看也就三十行为什么我会读两个小时因为所有信息都被“藏”起来了。参数含义藏在函数外部的调用上下文里数字含义藏在业务背景里程序逻辑藏在一层层if嵌套里。每当我要确认一个变量的含义就得顺着调用链往回翻每想确认一个数字的来路就得去猜它是折扣、税额还是某种阈值。这个过程的本质是写代码的人用了三秒省掉一个清晰命名而读代码的人用三十分钟把缺失的信息补回来。这个案例绝不是个例。我在不少团队评审里看到过类似代码data、res、temp、flag这类名字满天飞注释要么没有要么在重复代码内容。这类代码当时写的时候可能没什么感觉因为写的人脑子里装着完整的上下文但两周之后自己回来改都未必能秒懂更别说交给别人接手。1.2 可读性的价值在于降低“沟通成本”代码有个非常朴素的量化规律写一次读很多次。写代码发生在开发期但读代码发生在排错、评审、扩展、交接、审计等各种后续环节里。一个项目越老读过某段代码的人就越多这段代码的可读性就越值钱。我习惯把可读性看作“代码与未来读者之间的沟通成本”。沟通成本低意味着后续任何修改都能快速定位、放心理解、安全下手。沟通成本高则会连带出一系列问题新人接手慢、缺陷排查难、不敢动旧逻辑、不同人改出风格分裂的代码。这些问题不会在功能上线那天暴露但它们会在六个月、一年后准时“分期还款”而且利息不低。所以说可读性不是“代码写得漂亮”这种锦上添花的事它是工程效率的一部分。你重视可读性本质上是重视团队里所有人的时间。2. 命名把意图写进每一个标识符2.1 命名不是选个词而是压缩信息密度命名是代码可读性的第一道关口也是最容易立竿见影的改进点。一个变量名、函数名、类名本质上是一条“信息压缩通道”它把一段业务含义浓缩成几个单词让读者在扫到这个名字的瞬间就能还原出背后的上下文。举个很典型的例子。下面这三个变量内容可能完全一样但传达的信息天差地别很糟d、dt、data、temp一般list、arr、result不错unpaid_order_list、customer_phone_list、pending_request_counttemp是最典型的反面教材。很多人写临时变量的一律叫temp它到底临时装的是什么读者完全无从推断。如果你只是循环里存个中间值那用currentItem、sumScore之类的名字信息量立刻就不一样了。同样变量名要尽量反映“业务含义”而不是“实现细节”。比如存一批订单名字叫orders就比叫array有意义但如果这批订单是已经支付但还没发货的那pendingShipments又比orders更进一步。命名越贴近业务语言读者越容易把代码和需求对应起来。2.2 动词、名词和布尔值各司其职命名的另一个易错点是词性混乱。我总结了一套基本规则简单直接函数用动词或动宾结构比如getUserById()、sendEmail()、calculateDiscount()。如果函数返回布尔值用is、has、can、should开头例如isPaid()、hasPermission()、shouldRetry()。类名和模块名用名词比如OrderService、PaymentValidator、DiscountPolicy。普通变量用名词或形容词名词比如userName、availableStock、maxRetryCount。常量用全大写加下划线比如MAX_RETRY_COUNT、DEFAULT_TIMEOUT。这些规则不是强迫症而是让代码具备“一眼识别”的能力。看到isPaid()你本能知道它返回布尔看到MAX_RETRY_COUNT你本能知道它是不可变的阈值。这种默契形成后读代码的速度会明显加快。2.3 命名长度要跟着作用域走关于命名长度网上有两种极端一种崇尚越短越好另一种恨不得把整个句子都塞进去。我的经验是作用域越小命名越短作用域越大命名越长。循环里跑三行的局部变量用i、j、k完全没问题读者瞬时能理解一个跨文件使用的全局变量或常量最好把约束、单位、含义都说清比如MAX_UPLOAD_FILE_MB就比MAX强一百倍。长度不是罪过没有信息的长度才是罪过。命名还有一个容易踩的坑就是缩写和简写。除非是行业公认缩写如id、url、http否则尽量别自己造缩写。getUsrInf和getUserInfo的区别看着只差几个字母但对陌生读者来说前者每次都要在脑子里做一次解码看多了非常累。3. 函数设计让每个函数只说一句话3.1 函数的单一职责怎么判断函数可读性的核心是它是否承担了过多事情。判断办法很朴素你能不能用一个简单句不费力地说清楚这个函数在做什么如果答案需要“先这样、再那样、然后判断一下、最后返回”那这个函数大概率需要拆。比如我见过一个函数它打开文件的同时顺手做了数据清洗然后又把结果写回数据库。这三个动作任何一个单独拆出来都能用一句话说清合在一起读者每次阅读都得同时在脑子里维护三条线。拆开之后每条线都可以单独测试、单独修改逻辑清晰改动一个环节也不会牵连另外两个。我常用的拆分粒度标准是函数体超过二三十行或缩进超过三层就认真考虑拆分。这不是硬指标而是触发反思的信号。有些二三十行的函数逻辑平铺直叙完全可读但也有些函数十行就嵌套得惨不忍睹。重点在于不要跟代码行数较劲要跟“认知负担”较劲。3.2 用卫语句和提前返回干掉嵌套代码可读性的头号敌人往往是层层嵌套的if。我自己早期也爱写“嵌套流”后来才意识到那其实是担心各种边界情况结果把正常路径和异常路径全搅在了一起。先看一段反面示例function processOrder(order) { if (order) { if (order.status paid) { if (order.items order.items.length 0) { let total 0; for (const item of order.items) { total item.price * item.quantity; } return total; } } } return 0; }这段代码的逻辑本身不难但读的时候必须一层层往里钻才能看到真正的主逻辑。更麻烦的是return 0藏得远看不太出来哪些分支会走到它。同样功能换成卫语句加提前返回function processOrder(order) { if (!order) { return 0; } if (order.status ! paid) { return 0; } if (!order.items || order.items.length 0) { return 0; } let total 0; for (const item of order.items) { total item.price * item.quantity; } return total; }前后对比最大的变化是边界情况和主流程被分开了。每个卫语句像一张筛子把没法处理的输入在入口处直接筛掉剩下的主体代码描述的就是正常流程本身。读者不用再嵌套着读直线扫下来就能抓住核心。卫语句还带来一个额外好处后续想加新的拦截条件只需在函数前面加一个if加一个return不必把后面主体代码整体包进一层新缩进diff 会干净很多。3.3 参数数量太多时考虑收拢函数参数越多调用方就越难一眼看清每个入参的含义。三个参数是一个经验阈值超过三个我比较建议把联系紧密的参数收拢成一个对象。比如这个调用create_report(2024-01-01, 2024-12-31, 1, True, sales)五个参数顺序稍错一个含义完全就变了。如果改成传一个参数对象report_config ReportConfig( start_date2024-01-01, end_date2024-12-31, report_typesales, granularitymonthly, include_trendTrue, ) create_report(report_config)调用处立刻变得像声明一样清晰后续扩展字段也不用改函数签名只在对象里加一个属性即可。这个改进前期可能感觉多写了几行但后面只要有一个人需要读调用处它就把那几行成本加倍赚回来了。4. 注释与文档写给未来的维护者不是写给编译器4.1 好注释只解释“为什么”有一种普遍误解觉得注释就是翻译代码。于是到处看到# 把用户信息保存到数据库 user_repository.save(user)这种注释把代码又抄了一遍纯属浪费读者眼球。好的注释应当集中在“代码本身没法表达的信息”上最重要的就是两部分业务意图和约束背景。我举一个更好的例子# 支付前把金额四舍五入到“分”。 # 合作渠道的分账规则是固定两位小数如果传入更精细的小数 # 渠道侧会直接拒绝这笔支付。 amount_in_cents round(amount, 2)这段注释没有介绍任何语法层面的东西它回答了两个代码里看不到的问题为什么是两位小数以及不做会发生什么。下次有人觉得“要不要改成三位小数”读一眼注释就能直接判断风险。我自己写注释时最常用的框架是如果这段代码需要解释先问自己能不能通过命名和拆分让它变得不需要解释如果确实不行那就补一句“为什么这么做”而不是“做了什么”。4.2 注释区里的三个雷区注释区的雷区我踩过不少说三个最常见的注释掉的代码。一坨被注释掉的旧逻辑读者会犹豫它还有用吗要不要恢复是不是要改常见的心理是不舍得删但版本管理系统已经把历史记录保存好了真需要时能翻回来。留在主代码里只会增加噪音。该删就删这是我对自己的硬性要求。重复代码的注释。比如“这段是过滤”“这段是排序”读者看一眼代码结构就知道的事注释再加一层纯属视觉负担。过期注释。代码后来改了注释却没跟着改这比没有注释更坑。它会让读者要么错误理解逻辑要么花额外精力去辨析“到底谁是对的”。所以一旦发现注释和代码不一致立刻改不要留着“回头再弄”。还有一点和注释相关函数头的文档字符串很有价值。JavaDoc、Docstring、JSDoc里的函数说明、参数含义、返回值约定对使用者来说就是最方便的说明书。但如果每个函数都写满几行日常文字也会导致文章失控。我的习惯是公共接口、复杂逻辑、带有隐性约束的核心函数必写普通一目了然的短函数不写。5. 格式与布局让代码有视觉节奏5.1 空行、分组与层次感格式问题经常被轻视但它对“视觉可读性”的影响非常大。回想一下读教科书和读小作文的差别排版乱的文章再好的内容也会让人读得烦躁。代码也一样。空行是我最常用来划分层次的手段。相关代码行放在一起逻辑段落之间用空行隔开这个习惯成本极低收益却立竿见影。比如一个函数里有“校验入参、拉取数据、计算、拼装返回结果”四段每段之间空一行读者一眼就能看到函数的结构地图如果全部挤在一起就得靠逐行分析才能重新切分。其次是对齐和统一。同类代码风格不统一比如有的地方用双引号有的单引号有的缩进两格有的四格会让读者在意外的地方反复中断注意力。别总想着“反正代码能跑”代码的第一读者是人人的注意力和耐心都是有限的。5.2 用自动化工具把格式争论消灭掉格式问题完全靠人盯是一件很低效的事而且每个开发者的审美都不一样评审时很容易陷入“你觉得这样好看我觉得那样好看”的内耗。我的建议是把能交给工具的全都交给工具。现在主流语言基本都有成熟的格式化方案。JavaScript 和 TypeScript 可以用PrettierPython 可以用Black或autopep8Java 可以用google-java-format或spotlessGo 直接有内置的gofmt标准格式。再配合ESLint、Ruff、Checkstyle这类静态检查工具很多可读性层面的是非问题都能自动检出和修正。还有一个很实用的项目级小文件.editorconfig。它定义了缩进风格、字符集、换行符等基础格式只要团队里每个人装了对应插件不管用什么编辑器打开项目都会自动统一格式基线。这些工具看似不起眼但它们能省掉大量评审时间让评审的精力集中在真正的逻辑问题上。值得强调的是格式化工具解决的是“风格一致”不解决“可读性好”。工具能帮你把缩进对齐但没法替你决定某个变量该叫什么名字也没法告诉你函数是不是该拆。常量、命名、拆分这些还是要靠人。6. 实战把一段“能跑”的代码重构到“能读”6.1 原始代码与问题诊断理论聊了不少不如直接上一段实际代码走一遍完整重构。下面是我在某项目里见过的一个价格计算函数我把它风格原样保留了下来def f(a, b): c 0.0 if a 5: c b * 0.9 else: c b * 0.95 if a 10: c b * 0.8 if a 20: c b * 0.7 t c * 1.06 if t 10000: t t - 100 return t第一眼问题就非常明显参数a、b完全没有含义局部变量c、t靠位置推断。魔法数字一堆5、10、20、0.9、0.8、0.7、0.95、1.06、10000、100每一个都需要读者靠猜。控制流存在重复覆盖合法但不直观。if a 10会在if a 5之后再次赋值读者必须心算到哪种档位生效。通过跟原有业务方确认我整理出了它的实际规则a是商品数量b是订单原始金额。数量达到对应档位时按折扣价计算1.06是税率加成如果税费后的总金额超过一万再减一百元让利。6.2 第一步把魔法数字变成有含义的常量理解了业务规则后第一步先把魔法数字全部常量化和命名化。这一步不改任何算法只是把“数字”翻译成“含义”TAX_RATE 0.06 LARGE_ORDER_THRESHOLD 10000 LARGE_ORDER_DEDUCTION 100这一步做完原先那堆裸数字对应的含义全部变成只看名字就清楚的东西。折扣档位也是一样把它们放进一张表里比散落在嵌套里好维护得多DISCOUNT_TIERS [ (20, 0.70), (10, 0.80), (5, 0.90), ] DEFAULT_DISCOUNT_RATE 0.956.3 第二步按职责拆分函数原始函数至少兼任了三件事计算折扣、计算税费、判断满减。这三件事的变更是独立的拆成三个函数之后任何一块规则变化都只影响其中一块。拆分后再给每个公共函数补上文档字符串说清楚参数和返回值。带着前面所有信息重构后的代码是def calculate_final_total(quantity: int, base_amount: float) - float: 按购买数量计算折后金额叠加税费后再判断是否满减让利。 discounted_price get_discounted_price(quantity, base_amount) total_with_tax discounted_price * (1 TAX_RATE) return apply_large_order_deduction(total_with_tax) def get_discounted_price(quantity: int, base_amount: float) - float: 根据购买数量档位返回对应的折扣价档位从高到低依次判断。 for min_quantity, discount_rate in DISCOUNT_TIERS: if quantity min_quantity: return base_amount * discount_rate return base_amount * DEFAULT_DISCOUNT_RATE def apply_large_order_deduction(total: float) - float: 满足阈值的大额订单直接减让利金额用于渠道促销活动。 if total LARGE_ORDER_THRESHOLD: return total - LARGE_ORDER_DEDUCTION return total6.4 第三步审视命名和控制流对比重构前后变化不止是“多了一些单词”。我现在逐条说明为什么每一个改动都有必要。第一参数名从a、b变成了quantity和base_amount。任何读者第一次看到这个函数不需要查调用处就能知道入参的物理含义。第二折扣判断从“顺序覆盖赋值”改成了“从高到低遍历档位表”。原来的写法读者需要心算一遍所有if的前后关系新写法把规则声明成一张表只要记住“从上往下匹配第一个满足的档位”逻辑自然清楚。以后要加一个“满 50 打 6 折”的档位只需要往DISCOUNT_TIERS里添一行不用动函数主体。第三get_discounted_price和apply_large_order_deduction被拆成独立函数后既方便单独写单测也可以在别处复用。比如库存盘点时如果也用同一套折扣规则直接 import 即可而不是复制粘贴那堆if分支。第四文档字符串写的是“根据档位返回折后价”这类业务性描述没有一句是在告诉读者代码语法保持了注释该有的姿态。6.5 重构后的效果复盘这个案例里算法完全没变运行出来的结果和旧代码逐条对比过一一对应。但代码的可读性从一个需要花两小时去猜的谜语变成了一份按结构就能读懂的说明。我从这个案例里最想强调的是可读性重构不追求一次做完。你可以先只重命名参数和魔法数字再做函数拆分最后优化逻辑结构。每一步单独提交、单独验证风险都很小。但反过来把一次重构攒到很大再动手就会出现“改动太多不敢提交”的窘境最后不了了之。很多人在代码评审时最担心的就是“我这次改动很大要不要把格式也顺手改了”。我的实际经验是优先做小步重构一次只解决一类问题。今天命名明天抽函数后天做常量提取。每次的小 diff 都清晰易懂评审压力小出问题也好定位。7. 常见问题与经验复盘7.1 团队规范为什么总是落地难讲了这么多规范技巧可能你会想直接把它甩给团队第二天就要求所有人遵守。但我在多个团队里推行过代码规范结论是强制贴文档给团队几乎必然失败。根因在于绝大多数人不抗拒“写出好代码”这个目标抗拒的是“额外增加沟通成本”。如果规范只是一篇几十条的长文每个人读一遍的理解还不一样执行起来自然五花八门。更可行的是做两件事第一把规范沉淀成工具配置。能通过 lint 规则检查的全部写进配置文件能用 formatter 自动调整的提交前直接格式化。工具判定可以代替一部分人工争论。第二把规范沉淀成“正反用例”。与其抽象地说“命名要有意义”不如在团队文档里放上坏代码和好代码的对比块再附上两句解释。新人培训时先让他们看几组正反例比背二十条规则有效得多。7.2 代码评审里怎么聊可读性才不伤和气代码评审是最容易因为可读性产生冲突的场景。一方觉得“我的代码没毛病你凭什么说不好”另一方觉得“这么写谁都看不懂”。我这些年总结下来的沟通姿势就一句话话要落在“未来维护”上不要落在“个人偏好”上。比如不要说“这个名字太丑了”而要说“如果三个月后有人回来改这个函数看到data可能不知道里面装的是什么我们换个更具体的名字成本更低”。更不要陷入“我喜欢这样你喜欢那样”的审美唇枪舌剑。可读性讨论的本质永远是哪种写得更让下一位读者省力。同时评审意见尽量可执行。与其说“这个函数有点乱”不如说“这一段可以拆成两个函数各自职责分别是……”可执行的建议更容易被采纳也更利于把团队水平往上拉。7.3 几个低成本高收益的小习惯最后分享几个我自己比较受用的日常习惯也许可以给你参考。第一个习惯是提交代码前自上而下读一遍自己的 diff。这几十秒到几分钟的阅读等价于站在一个陌生人的视角重新审查代码。我每次几乎都能发现可以改的命名、多余的注释、忘记拆分的函数。这个习惯比任何规范文档都直接。第二个习惯是写短提交信息但把“为什么”写清。提交历史也属于代码可读性的一部分。比如“修复金额精度问题”这种信息还算可以但更优秀的是“统一结账页金额精度到分避免渠道因精度拒绝支付”。别人在 git log 里扫一眼就能理解这个提交存在的意义。第三个习惯是定期做一次“可读性小扫除”。不需要专门排一天去大改代码而是每写几天代码后挑出一段最近改动频繁的模块花半小时做重命名、拆函数、删注释掉的代码。这种零存整取的方式不会打断开发节奏又能控制技术债的累积。写代码本质上是在跟未来的人交流包括未来的自己。第一次写下那行字的时候它的意思只有你懂三个月后再看它也只认得命名清晰、结构直白的形态。这些年我带过不少人也接手过不少陌生的老系统越来越确认一件事绝大多数代码质量问题都不是能力问题而是愿不愿意多花那半分钟把话说清楚的问题。