小马的世界

读书笔记-软件设计的哲学【17】一致性(Consistency)

2026-06-22 · 9 min read

一致性是降低系统复杂度、让系统行为更加直观的一项强大工具。如果一个系统具有一致性,这意味着相似的事情会以相似的方式完成,而不同的事情则会以不同的方式完成。一致性能够产生认知杠杆:一旦你学会了某种事情在一个地方是如何实现的,你就可以利用这些知识立刻理解其他采用相同方法的地方。如果系统不是以一致的方式实现的,开发者就必须分别学习每一种情况,而这将花费更多时间。

一致性还能减少错误。如果系统缺乏一致性,那么两个场景看起来可能相同,但实际上却不同。开发者可能会看到一个熟悉的模式,并根据过去遇到该模式的经验做出错误的假设。相反,如果系统具有一致性,那么基于相似场景做出的假设通常是安全的。一致性能够让开发者以更快的速度、更少的错误开展工作。

一致性的示例

一致性可以应用于系统的许多层面,下面列举几个例子。

名称(Names)。 第14章已经讨论过以一致方式使用名称所带来的好处。

编码风格(Coding style)。 如今,开发组织通常都会制定风格指南,其约束范围甚至超出了编译器所强制执行的规则。现代风格指南会覆盖许多方面,例如缩进、大括号位置、声明顺序、命名方式、注释规范,以及对某些被认为有风险的语言特性的使用限制。风格指南能够提高代码的可读性,并减少某些类型的错误。

接口(Interfaces)。 一个具有多个实现的接口也是一致性的例子。一旦你理解了该接口的某个实现,那么其他实现也会更容易理解,因为你已经知道它们必须提供哪些功能。

设计模式(Design patterns)。 设计模式是针对某些常见问题所形成的公认解决方案,例如用于用户界面设计的模型-视图-控制器(MVC)模式。如果你能够使用现有设计模式解决问题,那么实现过程通常会更快,更有可能成功,而且代码对阅读者来说也更加直观。第19.5节将对设计模式进行更详细的讨论。

不变式(Invariants)。 不变式是指变量或数据结构始终成立的某种属性。例如,一个存储文本行的数据结构可能会强制规定:每一行都必须以换行符结尾。不变式能够减少代码中需要考虑的特殊情况数量,并使代码行为更容易推理。

保持一致性

一致性很难长期维持,尤其是在多人长期协作开发同一个项目时。一个团队中的成员可能并不了解另一个团队建立的约定。新加入的成员也不知道这些规则,因此他们会无意间违反既有约定,甚至创造出与现有约定冲突的新约定。下面是一些建立和维护一致性的建议。

文档化(Document)。 创建一份文档,列出最重要的整体约定,例如编码风格指南。将这份文档放在开发者容易看到的地方,例如项目 Wiki 中显眼的位置。鼓励新加入团队的人阅读该文档,同时鼓励现有成员每隔一段时间重新阅读一次。许多组织都已经在网上公开发布了自己的风格指南;你可以考虑从这些现成的指南开始。

对于更加局部化的约定,例如不变式,应当在代码中合适的位置进行记录。如果你不把这些约定写下来,那么其他人很难遵守它们。

强制执行(Enforce)。 即使文档写得再好,开发者也很难记住所有约定。强制执行约定的最佳方式,是编写工具自动检查违规情况,并确保代码只有通过检查后才能提交到代码仓库。自动检查器对于底层语法层面的约定尤其有效。

我最近参与的一个项目曾经遇到过行结束符的问题。有些开发者使用 Unix 系统,其文本行以换行符(newline)结束;另一些开发者使用 Windows 系统,其文本行通常以回车符加换行符(carriage-return/newline)结束。如果某位开发者在一个系统上对之前由另一种系统编辑过的文件进行小幅修改,编辑器有时会将文件中所有行结束符替换为当前系统对应的格式。这样看起来就像文件中的每一行都被修改过一样,从而很难识别真正有意义的变更。

我们制定了一项约定:所有文件都只能使用换行符作为行结束符。然而,要确保每位开发者使用的所有工具都遵守这一约定却并不容易。每当有新开发者加入项目时,总会出现一波与行结束符相关的问题,因为他们需要时间来适应这一约定。

最终,我们通过编写一个简短脚本解决了这个问题。该脚本会在代码提交到源代码仓库之前自动执行。它会检查所有被修改过的文件;如果发现任何文件中包含回车符,则终止提交操作。该脚本还可以手动执行,用于修复受影响的文件——将所有回车换行序列替换为单独的换行符。这样不仅立刻消除了问题,也帮助新开发者学习了这项约定。

代码评审同样是强化约定和教育新开发者的重要机会。代码评审者越关注这些细节,团队成员就越快掌握这些约定,代码质量也会越高。

入乡随俗(When in Rome ...)。 所有约定中最重要的一条是:每位开发者都应该遵循那句古老格言——“入乡随俗(When in Rome, do as the Romans do)”。

当你开始修改一个新的文件时,先观察现有代码的组织方式。公有变量和方法是否写在私有成员之前?方法是否按字母顺序排列?变量命名使用的是“驼峰命名法(camel case)”,例如 firstServerName,还是“蛇形命名法(snake_case)”,例如 first_server_name

只要你看到任何看起来可能是一种约定的东西,就遵循它。当你需要做设计决策时,先问自己:项目中是否可能已经在其他地方做过类似决策?如果答案是肯定的,就找到一个现有示例,并在你的新代码中采用同样的方法。

不要改变现有约定(Don’t change existing conventions)。 抵制“改进”现有约定的冲动。“我有一个更好的想法”并不足以成为引入不一致性的理由。

你的新想法也许确实更好,但一致性所带来的价值,几乎总是大于某种实现方式相对于另一种实现方式的优势。在引入不一致行为之前,先问自己两个问题。

第一,你是否掌握了足以证明自己方案合理的重要新信息,而这些信息在旧约定建立时并不存在?

第二,新方案是否好到值得花费时间去修改所有旧代码中的相关用法?

如果你的组织对这两个问题的回答都是“是”,那么就进行升级改造吧;并且在完成后,不应再留下任何旧约定的痕迹。

不过,即便如此,你仍然面临一个风险:其他开发者可能并不了解新的约定,因此未来他们可能会再次引入旧做法。总体而言,重新审视并推翻既有约定,通常并不是对开发时间的最佳利用。

做得过头(Taking it too far)

一致性不仅意味着相似的事情应该以相似的方式完成,也意味着不同的事情应该以不同的方式完成。如果你对一致性过于执着,试图把本质不同的事物强行套入同一种模式,例如对实际上完全不同的对象使用同一个变量命名方式,或者将某个并不适用的设计模式硬套到一个任务上,那么你反而会制造复杂性和混乱。

只有当开发者能够确信“如果它看起来像 x,那么它就真的是 x”时,一致性才会带来价值。

结论

一致性是“投资思维(investment mindset)”的另一个典型例子。为了确保一致性,需要投入额外的工作:制定约定、创建自动化检查工具、在编写新代码时寻找可借鉴的类似场景,以及通过代码评审来教育团队成员。

这些投入所带来的回报是:你的代码会更加直观。开发者将能够更快、更准确地理解代码的行为,从而以更高的效率、更少的缺陷开展工作。