2.1 编码标准与风格
概述与动机
编码标准是共享的约定,让许多人写出的代码看起来像是出自同一位细心作者之手。它们涵盖命名、格式、文件布局、惯用法、错误处理,以及一个团队偏好的编程范式。在一个小团队里,个人品味就能成事。而在一个大型团队里(数百甚至数千名工程师,许多外包人员,人员流动率高),不一致性就会变成一项你在每次阅读、每次评审、每次入职培训时都要缴纳的税。标准把无数琐碎的风格之争变成一次性的决定,此后交由机器替你强制执行。
对于大型组织而言,这一点关乎实实在在的利害得失。代码被阅读的次数远远多于被编写的次数。在企业和政府环境中,一行代码可能会在其作者离职多年之后,被审计员、安全评审人员和维护者阅读到。一致的风格降低了阅读所需的心智成本,缩小了缺陷的暴露面,并让自动化分析在 代码检查工具(能自动标记出可能的缺陷和风格违规的工具)、安全扫描器和重构工具之间可靠地运作。在受监管的领域,比如金融服务、医疗保健、国防和公共部门系统,标准还构成了证明代码库可维护、受控的部分证据。
现代方法是把风格当作一个已被解决、已被自动化的问题来对待,而不是一个需要持续人为判断的事情。格式化工具和代码检查工具在编辑器中运行,在提交前钩子中运行,也在持续集成(CI,即在每次变更时运行的自动构建与测试流程)中运行。由机器来强制执行风格,这样你就能把评审的注意力花在设计和正确性上。目标不是为了统一而统一,而是消除摩擦:你应该能够在不同的服务和团队之间自如切换,而不必重新学习基础知识。
关键原则
- 一致性胜过个人偏好;一套统一约定、处处适用的风格,比一套”最好的”风格却参差不齐地应用要更有价值。
- 让强制执行自动化。格式化工具和代码检查工具才是权威来源,而不是代码评审里关于间距的评论。
- 为读者和维护者优化,而不是为最初的作者。
- 优先选择更广泛的语言社区已经在用的惯例,而不是自创的内部规则。
- 让标准易于采用:提供共享的配置、模板和工具,而不是一份没人读的 PDF 文档。
- 风格规则应该少而精、站得住脚、不含糊;每一条规则都要有强制执行机制,否则它只是一个建议。
- 命名是杠杆效应最高的可读性决策,值得明确的指导。
建议
为每种语言采用一套权威风格指南
对于你使用的每一种语言,采用一套被广泛认可的风格指南作为基线(例如该语言的社区指南或厂商指南),只需记录你的组织需要的增量差异。不要从零开始发明一套内部风格。把你的选择发布在一个中心化、易于查找的地方,并像对待代码一样对其进行版本管理。
让格式化工具成为不容商量的默认选项
对每一种拥有格式化工具的语言,都使用一个有主见的自动格式化工具,并将单一的共享配置纳入代码库进行版本管理。格式问题永远不应该在评审中被提起,因为它在保存时自动应用,并在 CI 中被验证。对于没有强力格式化工具的语言,选定一套代码检查工具配置,并以同样的方式对待它。
把代码检查工具当作强制关卡运行,而不是建议
用一套达成一致的规则集来配置代码检查工具,让违规触发构建失败,并把规则集保存在版本控制中,这样变更就要经过评审。把可自动修复的规则(自动应用它们)与需要人工判断的规则(标记并阻止)区分开来。新规则先以”警告”模式引入,清理完存量违规后,再把它们提升为”错误”级别。
在多个层面强制执行
提供编辑器集成以获得即时反馈,提供提交前钩子实现本地强制执行,并把 CI 检查作为权威关卡。你越早发现违规,代价就越低。CI 必须是最后一道防线,因为本地钩子是可以被绕过的。
给命名制定明确的规则
为每种语言规范大小写惯例,要求名称能揭示意图,禁止使用容易误导的缩写,并为布尔值、集合、单位和异步操作定义惯例。把你的领域词汇写入一份共享词汇表,让同一个概念在任何地方都用同一个名字。
有意识地管理多语言场景下的一致性
在一个横跨多种语言的代码库中,即使语法不同,也要力求概念上的一致(错误处理模式、日志结构、项目布局)。从一个中心仓库为每种语言提供配置,这样一个新服务就能通过模板或脚手架自动继承这些标准。
把惯用法和范式编写成文档
超越格式化的层面。把你偏好的惯用法写下来,比如如何处理错误、如何组织模块结构,以及何时使用异常而非结果类型,再加上你的团队所偏好的编程范式。真正的可读性和可维护性正体现在这里。
权衡:优点与缺点
| 方法 | 优点 | 缺点 |
|---|---|---|
| 严格的自动格式化工具,零配置 | 彻底终结所有格式争论;即时一致性;入职几乎零成本 | 一些不受欢迎的选择不容商量;首次应用时会产生巨大的初始差异 |
| 带内部规则的可配置代码检查工具 | 贴合组织需求;能编码真正的缺陷预防规则 | 配置漂移;规则上的无谓争论;维护负担 |
| 整体采用的社区标准 | 新员工熟悉;有强大的工具生态支持;维护成本低 | 可能不适合特殊的组织约束;偶尔会有别扭的规则 |
| 定制的内部标准 | 完全贴合组织需求 | 编写和维护成本高;新员工不熟悉;工具支持薄弱 |
| 各团队自主决定 | 本地士气高,贴合具体情境 | 碎片化;跨团队流动痛苦;工具不一致 |
强制执行默认规则,是用一点点个人自主权,换取巨大的集体收益:更少的评审摩擦、更快的入职速度、可靠的自动化。主要风险是把标准过度工程化成数百条规则,拖慢所有人的速度,却又拦不住真正的缺陷。要把规则集保持精简、有据可依,并倾向于采用一套已有的标准,让维护成本保持低廉。
与团队讨论的问题
哪些代码检查规则应该导致构建失败?你如何在不让所有人停摆的情况下,把一条规则从警告提升为错误? 本章主张每一条规则都需要有强制执行机制,新规则应该先以警告模式落地,清理完存量违规后再切换为错误级别。在一个大型团队中,针对一个不干净的代码库把某条规则切换为错误级别,会在一夜之间阻塞数百个无关的变更。把确凿的证据带到会议上:每一条候选规则当前的违规数量,以及它是可自动修复的,还是需要人工判断的。在企业和政府环境中,通过/不通过的界线也会输入审计关卡,所以一套含糊不清的规则集会削弱你的合规论证。确定一个分阶段的推行计划:自动修复能修的,为清理工作做好预算,然后再设关卡。
当你第一次给遗留代码应用格式化工具时,你要如何防止这次重新格式化破坏 git blame、淹没评审? 权衡表警告说会产生巨大的初始差异,反模式部分也指出把重新格式化提交与逻辑变更混在一起的问题。一次大规模的重新格式化会重写成千上万行代码,让 blame 指向这次重新格式化,而不是真正的作者,这会给多年后调试的人带来麻烦。把重新格式化做成一次孤立的、被清晰标注的提交,并把它登记进一个 blame 忽略文件,让历史记录保持有用。对于要追溯谁改了什么的审计员而言,这种隔离正是干净证据与噪音之间的区别。在动手改代码之前就要商定好顺序,而不是事后再补。
谁拥有你的命名词汇表和领域词汇?一个新术语是如何被加进去的? 本章把命名称为杠杆效应最高的可读性决策,并要求你把领域词汇写入一份共享词汇表。如果没有一个明确的负责人,同一个概念就会在不同团队之间获得三个不同的名字,静态分析和搜索工具也会因此失去可靠性。把你代码库中已经出现命名冲突的具体例子带来作为具体信号。指定一位负责人和一条轻量级的提案流程,让新增或重命名一个术语,成为一次小型的、经过评审的变更,而不是每个拉取请求里都要吵一遍的争论。这个答案会改变入职体验:新员工只需读一份词汇表,而不必从不一致的代码里反向推测意图。
对于每一种语言,你是否整体采用一套公认的社区或厂商风格指南?内部的增量差异在哪些地方真正站得住脚? 本章主张你应该以一套现有标准作为基线,只记录你的组织需要的增量差异,因为一套定制标准编写和维护的成本高,而且新员工也不熟悉。对立的拉力是真实存在的:一项内部约束(一条安全规则、一个遗留框架、一项无障碍要求)有时确实会与社区默认做法相冲突,而你保留的每一项增量差异,都是一条从此由你永远拥有并维护的规则。把提议的增量差异清单带到会议上,每一项都附上驱动它的具体约束,并做好准备砍掉任何仅仅出于个人品味的差异。在企业和政府环境中,一个与更广泛语言社区相匹配的基线,也意味着外包人员和新供应商一到岗就已经熟悉,这缩短了入职时间,也强化了审计员所寻求的可维护性证据。
在一个多语言代码库中,哪些惯例真正是普适的,哪些应该保持语言本地化?你如何防止各仓库的配置各自漂移? 本章要求即使语法不同,也要在各语言之间保持概念上的一致(错误处理、日志结构、项目布局),并要求从一个中心仓库为每种语言提供配置,这样新服务就能自动继承标准。这里的张力在于,把一种语言的惯用法强加给另一种语言会产生别扭、不地道的代码,而放任每个团队各自分叉自己的配置,最终会让”这套标准”变得毫无意义。把你当前各仓库的代码检查工具和格式化工具配置清单,以及一份显示它们已经漂移了多远的差异对比带来,作为具体信号。对于一个运行着数十个服务的大型组织,要确定分发机制(模板、脚手架、一个共享配置包),这样一次规则变更只需传播一次,而不必手动复制进每一个仓库。
什么时候禁用一条规则是正当的?谁来评审这个屏蔽操作?你如何防止大规模的屏蔽把标准掏空? 反模式部分指出,广泛的行内屏蔽是一个信号,表明某条规则本身有问题,或者某个团队已经放弃了,然而一项僵化的、不允许任何例外的政策,也会逼着人们为了糊弄代码检查工具而写出更差的代码。商定一条轻量级的路径:一次屏蔽必须附带理由,作用范围要尽可能窄,并且要在评审中可见,而不是埋没在一个全局忽略文件里。把每条规则、每个仓库当前的屏蔽次数带来,因为一条被屏蔽了数百次的规则,说明的是这条规则本身的问题,而不是代码的问题。在受监管和公共部门的工作中,未经解释的大规模屏蔽会直接削弱审计论证,因为流水线将无法再证明合并的代码真正通过了商定的关卡。
行业视角
创业公司。 速度制胜,所以从第一天起就为你使用的那一种语言采用社区默认的格式化工具和代码检查工具,并在第二位工程师入职之前,把它们接入提交前钩子和 CI。不要编写一套你没有时间维护的内部风格:仓库里附带的配置就是全部的标准。当你添加第二种语言时,去用那种语言的权威指南,而不是从零开始发明惯例。
小型企业。 由于没有专职的工具专家、预算也紧张,要完全依靠随你的语言一同发布或搭配发布的免费、有主见的格式化工具,并接受它的默认设置,而不是去调整它。这是一个典型的”买而不是造”的场景:维护一套定制规则集要耗费你没有的时间,而一个开箱即用的格式化工具不花一分钱,还能立刻终结风格之争。把配置保存在仓库里,这样你明年雇的那位外包人员,不必经过任何讨论就能继承它。
企业。 在这个规模上,工作是跨众多团队的治理:一个存放各语言共享格式化工具和代码检查工具配置的中心工程标准仓库,从拉取这些配置的模板生成的新服务,以及阻止不合规合并的 CI 关卡。像对待代码一样对规则集进行版本管理,并通过定期评审来推动变更,让标准是有意识地演进,而不是随意漂移。回报是工程师在团队之间流动时能进入熟悉的代码环境,而自动化工具也能产生可靠的信号,因为每个仓库都保持一致。
政府。 采购和问责塑造着这个选择:把一套具体的风格和安全规则集作为运营授权要求的一部分强制推行,并让流水线出具一份报告,证明每一次合并的变更都通过了商定的关卡,作为审计证据。由于格式化是自动应用的,来自多个供应商和外包方的代码看起来是一致的,这保护了在合同结束很久之后仍要继续进行的公共维护工作。优先选择公认的社区基线而非定制规则,这样标准就是透明的,未来任何供应商都能采用它,而不会被专有工具锁定。
示例
创业公司。 一家四人规模的创业公司从第一天起就为其使用的那一种语言采用了社区默认的格式化工具和代码检查工具,并将它们接入提交前钩子和 CI,这样在评审中就没有人再争论间距问题。由于配置随仓库一起发布,第五位和第六位新员工会自动继承它,从未见过一条关于格式的评论。当团队后来添加第二种语言时,他们直接采用了那种语言的标准指南,而不是发明一套他们没有时间维护的内部风格。
企业。 一家大型银行在数十个团队中用 Java、Python 和 TypeScript 运行各种服务。它发布了一个中心化的”工程标准”仓库,存放每种语言共享的格式化工具和代码检查工具配置。新服务是从一个拉取这些配置的模板生成的,所以每个仓库从一开始就是合规的。CI 会因任何违规而阻止合并,一次季度评审负责管理规则变更。工程师在团队之间流动时的入职时间明显缩短了,因为每个仓库看起来都很熟悉。
政府。 一家正在现代化一套遗留系统的公共部门机构,把一套无障碍和安全代码检查规则集,作为其运营授权(ATO,即在生产环境中运行该系统所需的正式批准)要求的一部分强制推行。风格合规成为了审计证据的一部分:流水线会生成一份报告,显示所有合并的代码都通过了商定的静态分析关卡。由于格式化是自动应用的,来自多个供应商的外包人员生产出的代码在视觉上是一致的,这让政府在合同结束之后的长期维护工作变得更容易。
商业理由:动机、投资回报率与总拥有成本
采用标准的成本大多是一次性的:选定指南、接入工具,以及应用一次大规模的初始重新格式化提交。经常性成本很低,因为强制执行是自动化的。不采用标准的成本则是经常性且会不断累积的:每一次评审都要在风格上花费几分钟,每一次入职都更慢,静态分析工具产生噪音,不一致的代码还会隐藏缺陷。在一个大型组织中,这些分钟数累加起来,相当于损失了若干全职人力。
回报体现在评审延迟的减少、与风格相关的评审评论减少、入职速度加快,以及自动化工具产生更高质量信号。在受监管的环境中,还有一层额外的回报体现在审计准备度上:可展示、可强制执行的控制措施降低了合规评审的工作量和风险。要向领导层说明理由,把标准定性为一个低成本、高杠杆的开发者生产力和审计姿态的支点,并利用评审评论分析和入职调研数据,给不一致性当前的代价标上一个数字。
反模式与陷阱
- 在代码评审中争论风格: 这是强制执行未自动化的信号;把规则搬进工具里去。
- 没人读的标准文档: 一个没有强制执行的维基页面只是装饰品;每一条规则都需要一套机制。
- 规则泛滥: 数百条吹毛求疵的规则,拖慢了工作,却拦不住真正的缺陷。
- 配置漂移: 每个仓库各自分叉自己的代码检查工具配置,直到”这套标准”变得毫无意义。
- 在功能开发过程中重新格式化整个仓库: 把重新格式化提交与逻辑变更混在一起,会破坏评审和 blame;大规模的重新格式化要做成孤立的、被清晰标注的提交。
- 用大规模屏蔽来忽略代码检查工具: 广泛的行内禁用,是一个信号,表明某条规则有问题,或者某个团队已经放弃了。
- 没有归属的标准: 没有明确的负责人,就意味着规则永远不会演进、只会腐坏。
成熟度模型
- 第 1 级,启动: 风格因作者而异,且是被动应对的;没有共享配置;格式在评审中被争论,最后由当天最在意的人来定夺。
- 第 2 级,发展: 各个团队各自采用格式化工具和代码检查工具,但配置和规则集因团队和仓库而异,所以一致性只能维持在各团队自己的边界之内。
- 第 3 级,标准化: 每种语言的中心化共享配置都有文档记录,并在组织范围内被强制执行;CI 会阻止不合规的合并;新仓库通过模板或脚手架自动继承标准。
- 第 4 级,管理: 标准依据数据被度量和管控:违规率、屏蔽次数、与风格相关的评审评论、以及入职时间,都相对于基线被追踪,规则变更依据这些证据而非主观意见来推进或废弃。
- 第 5 级,编排: 标准在整个组织范围内被持续改进和整合;多语言的惯用法和领域词汇都有文档记录并被强制执行,强制执行几乎不产生摩擦,规则集随着语言、工具和组织需求的变化而调整。
讨论话题
- 强制执行的规则与信任工程师判断力的文档化指南之间,界线应该划在哪里?
- 当一条广受喜爱的社区规则与一项真实的内部约束相冲突时,组织应该如何处理?
- 谁拥有这些标准?规则变更是如何被提议、讨论并推行、同时又不造成中断的?
- 在一个多语言代码库中,哪些惯例应该是真正普适的,哪些应该保持语言本地化?
- 你如何在不进行破坏性的”大爆炸式”重新格式化的情况下,把标准回补到一个庞大的遗留代码库上?
- AI 辅助工具应该在建议或强制执行超越机械格式化的惯用法方面,扮演什么角色?
关键要点
- 把风格当作一个已被自动化解决的问题来对待,这样人力就能专注于评审设计和正确性。
- 采用现有的社区标准,只记录增量差异。
- 在编辑器、提交前钩子和 CI 这几个层面强制执行,以 CI 作为权威关卡。
- 让规则集保持精简、站得住脚、由中心统一治理。
- 命名和惯用法,而不是间距,才是真正赢得可读性的地方。
参考文献与延伸阅读
- Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship
- Andrew Hunt and David Thomas, The Pragmatic Programmer
- Steve McConnell, Code Complete
- Dustin Boswell and Trevor Foucher, The Art of Readable Code
- Kevlin Henney (ed.), 97 Things Every Programmer Should Know
- Google, Google Engineering Practices and language style guides(作为参考范例)