1.2 你的意图是什么
为什么要努力编写整洁和可维护的代码?为什么要如此关注健壮性呢?这些答案的核心在于沟通。你不是在交付一个静态的系统。代码将继续变化。你还必须考虑到维护人员会随着时间的推移而变化。在编写代码时,你的目标是交付价值。另外,也需要考虑让其他开发人员也能以同样快的速度交付价值。为了做到这一点,你需要能够在不与未来的维护人员见面的情况下交流和展示你的意图。
让我们看看在一个虚构的遗留系统中找到的代码块。我想让你估计一下你需要多长时间来理解这段代码在做什么。如果你对这里的所有概念都不熟悉,或者你觉得这个代码很复杂,也没关系。
这个函数获取一个食谱,并调整每种配料以匹配新的“食用量”。但是,此代码中有许多问题:
•pop用来干什么?
•recipe[0]指的是什么?为什么这是旧的食用量?
•为什么需要一个注释来提醒我使用容易衡量的数字?
如果你觉得有必要重写代码,我不会怪你。这样写看起来更好:
那些喜欢整洁代码的人可能更喜欢第二个版本。没有原始循环,变量不会变化。我要返回一个字典,而不是一个元组列表。这种情况下,所有这些变化都可以被视为积极的。但是,我刚刚可能引入了三个不易发现的缺陷:
•在最初的代码片段中,我清除了原始代码,现在则不是了。即使只有一个区域的调用代码依赖于这种行为,我也打破了那个调用代码的假设。
•通过返回一个字典,列表中不会再重复出现配料。这可能会对多个部分(例如主菜和酱汁)使用了相同配料的食谱产生影响。
•如果任何配料被命名为“servings”,就产生了一个命名上的冲突。
这些是否是缺陷取决于两点:原作者的意图和代码调用。作者的本意是解决问题,但我不确定他们为什么要以这种方式写代码。为什么它们会弹出元素?为什么“servings”在列表里面是一个元组?为什么要使用一个列表?根据推测,代码原作者知道原因,并将其传达给了办公室的同事。他们的同事根据这些假设写了调用代码,但随着时间的推移,原来的意图已经不清楚了。如果不能与未来的开发人员沟通,要维护这个代码就只剩下了两个选择。
•查看所有的调用代码,确保这个操作在实施之前是互不依赖的。如果这是一个有外部调用者的库的公共API,那就祝你好运。我会花很多时间来做这件事,这将使我感到沮丧。
•做出改变,然后等着看后果是什么(客户投诉、测试被破坏,等等)。如果幸运的话,不会发生什么糟糕的事;如果不幸运的话,就会需要花大量的时间来修复用例,这将使我感到沮丧。
就维护而言,这两种选择都不见得有什么成效(特别是在必须要修改这段代码的时候)。我不想浪费时间,想尽快处理好当前的任务,然后继续下一个任务。如果考虑如何调用这段代码,情况会变得更糟。想一想你是如何与以前未见过的代码交互的。你可能会看到其他调用代码的例子,复制它们以适应你的用例,并且从未意识到你需要传递一个叫作“servings”的特定字符串作为列表的第一个元素。
这些都是会让你感到烦恼的决定。我们都在更大的代码库中看到过它们。它们并不是恶意编写的,而是随着时间的推移,有机地维持着最好的意图。函数开始时很简单,但随着用例的增加和多个开发人员的贡献,这些代码往往会变形并掩盖了最初的意图。这是可维护性正在受到损害的明显标志。你需要在你的代码中预先表达出你的意图。
那么,假如原作者使用了更好的命名模式和更好的类型呢?这段代码会是什么样子?
这看起来要好得多,编写得更好,并且清楚地表达了原始意图。原来的开发人员把他们的想法直接编写到代码中。从这段代码中,你知道了以下情况:
•我正在使用一个Recipe类。这使得我可以抽象出某些操作。根据推测,在这个类里面有一个不变量,允许重复的配料(我将在第10章中更多地讨论类和不变量)。这里提供了一个通用的词汇,使函数的行为更加明确。
•食用量现在是Recipe类的一个明确的部分,而不需要成为列表中的第一个元素,后者是作为一种特殊情况处理的。这大大简化了调用代码,并防止无意中的冲突。
•很明显,我想把旧食谱上的配料清除掉。至于我为什么需要做一个.pop(0)则没有任何的模糊理由。
•配料是一个单独的类,并且处理分数(https://oreil.ly/YxUHK)而不是一个显式浮点数。所有参与者都更清楚地知道我在处理分数单位,并且可以很容易地做一些事情,比如limit_denominator(),当人们想要限制测量单位时,可以调用它(而不是依靠一个注释)。
我已经用类型代替了变量,如食谱类型和配料类型。我还定义了一些操作(clear_ingredients、adjust_proportion)、来表达我的意图。通过这些修改,我使代码对未来的读者来说非常清晰。未来的开发人员不再需要和我讨论才能理解这些代码。相反,他们不用跟我讨论就能理解我在做什么。这就是异步沟通的最佳方式。
异步沟通
在一本Python书中写到异步沟通而不提及async和await是很奇怪的。但我恐怕要在一个更复杂的地方讨论异步沟通:现实世界。
异步沟通意味着信息的产生和消费是相互独立的。在生产和消费之间有一个时间差,这可能是几个小时,就像合作者在不同时区的情况一样,也可能是几年,因为未来的维护者会试图对代码的内部运作进行深入的研究。你无法预测什么时候有人会需要了解你的逻辑,在他们使用你生成的信息时,你甚至可能已经不在那个代码库上(或为那个公司)工作了。
与异步沟通不同,同步沟通是指实时的思想交流。这种形式的直接沟通是表达想法最好的方式之一,但不幸的是,它无法扩展,你也不能一直在那回答问题。
为了评估每种沟通方式在试图理解意图时是否合适,让我们看看两个影响因素:距离和成本。
距离指的是为了使沟通富有成效,沟通者在时间上需要有多近。有些沟通方法更擅长实时传递信息。其他的沟通方式在几年后仍然会很有效。
成本是衡量沟通投入的标准。你必须权衡沟通所花费的时间和金钱与所提供的价值。然后,未来开发者必须权衡消费信息的成本与他们想要交付的价值。编写代码而不提供任何其他沟通渠道是你的底线,你必须这样做才能创造价值。为了评估额外的沟通渠道的成本,以下是我考虑的因素:
可发现性
在正常工作流程之外找到这些信息的容易性如何?这些信息的生命周期有多长?搜索信息容易吗?
维护成本
信息的准确性如何?它需要多久更新一次?如果这些信息过时了,会出现什么问题呢?
生产成本
有多少时间和金钱投入沟通?
在图1-1中,我根据自己的经验绘制了一些常用的沟通方式所需的成本和距离。
图1-1:不同沟通方式的成本-距离图
成本-距离图有四个象限。
低成本,近距离
这种沟通方式的生产和消费成本都很低,但不能跨时间扩展。直接沟通和即时回复就是很好的例子。将这些视为及时的信息,它们只有在用户积极倾听时才有价值。不要依赖这些方式来与未来维护者沟通。
高成本,近距离
这种沟通方式成本高昂,而且往往只发生一次(如会议或会面)。这种沟通方式应该在交流时传递大量的价值,因为它们不会为未来提供太多的价值。你参加了多少次浪费时间的会议?你会直接感受到价值的损失。每一位参会者都需要成倍的成本(所花费的时间、空间、后勤保障工作等)来进行交谈。代码检查很少在完成后立即得到关注。
高成本,远距离
这是非常昂贵的沟通方式,但是因其远距离的特点,随着时间的推移,这些成本可以在价值交付中得到回报。电子邮件和敏捷看板中包含了大量的信息,但其他人是无法发现的。这对于不需要频繁更新的大型概念来说非常有用。要从所有的干扰因子中筛选出你想要的信息,将会是一场噩梦。视频记录和设计文档对于及时理解信息非常有用,但维持的成本很高,不要依赖这些沟通方式来理解日常的决策。
低成本,远距离
这种沟通方式的成本很低,而且很容易使用。代码注释、版本控制历史记录和项目文档都属于这一类,因为它们与我们编写的源代码相邻,用户在多年后仍可以查看。开发人员在日常工作流程中遇到的任何事情都是可以被发现的。这些沟通方式很自然地适用于有人负责源代码的地方。然而,你的代码是你最好的文档工具之一,因为它是系统的记录和真实原理的唯一来源。
讨论
图1-1是基于通用的用例创建的。考虑你和你的组织使用的沟通方式,你会把它们画在图中的哪个位置?获取准确的信息有多容易?生产信息的成本有多高?根据你对这些问题的答案,可能会得到一个稍有不同的图,但真实原理的唯一来源将出现在你交付的可执行软件中。
低成本、远距离的沟通方式是与未来沟通的最佳工具。你们应该努力将沟通的生产成本和消耗降到最低。无论如何,你必须编写能够交付价值的软件,所以成本最低的选择是将代码作为主要的沟通工具。你的代码库将成为清晰地表达你的决定、意见和变通方法的最佳选择。
然而,要使这个断言成立,代码也必须是低成本的。你的意图必须在代码中清晰地表达出来。你的目标是尽量减少读者理解代码所需的时间。理想情况下,读者不需要阅读你的代码实现,而只需阅读你的函数签名。通过使用良好的类型、注释和变量名,代码的功能就会非常清晰。
自文档化的代码
对于图1-1的错误反应是“我们只需要自文档化的代码”。代码绝对应该自我记录正在做的事情,但不能覆盖到每一个用例中去。例如:版本控制将提供更改的历史记录;设计文档讨论的是不局限于任何一个代码文件的全局设想;会议(如果安排得当)可以是执行计划的重要事件;交流协商非常适合与大量听众同时分享想法。虽然本书专注于告诉你在代码中可以做什么事,但不要丢弃任何其他有价值的沟通方式。