针对编码和系统的高效之心智模型


这是一篇从心理模型也就是心智模型角度分析编码的文章,比较晦涩难懂,实际上中心意思是,每段代码其实只是人在编写这段代码时的心智模型投射,我们不能把代码看成是客观的存在,而是主观的产物,甚至参合了当时心理活动或各种直觉感知,因此,当我们修改这段代码时,一般也需要将自己的心智模型接近到当时编写这段代码时的场景,这样才能比较无误地修改。

一个多月前,Carmen Andoh在哥本哈根举行的一场关于可视化的聚会上发表了演讲,尽管这真的是关于心理模型的讨论。在她的演讲中,Carmen将心智模型描述为:

心智模型是对某人在现实世界中如何运作的思维过程的解释。它代表了周围的世界,它的各个部分之间的关​​系,以及一个人对自己的行为及其后果的直觉感知。

在“ 软件设计哲学 ”一书中,John Ousterhout写道:

复杂性不是由单一的灾难性错误引起的; 它累积在很多小块中。单个依赖或默默无闻本身不太可能显着影响软件系统的可维护性。复杂性的产生是因为随着时间的推移,成百上千的小依赖性和晦涩难关。最终,有这么多小问题,系统的每一个可能的变化都会受到其中几个的影响。

复杂性的增量性质使其难以控制。很容易让自己相信,你当前的变化带来的一点点复杂性并不重要。但是,如果每个开发人员都采用这种方法进行每次更改,则复杂性会迅速累积 一旦复杂性积累起来,就很难消除,因为修复单个依赖或默默无闻本身并不会产生很大的影响。

随着越来越多的代码被添加或修改到代码库,大量开发人员处理大型代码库是不可避免的。当涉及到系统时,问题就更加严重,因为我们现在不只处理代码,还处理由不同团队(甚至组织)编写和运行的各种系统,其中构建和运行这些系统的心智模型不是'经过充分验证或沟通,当然理想情况下是受欢迎的。

也就是说,我确实认为有一些指导原则,如果在编写代码时遵循,可以帮助缓解未来阅读代码者的认知负担,并温和地引导他们达到编写者一个心理模型的近似值。

优化可理解性
如果我要绘制需求的层次结构,但是对于代码库,我会将可理解性放在金字塔的最底层。

1. 可维护性
2.可扩展性
3.可测试性
4.可调试性
5.可理解性

可理解性是一段代码可读性最基本的需求之一。优化可理解性可以导致优化上述层次结构中的所有其他内容。可理解的代码是适合调试的代码。代码通常可以使用测试用例进行调试。易于测试的代码通常是模块化和可扩展的代码。模块化和可扩展代码也是通常可维护的代码。
这里需要注意的是,这不适用于代码。它同样适用于系统,但在系统的情况下,系统的可理解程度往往是可观察的程度。

