Skip to content

CHAPTER 8 Style Guides and Rules

kimi edited this page Aug 12, 2021 · 8 revisions

代码风格指南和规则

大多数工程组织都有管理其代码库的规则--关于源文件存储位置的规则,关于代码格式的规则,关于命名和模式以及异常和线程的规则。大多数软件工程师都在一套控制他们如何操作的政策范围内工作。在谷歌,为了管理我们的代码库,我们维护了一套风格指南来定义我们的规则。

规则就是法律。它们不只是建议或推荐,而是严格的、强制性的法律。因此,它们是普遍可执行的--除非在需要使用的基础上被批准,否则规则不能被忽视。与规则相反,指南提供了建议和最佳实践。这些位是好的,甚至是非常值得遵循的,但与规则不同的是,它们通常有一些变化的空间。

我们把我们定义的规则,即写代码必须遵守的 "做"和 "不做",收集在我们的编程风格指南中,这些指南被视为典范。"风格"在这里可能有点名不副实,意味着一个仅限于格式化做法的集合。我们的风格指南不仅仅是这样;它们是管理我们代码的一整套惯例。这并不是说我们的风格指南是严格的规定性的;风格指南的规则可能需要判断,例如使用 "在合理范围内尽可能描述"的名称的规则。相反,我们的风格指南是对我们的工程师负责的规则的最终来源。

我们为谷歌使用的每一种编程语言制定了单独的风格指南。在高层次上,所有的指南都有类似的目标,旨在引导代码开发,并着眼于可持续性。同时,它们之间在范围、长度和内容上也有很大的差异。编程语言有不同的优势,不同的特点,不同的优先级,以及在谷歌不断发展的代码库中采用的不同历史路径。因此,独立定制每种语言的指南要实际得多。我们的一些风格指南是简洁的,专注于一些总体原则,如命名和格式化,正如我们的Dart、R和Shell指南所展示的。其他的风格指南包括更多的细节,深入研究特定的语言特征,并延伸到更长的文件中--特别是我们的C++、Python和Java指南。一些风格指南重视语言的典型非Google使用--我们的Go风格指南非常简短,只在一个摘要指令中增加了一些规则,以遵守外部认可的惯例。其他的规则包括从根本上不同于外部规范的规则;我们的C++规则不允许使用异常,这是一个在Google代码之外广泛使用的语言特性。

即使是我们自己的风格指南也有很大的差异,这使得我们很难准确地描述一个风格指南应该包括什么。指导谷歌风格指南发展的决定源于保持我们代码库可持续发展的需要。其他组织的代码库对可持续发展有不同的要求,因此需要一套不同的定制规则。本章主要从Google的C++、Python和Java风格指南中抽取例子,讨论指导我们制定规则和指南的原则和过程。

为什么有规则?

那么,我们为什么要制定规则?制定规则的目的是为了鼓励 "好"的行为,阻止 "坏"的行为。对 "好"和 "坏"的解释因组织而异,取决于该组织所关心的问题。这种指定不是普遍的偏好;好与坏是主观的,并根据需要而定。对于一些组织来说,"好"可能会促进支持小内存足迹的使用模式,或优先考虑潜在的运行时优化。在其他组织中,"好"可能会促进行使新语言功能的选择。有时,一个组织最关心的是一致性,所以任何与现有模式不一致的东西都是 "坏的"。我们必须首先认识到一个特定的组织所重视的东西;我们使用规则和指导来鼓励和阻止相应的行为。

随着一个组织的成长,既定的规则和指南塑造了编码的共同词汇。一个共同的词汇允许工程师专注于他们的代码需要说什么,而不是他们如何说。通过塑造这种词汇,工程师会倾向于做默认的 "好"事情,甚至是下意识的。因此,规则为我们提供了广泛的杠杆作用,使常见的开发模式朝着理想的方向发展。

创建规则

在定义一套规则时,关键问题不是 "我们应该有什么规则"?要问的问题是,"我们试图推进什么目标?" 当我们专注于规则所要服务的目标时,确定哪些规则支持这一目标,就会更容易提炼出一套有用的规则。在谷歌,风格指南作为编码实践的法律,我们不问:"什么东西进入风格指南?"而是问:"为什么一些东西进入风格指南?" 我们的组织通过拥有一套规范编写代码的规则获得了什么?

