代码的简洁性:推理与选择
2020年8月7日,作者:Max Kanat-Alexander
任何软件系统最重要的属性之一,是无需运行它就能理解其将要做什么的能力。这个概念通常被称为系统的“可推理性”。本质上,你希望在不先观察系统运行的情况下,就能对其结构、行为和结果做出断言。
要理解这为何重要,可以想象一个包含一百个不同部分的系统。为了简单起见,我们假设它是一个真实的物理系统,而非计算机。比方说我们有一个自动化工厂生产汽车,从原材料到成品车有100个步骤。每个部分都对输入材料进行某种改变,以生产出输出产品。我们可以通过多种方式配置这个系统及其各个部分:我们可以让每个部分执行多个操作,而根据采取的操作不同,我们选择的下一个机器实际上也不同。例如,假设我们正在将金属加工成圆棒。每辆汽车所需圆棒的数量不同,而我们的圆棒可以由5种不同的金属制成。因此,这台机器有一个程序,每次收到一根钢条时,都会决定制造哪种圆棒。这个决定取决于一天中的时间和对我们汽车当前的需求。然后,根据制造出的圆棒类型,该圆棒将被送往五个不同后续机器中的一个。
现在想象一下,整个系统中的每台机器都是如此——它接收一组复杂的输入,产生一组复杂的可能输出,这些输出又被送往一组复杂的可能的下一个机器。这不仅使得人类在任何给定时间都不可能对整个系统的确切行为做出断言(即可推理),甚至连推理单个部分的行为都会变得困难。
现在想象另一种设置:每台机器接收一个输入,提供一个输出,并且每台机器只与另一台机器“对话”(即,其输入始终来自一个特定的机器,其输出总是去往另一个单一的机器)。尽管一次性思考整个系统可能很困难,因为它仍然有100台机器,但很容易查看每个单独的部分,并由此推理出各个部分以及整个系统的逻辑行为。
这是简洁性的核心部分——对类似这样的系统进行推理的能力。当你查看软件系统的任何单独部分时,你应该能够在不运行该部分的情况下,对其行为、保证、结构和潜在结果做出断言。该部分如何与系统的其余部分接口应该非常清楚——我们要么应该确切地知道是什么调用它以及它调用了什么,要么应该理解创造了该部分使用边界的那种结构。例如,这就是为什么许多编程语言中“私有”和“公共”函数的概念能够提升系统可推理性的原因——它们是告诉我们什么可能发生、什么不可能发生的边界。而当你看一个函数或类的实际实现时,通过阅读代码和注释应该很容易理解它正在执行的操作。这就是为什么函数和变量的命名如此重要的原因——因为好的命名允许读者推理系统的行为和边界。
选择
然而,要让系统具备这种质量,还有另一个非常重要的组成部分。为了解释这部分,请想象一下我们假想的汽车工厂中的每台机器都不是自动化的,而是由一个人操作。这更像是一位正在编写实际代码的软件工程师,“运行”着他们的IDE、计算机、编译器、编程语言等“机器”。
在我们第一个例子中,复杂机器做出复杂决策的地方,想象一下,之前自动化机器做出的所有选择,现在必须由一个人来完成。也就是说,每次一块金属进入我们的机器时,一个人必须查看它,决定它是什么类型的金属,决定制造哪种圆棒,这一切都需要根据查询汽车的当前需求并记下时间来完成。在一个真实的工厂里,其中一些或许可以接受。这至少为一个人创造了一份有趣的工作。但即便如此,你也能看到你会为许多错误和不良结果敞开大门。
与我们的后一个例子相比,在那里我们有输入和输出都很简单的简单机器。一个人操作它们会非常容易,你甚至可能让一个人操作多台机器,并且几乎消除了所有出错或产生不良结果的可能性。
现在考虑到在编程中,程序员常常操作着数十个甚至数百个这样的“机器”——就他们维护的类和函数而言。因此,复杂汽车工厂的一个更好的类比是让一个人操作所有一百台机器。正如你所见,如果系统的每个部分都向操作者提供了太多需要做出的决策,制造我们的“汽车”很快就变得不可能。即使你能做到,你的生产速度也会极其缓慢,并且会使操作机器的人员精疲力竭。瞧,这正是那些不得不维护具有那种复杂程度的软件系统的团队所发生的情况。
然而,当我们在“工厂”中加入了人之后,我们引入的关键点是什么?我们引入了决策(人类用头脑做的事情)和选择(呈现给人类的选项)这两个因素。
有些思想流派认为,所有开发人员应该随时被赋予对其软件系统做出每一个可能决策的权力。这听起来很棒,因为这听起来像是为聪明人提供了智力自由——这是我们所有人都想要的。然而,如果你把这个原则推向极致,你实际上最终会为你的开发人员创造一个复杂的汽车工厂——一个需要做出的选择如此之多,以至于他们要么变得瘫痪,要么注定会做错,要么开发出其他人难以理解的极其不一致的系统。
那么,这里的解决方案是什么?是剥夺所有人的所有选择,让他们成为执行首席架构师意志的无头脑的机器人吗?好吧,我相信有些软件架构师会喜欢那样,但实际上,这是一个有点极端的解决方案。答案是要认识到哪些选择对开发人员来说是重要的、需要能够做出的,而哪些是不重要的。
这在软件团队中取决于你的角色以及你在软件生命周期的哪个阶段。例如,如果你刚刚创办一家新公司并且你是第一个开发人员,那么你能够选择公司将要运行的基本平台的几乎所有方面——你使用的语言、框架、库等——是很重要的。但即便如此,你也不希望那些框架和库向你呈现你不需要做出的决定。想象一下,如果一个编译器停下来问你它应该如何优化每一段代码。这会有助于你或提高你的生产力吗?这对你的公司或你试图实现的目标真的会带来净收益吗?我不这么认为。
然后,在项目生命周期的不同阶段,一旦你已经标准化了一种语言和你正在使用的特定框架,你通常不会允许一个随机的初级开发人员为他们的代码库部分选择不同的语言或框架。这是一个他们不需要花时间去做的决定——随大流对他们来说效率更高。即使可能有更好的语言或框架可以使用,仅仅为了实现这位初级开发人员的一个功能而重写整个系统,看起来并不是资源的好用途。
总的来说,如果你能移除足够多开发人员不需要拥有的选择,你实际上可以在整个公司的范围内节省相当多的开发时间。想象一下,如果你公司的每个团队在开始开发系统之前,都必须花两周时间审查不同的框架。现在想象一下,你标准化了一个(即使不完美但)足够好(也就是说,它能够满足所有将要使用它的人的业务需求)的框架,并且没有人再需要做那个决定。你为整个公司节省了多少工程时间?这是巨大的——从长远来看,这比你几乎任何其他的生产力改进都要大。
现在,重要的是要记住,有些决定是开发人员需要做出的。他们绝对需要能够决定其系统的业务逻辑如何运作——这是他们能够开展工作的核心要求。过去有一些框架和库根本不允许人们编写他们需要的系统,这种程度的限制对生产力是有害的。例如,想象一下你的公司标准化了一个支持HTTP但不知何故根本上无法支持SSL(即没有HTTPS)的框架。当你出于安全目的需要加密连接时,那将是灾难性的。所以那将是一个非常糟糕的限制。
有时,这是一条非常微妙的界限,但总的来说,我发现,从长远来看,偏向于删除选择实际上会让开发人员更快乐,因为这使他们更有效率。当你最初从人们那里拿走某些选择时,这非常困难,因为他们感觉你在影响他们的个人自由。在某种程度上,在短期内,你确实是。但事实是,你试图为创造提供更多的自由——从根本上说,这是那个开发人员真正想要的自由。限制选择的目的应始终是提高创建系统的能力。你不是在扼杀生产,你是在以某人根本不需要做出的选择的形式,删除干扰、障碍和困惑。
-Max
评论
Steven Gordon, PhD 说: 2020年8月7日下午2:11
在实践中,几乎总是存在对不透明的遗留子系统的某种依赖,这使得推理和选择这两个目标都难以完全实现。理论上这些目标很好,但你需要足够的容错能力,以便不完全达到这些目标不会威胁导致你陷入欺骗性推理或选择政策过于僵化。
至于推理,我宁愿拥有一套组织良好的自动化测试来理解,而不是整个代码库。自动化测试使得实际验证你的推理所基于的假设变得微不足道,而不会因为那些可能早已离开或隐藏在不透明的遗留子系统中的依赖关系而受到“过于聪明”的开发人员的伤害。
回复
Max Kanat-Alexander 说: 2020年8月8日晚上11:54
我明白你的意思,但我确实认为,开发人员通常有能力将他们正在工作的系统部分变得足够简单以便推理。诚然,底层平台的复杂性可能使这非常具有挑战性,特别是当它们设计时没有考虑这些原则时。但在实际应用中,人们可以设计出系统的各个部分能够以合理准确性被推理的系统。
即使是容错的观点,理想情况下也涉及对系统进行推理的能力——即其在错误条件下的行为。
能够通过测试来推理一个系统是可以的,这是一个非常有用的工具,我同意。它们在验证你的推理方面也做得很好,这是真的。但我希望代码本身,当你查看它时,是预期读者能够推理的东西,并且当集成到更大的系统中时,不会向用户呈现他们不需要做出的选择。
-Max
回复
Kursith 说: 2020年10月10日凌晨12:33
然而现在一切都在自动化。我想知道工程师们该做什么呢?这篇博客对我很有帮助,让我明白技术是随着人类的创造力而发展的。感谢分享知识。它确实让具有机械知识的软件开发人员受益。
回复