确定所有不同的目标受众
优化可理解性的关键因素是为正在编写的代码提供明确定义的目标受众。单个代码库可以包含多个组件(库,二进制文件,配置文件等),每个组件可以满足不同的目标受众。不考虑各种目标受众的(可能是对立的)需求可能导致使用API​​的尴尬。
在有库包的情况下,目标受众包括库包的所有不同的潜在客户。对于类似语言的标准库,目标受众包括整个开发人员社区。对于纯粹关注某个业务逻辑功能的库,目标受众可能包含团队或组织中的其他开发人员。
对于我编写的代码类型,我还需要了解运营商的经验,这表明它对负责代码操作的人的易用性(在某些情况下,运营商可能是SRE,在其他情况下,运营商可能是编写代码的软件工程师。
一般而言,在确定目标受众并决定他们需要接触哪些叙述以便让他们快速启动和运行时,有必要考虑受众的背景,领域专业知识和经验水平。通常情况下,目标受众可能很好地包括新手和退伍军人。在这种情况下,对代码可理解性的试金石是由新手的经验决定的。当与一个特定的目标受众打交道,重要的是这些API它们暴露目前,感觉自然的词汇它们,即使不一定是语义的实现者可能是最舒服的(有内部和外部的API这种情况可以提供很多缓解)。

代码的自我记录是一个神话
关于自我记录代码的优缺点已经写了很多。根据我的经验,代码自我记录的问题在于它不允许传播编写代码的上下文。代码是团队理解问题,与用户沟通,以及在各种竞争约束和要求之间达成妥协的工件的最终结果。代码本身让读者知道做出了哪些决定,但不是为什么这样做。
而且,只有一些人可以用代码捕获意图; 试图窃取代码中的每一个上下文都会导致代码看起来笨重且难以理解(考虑代码具有非常冗长和不直观的变量或函数名称,无数间无意义的间接或思想层数由破碎的代码片段表示永远不会真正走到一起,完整地重建原始想法)。
虽然代码不是设计文档,但在阅读代码时,它有助于了解某些上下文。当出于性能原因以某种方式编写某段代码,或者为了使代码更具可扩展性,或者在时间和预算限制以及流动性要求下编写代码时,尤其如此。这个上下文对于未来的代码读者非常有价值,可以理解代码诞生的环境以及代码存在的原因,然后为未来的读者铺平道路,为代码如何最好地进化做出明智的选择。

注释比代码更容易解​​析
自我记录代码的支持者也不支持在代码中包含任何注释。试图自我记录的代码是像散文一样阅读的代码。短的词语和函数名,易于加工代码。
必须从头开始构建一个心智模型(更不用说管理现有实现的模型),如果只有纯代码帮助他,对于代码库的新人来说,这不是一个非常好的用户体验。代码注释评论是有关代码上下文的心智模型的接口,可以这么说。它告诉我“是什么和为什么”。在最好的情况下,评论和代码是互补的。最好的评论是那些使实现(代码) 更容易理解的评论。

更好的层组合
设计良好的代码库通常由层和子系统组成,每个层提供尽可能接近密封的抽象,同时向用户公开简单的接口。
但是,在设计层时,同样重要的是要注意这些层的组合可以显着地塑造代码库整体的可理解性。将软件分解为层次应该反映出这样一个事实,即最强大的心智模型是直观的,模块化的,层次化的, 但却适合于广度优先的探索。
例如,大多数开发人员发现同步 API比异步API更容易使用。同步API为用户提供了一个时间接口,这种接口直观且易于推理。像Go这样的语言成功地封装了语言运行时的底层异步,为最终用户提供了一个非常简单的API来编程。
层次可以以广度优先的方式进行探索也是至关重要的,程序员希望只需要进行浅层搜索即可熟悉更高级别的API。这使他们能够立即开始有效地使用更高级别的API,这可以使他们更快地生产。读者应该进入实施细节的唯一原因是他们需要更改底层实现。

每一层都应该封装一个“错误内核”
对于处理纯粹业务逻辑的系统(比方说,支付或预订航班),错误内核是有问题的业务逻辑。对于网络代理,错误内核包含处理网络级故障的代码。
在设计软件层时,每个层都封装了一个错误内核。作为一个层的用户,重要的是我可以获得一个API,它可以在快乐路径中抽象出大部分实现的复杂性。

不要为了共同理解而忽略细节
有时,团队中的某些成员对代码库或系统的某个部分有共同的理解。这可能是由于各种原因造成的。可能是因为这些团队成员在团队中的时间比其他团队长得多,或者因为他们拥有特定的领域专业知识。这也可能是因为代码库如此庞大,以至于团队中的每个人都不能同时掌握它的每个部分。在某些公司,有人被指定为“代码所有者”,他们“拥有”的代码的所有更改都需要得到他们的批准。
如果某些人“拥有”代码的更高级别接口易于理解和易于使用,我并不反对拥有代码所有者的概念。有时,团队中的某些人最终会比其他人更熟悉代码库的某些部分,这是不可避免的。
对某些人来说,“共享理解”对他人来说是“隐藏的知识”,对于新的代码读者来说,可能会变成“未知 - 未知”。一段代码的原始实现者需要花费一定的时间和精力来构建创建它的心智模型,如果没有记录,那么接触代码的下一个程序员不仅需要花费同样(如果不是更多)努力尝试理解它是如何工作的,他们也冒险离开并理解它们认为完整,但没有意识到必需品已被无意中隐瞒了。当它与上述错误内核有关时,这可能被证明是灾难性的。
此外,正如本文中已经提到的那样,即使那些对一段代码或系统具有“共同理解”的人也会拥有相同的心智模型。记录假设被分享并因此被视为“隐含”的知识可以帮助发现令人惊讶的细微差别和有关“共享感知”的细节,这些感知可能最初可能是共享的,但最终已经失去了时间。在我的经验中,错误的代码过度沟通方面从未出现过错误。

隐式假设和依赖关系是明确的
隐含的假设和依赖关系是导致代码模糊的最严重的违规者之一。

具体实现比抽象更好
在编写代码或设计层时,思考如何为新用户提供具体实在的内容会有很大帮助。这可以是代码的示例用法,也可以是某个行为何时生效或模块的某个病态用例可能是什么的解释。

比较,对比和重新校准心理模型的验证
混沌工程只是验证的一种形式,可以导致心智模型的重新校准。其他形式的验证有助于在代码和算法级别重新校准,包括正式规范,基于属性的测试和模糊测试。

在设计接口时,重要的是要考虑系统的不同目标受众,并了解每个目标受众可能需要根据他们的需求定制自定义接口。

结论
Barbara Liskov关于编程的观点:
最好的代码就像是一篇好文章。它需要一个精心实现的结构; 每个字都应该起作用。以这种方式编程需要与读者同理心。它还意味着将代码视为达到目的的手段,而不仅仅是作为工件本身。

代码的质量不是由其初始作者而是由未来的代码读者和调试者来判断,因为重构编写代码的心理模型的责任完全落在代码的读者身上。

减少未来读者的认知负担并帮助他们建立更好的代码心智模型,最大限度地降低了引入错误的风险,为下一代维护者提供了快速进展的能力。它还有助于建立一种在管理复杂性方面向前推进的文化,有效地分摊代码库的维护成本。​​​​​​​