指导原则

让我们把事情放在背景中。谷歌的工程组织由超过3万名工程师组成。这些工程人员在技能和背景方面表现出极大的差异性。每天大约有60,000个项目提交到一个超过20亿行的代码库中,而这个代码库可能会存在几十年。我们正在优化一套不同于其他大多数组织需要的价值,但在某种程度上,这些问题是普遍存在的--我们需要维持一个对规模和时间都有弹性的工程环境。

在这种情况下,我们的规则的目标是管理我们的开发环境的复杂性,保持代码库的可管理性,同时仍然允许工程师有成效地工作。我们在这里做了一个权衡:帮助我们实现这一目标的大量规则意味着我们限制了选择。我们失去了一些灵活性,甚至可能会得罪一些人,但权威的标准所带来的一致性和减少冲突的好处是最重要的。

鉴于这种观点,我们认识到一些指导我们规则发展的总体原则,这些原则必须:

  • 承担起自己的责任
  • 对读者进行优化
  • 保持一致
  • 避免容易出错的和令人惊讶的结构
  • 必要时向实际情况让步

规则必须发挥其作用

不是所有的东西都应该被纳入风格指南。要求一个组织中的所有工程师学习和适应任何新的规则,都是一种非零的成本。如果规则太多,不仅工程师在写代码时更难记住所有相关的规则,而且新的工程师也更难学会他们的方法。更多的规则也会使维护规则集变得更具挑战性和更昂贵。

为此,我们特意选择不包括预计不言自明的规则。谷歌的风格指南并不打算用律师的方式来解释;没有明确规定的东西并不意味着它是合法的。例如,C++风格指南没有禁止使用goto的规则。C++程序员已经倾向于避免使用它,所以包括一个明确的规则来禁止它将带来不必要的开销。如果只有一两个工程师犯了错误,通过创建新的规则来增加每个人的心理负担是不合适的。

为读者优化

我们规则的另一个原则是为代码的读者而不是作者进行优化。考虑到时间的流逝,我们的代码被阅读的频率将远远超过它被编写的频率。我们宁可让代码打得繁琐,也不愿意让它难以阅读。在我们的 Python 风格指南中,在讨论条件表达式时,我们认识到它们比 if 语句更短,因此对代码作者来说更方便。然而,由于它们往往比更冗长的 if 语句更难让读者理解,我们限制了它们的使用。我们重视 "简单阅读"而不是 "简单编写"。我们在这里做了一个权衡:当工程师必须为变量和类型反复输入可能更长的描述性名称时,前期成本会更高。我们选择为所有未来的读者提供可读性而支付这一成本。

作为优先级的一部分,我们还要求工程师在他们的代码中留下预期行为的明确证据。我们希望读者在阅读时能够清楚地了解代码在做什么。例如,我们的Java、JavaScript和C++风格指南规定,当一个方法覆盖一个超类方法时,要使用override注解或关键字。如果没有明确的原地设计证据,读者很可能会弄清楚这个意图,尽管这需要每个读者在阅读代码时多花点心思。

当预期行为的证据可能令人惊讶时,它就变得更加重要了。在C++中,仅仅通过阅读一段代码,有时很难追踪指针的所有权。如果一个指针被传递给一个函数,在不熟悉该函数的行为的情况下,我们不能确定会有什么预期。调用者仍然拥有这个指针吗?函数是否拥有所有权?在函数返回后,我可以继续使用这个指针吗,或者它可能已经被删除了?为了避免这个问题,我们的C++风格指南倾向于在打算转移所有权时使用std::unique_ptr。unique_ptr是一个管理指针所有权的结构,确保指针只存在一个副本。当一个函数将unique_ptr作为参数并打算取得指针的所有权时,调用者必须明确地调用移动语义。

// Function that takes a Foo* and may or may not assume ownership of 
// the passed pointer.
void TakeFoo(Foo* arg);

// Calls to the function don’t tell the reader anything about what to 
// expect with regard to ownership after the function returns.
Foo* my_foo(NewFoo());
TakeFoo(my_foo);

将此与以下内容进行比较。

// Function that takes a std::unique_ptr<Foo>.
void TakeFoo(std::unique_ptr<Foo> arg);
// Any call to the function explicitly shows that ownership is 
// yielded and the unique_ptr cannot be used after the function 
// returns.
std::unique_ptr<Foo> my_foo(FooFactory()); TakeFoo(std::move(my_foo));

考虑到风格指南的规则,我们保证所有的调用站点只要适用就会包括所有权转移的明确证据。有了这个信号,代码的读者不需要了解每个函数调用的行为。我们在API中提供了足够的信息来推理它的交互关系。这种在调用地点的清晰的行为文档确保代码片段保持可读性和可理解性。我们的目标是局部推理,目标是清楚地了解在调用地点发生了什么,而不需要寻找和参考其他代码,包括函数的实现。

大多数涉及注释的风格指南规则也是为了支持这一目标,即为读者提供就地证据。文档注释(预先放在某个文件、类或函数上的块状注释)描述了后面代码的设计或意图。实现注释(穿插在代码本身中的注释)证明或强调不明显的选择,解释棘手的部分,并强调代码的重要部分。我们有涵盖这两类注释的风格指南规则,要求工程师提供另一个工程师在阅读代码时可能寻找的解释。

要有一致性

我们对代码库内的一致性的看法与我们对谷歌办公室的理念相似。由于工程人员众多,分布广泛,团队经常被分到不同的办公室,谷歌员工也经常发现自己在其他地方出差。尽管每个办公室都保持其独特的个性,接受当地的风味和风格,但对于完成工作所需的任何事情,都刻意保持一致。一个来访的Googler的徽章可以在当地所有的徽章阅读器上使用;任何谷歌设备都可以获得WiFi;任何会议室的视频会议设置都有相同的界面。Googler不需要花时间学习如何设置这一切;他们知道,无论他们在哪里都是一样的。在办公室之间移动,仍能轻松完成工作。

这就是我们对源代码的追求。一致性是使任何工程师能够跳到代码库中不熟悉的部分并相当迅速地开始工作的原因。一个本地项目可以有它独特的个性,但它的工具是一样的,它的技术是一样的,它的库是一样的,而且它都能工作。

一致性的优势

尽管对于一个办公室来说,不允许定制徽章阅读器或视频会议界面可能会感觉到限制性,但一致性的好处远远超过了我们失去的创造性自由。代码也是如此:保持一致有时可能会感觉受到限制,但这意味着更多的工程师能以更少的努力完成更多的工作。

  • 当一个代码库的风格和规范内部一致时,编写代码的工程师和阅读代码的其他人就可以专注于正在完成的工作,而不是它的呈现方式。在很大程度上,这种一致性允许专家分块。当我们用相同的接口来解决我们的问题,并以一致的方式对代码进行格式化时,专家就更容易扫视一些代码,将注意力集中在重要的部分,并理解它在做什么。这也使得代码的模块化和发现重复的工作变得更加容易。由于这些原因,我们把大量的注意力集中在一致的命名规则上,一致地使用通用模式,以及一致的格式和结构。也有很多规则在一个看似很小的问题上提出了一个决定,仅仅是为了保证事情只用一种方式完成。例如,选择缩进所需的空格数,或对行长的限制。这里有价值的部分是有一个答案的一致性,而不是答案本身。
  • 一致性使扩展成为可能。工具是一个组织进行扩展的关键,一致的代码使得建立能够理解、编辑和生成代码的工具更加容易。如果每个人都有不同的小块代码,那么依赖于统一性的工具的全部好处就无法应用--如果一个工具可以通过添加缺失的导入或删除未使用的包含来保持源文件的更新,如果不同的项目为他们的导入列表选择不同的排序策略,那么这个工具可能无法在任何地方工作。当每个人都在使用相同的组件,当每个人的代码都遵循相同的结构和组织规则时,我们就可以投资于能在任何地方工作的工具,为我们的许多维护任务建立自动化。 如果每个团队都需要单独投资于同一工具的定制版本,为他们独特的环境量身定做,我们就会失去这种优势。
  • 一致性在扩展组织的人力部分时也有帮助。随着组织的发展,从事代码库工作的工程师数量也在增加。尽可能地保持每个人都在工作的代码的一致性,可以更好地跨项目流动,最大限度地减少工程师转换团队的时间,并建立组织的能力,以灵活和适应人数需求的波动。一个不断成长的组织也意味着其他角色的人与代码互动--例如,SRE、库工程师和代码管理员。在谷歌,这些角色往往跨越多个项目,这意味着不熟悉某个团队项目的工程师可能会跳到这个项目的代码上工作。在整个代码库中保持一致的经验使这种做法变得有效。
  • 一致性也确保了对时间的适应性。随着时间的推移,工程师离开项目,新人加入,所有权转移,项目合并或分裂。争取一个一致的代码库可以确保这些转变的成本很低,并允许我们对代码和工作在上面的工程师有几乎不受约束的流动性,简化了长期维护所需的过程。

在规模上
几年前,我们的C++风格指南承诺几乎不会改变会使旧代码不一致的风格指南规则。"在某些情况下,可能有很好的理由来改变某些风格规则,但我们还是要保持原样,以保持一致性。
当代码库比较小的时候,旧的、有灰尘的角落比较少,这是有道理的。 当代码库越来越大,越来越老的时候,这就不再是一个需要优先考虑的问题了。这(至少对我们的C++风格指南背后的仲裁者来说)是一个有意识的变化:当打击这一点时,我们明确指出,C++代码库将永远不会再完全一致,我们甚至也不希望如此。
不仅要根据当前的最佳实践来更新规则,而且还要要求我们将这些规则应用于所有曾经写过的东西,这简直是太大的负担了。我们的大规模变更工具和流程允许我们更新几乎所有的代码,以遵循几乎每一种新的模式或语法,从而使大多数旧的代码表现出最新的批准的风格(见第22章)。然而,这样的机制并不完美;当代码库变得如此之大时,我们不可能确保每一段旧代码都能符合新的最佳实践。要求完美的一致性已经达到了代价太大的地步。

设定标准。当我们提倡一致性时,我们倾向于关注内部一致性。有时,在全球惯例被采用之前,地方性的惯例就已经出现了,而调整所有的东西来适应这种情况是不合理的。在这种情况下,我们提倡一致性的等级制度。"保持一致"从本地开始,一个特定文件的规范先于一个特定的团队的规范,而团队的规范又先于大项目的规范,大项目的规范又先于整个代码库的规范。事实上,风格指南中包含了许多明确服从于本地惯例的规则,重视这种本地一致性而不是科学的技术选择。

然而,对于一个组织来说,仅仅创建并坚持一套内部惯例是不够的。有时,应考虑到外部社区采用的标准。

计算空格
Google 的 Python 风格指南最初规定我们所有的 Python 代码都要缩进两个空格。外部 Python 社区使用的标准 Python 风格指南使用四空格缩进。我们早期的Python开发大多是为了直接支持我们的C++项目,而不是为了实际的Python应用。因此,我们选择使用两空缩进,以便与我们的 C++ 代码保持一致,因为我们的代码已经以这种方式进行了格式化。随着时间的推移,我们发现这个理由其实并不成立。写Python代码的工程师读写其他Python代码的频率比读写C++代码的频率高得多。每次我们的工程师需要查找一些东西或参考外部代码时,我们都要付出额外的努力。每次我们试图将我们的代码输出到开放源码时,我们也会经历很多痛苦,花时间来协调我们内部代码和我们想要加入的外部世界之间的差异。
当Starlark(谷歌设计的一种基于Python的语言,作为构建描述语言)拥有自己的风格指南的时候,我们选择改变为使用四空格缩进,以与外部世界保持一致。

如果惯例已经存在,一个组织与外部世界保持一致通常是个好主意。对于小型的、独立的和短暂的工作来说,这可能不会有什么影响;内部的一致性比在项目的有限范围之外发生的事情更重要。一旦时间的流逝和潜在的扩展成为因素,你的代码与外部项目互动的可能性就会增加,甚至最终出现在外部世界。从长远来看,坚持广泛接受的标准可能会得到回报。

Clone this wiki locally