市面上有这么多流行术语和最佳实践,但让我们关注一些更基本的东西,重要的是开发人员在浏览代码时感到的困惑程度。
- 混乱会浪费时间和金钱。
- 混乱是由高认知负荷引起的。
它不是一些花哨的抽象概念,而是一种基本的人类约束。
认知负荷
认知负荷是指开发人员为了完成一项任务需要思考的量。
阅读代码时,你会把变量值、控制流逻辑和调用序列等信息记在脑子里。一般人的工作记忆中大约可以容纳四个这样的信息块。一旦认知负荷达到这个阈值,理解事情就会变得更加困难。
假设我们被要求对一个完全不熟悉的项目进行一些修复。我们被告知一位非常聪明的开发人员为该项目做出了贡献。使用了许多很酷的架构、花哨的库和流行的技术。
换句话说,作者给我们带来了很高的认知负荷。
我们应该尽可能的减少项目中的认知负荷。
认知负荷的类型
- 内在性类型:由任务固有的难度引起,它无法降低,它是软件开发的核心。
- 无关类型: 由信息呈现方式产生。由与任务不直接相关的因素(例如聪明作者的怪癖)引起。可以大大减少。我们将重点关注这种类型的认知负荷。
复杂条件
if val > someConstant // + |
引入具有有意义名称的中间变量:
isValid = val > someConstant |
继承噩梦
我们被要求为管理员用户改变一些事情:
AdminController extends UserController extends GuestController extends BaseController |
- 哦,部分功能在BaseController,让我们看一下:+
- 基本角色机制在引入GuestController:++
- 部分内容在改变UserController:+++
- 最后我们到了,AdminController让我们编写代码吧!++++
哦,等等,还有SuperuserControllerwhich extends AdminController。通过修改,AdminController我们可以破坏继承类中的东西,所以让我们SuperuserController先深入研究一下:
优先使用组合而不是继承。
太多小方法、类或模块
在此上下文中,方法、类和模块可以互换
诸如“方法应该少于 15 行代码”或“类应该很小”之类的口号被证明是错误的。
深层模块- 接口简单,功能复杂
浅层模块- 接口相对复杂,功能较少
浅层模块太多会使项目难以理解。我们不仅要记住每个模块的职责,还要记住它们之间的所有交互。要了解浅层模块的用途,我们首先需要查看所有相关模块的功能。
信息隐藏至关重要,我们不会将太多复杂性隐藏在浅层模块中。
我有两个宠物项目,它们都差不多有 5000 行代码。第一个有 80 个浅层类,而第二个只有 7 个深层类。我已经有一年半没有维护过这两个项目了。
回来后,我意识到要理清第一个项目中这 80 个类之间的所有交互非常困难。我必须重建大量的认知负荷才能开始编码。另一方面,我能够快速掌握第二个项目,因为它只有几个深层类和一个简单的界面。
最好的组件是那些提供强大功能且接口简单的组件。
John K. Ousterhout
UNIX I/O 的接口非常简单。它只有五个基本调用:
open(path, flags, permissions) |
该接口的现代实现有数十万行代码。许多复杂性隐藏在幕后。但由于其接口简单,因此易于使用。
这个深度模块示例取自John K. Ousterhout 所著的《软件设计哲学》一书。
功能丰富的语言
当我们最喜欢的语言发布新功能时,我们感到很兴奋。我们花了一些时间学习这些功能,并在此基础上编写代码。
如果有很多功能,我们可能会花半个小时玩几行代码,来使用一个或另一个功能。这有点浪费时间。但更糟糕的是,当你稍后回来时,你必须重新创建那个思考过程!
你不仅要理解这个复杂的程序,还要理解为什么程序员决定采用这种方式从现有的特性中解决问题!发表这些言论的人正是 Rob Pike。
通过限制选择的数量来减少认知负荷。
语言特征是可以的,只要它们彼此正交,来自一位拥有 20 年 C++ 经验的工程师的想法⭐️
分层架构
抽象本应隐藏复杂性,但在这里它只是增加了间接性。从一个调用跳到另一个调用,阅读并找出哪里出了问题以及缺少了什么,这是快速解决问题的必要条件。由于这种架构的层解耦,需要大量额外的、通常是脱节的跟踪才能找到发生故障的点。每个这样的跟踪都会占用我们有限的工作内存空间。
这种架构最初看起来合情合理,但每次我们尝试将其应用于项目时,弊大于利。
最后,我们放弃了它,转而采用老套的依赖倒置原则。无需学习端口/适配器术语,无需不必要的水平抽象层,也无需增加认知负担。
如果您认为这种分层可以让您快速替换数据库或其他依赖项,那您就错了。更改存储会导致很多问题,相信我们,对数据访问层进行一些抽象是您最不用担心的事情。
在最好的情况下,抽象可以节省 10% 的迁移时间(如果有的话),真正的麻烦在于数据模型不兼容、通信协议、分布式系统挑战和隐式接口。
那么,如果未来没有回报,为什么要为这种分层架构付出高认知负荷的代价呢?
不要为了架构而添加抽象层。只要您需要一个扩展点,并且出于实际原因有理有据,就可以添加它们。抽象层不是免费的,它们将保存在我们的工作内存中。
领域驱动设计
领域驱动设计有很多优点,尽管它经常被误解。人们说“我们用 DDD 编写代码”,这有点奇怪,因为 DDD 是关于问题空间的,而不是关于解决方案空间的。
通用语言、领域、划定的界限上下文BC、聚合、事件风暴都与问题空间有关。它们旨在帮助我们了解领域的见解并提取边界。DDD 使开发人员、领域专家和业务人员能够使用单一、统一的语言进行有效沟通。我们倾向于强调特定的文件夹结构、服务、存储库和其他解决方案空间技术,而不是关注 DDD 的这些问题空间方面。
我们对 DDD 的理解方式很可能是独特且主观的:如果我们基于这种理解来编写代码,也就是说,如果我们创建了大量无关的认知负荷,那么未来的开发人员就注定要失败。
示例
- 我们的架构是标准的 CRUD 应用程序架构,是 Postgres 之上的 Python 整体
- Instagram 如何仅靠 3 名工程师就将用户规模扩大到 1400 万
- 我们曾经认为“哇,这些人真是太聪明了”的公司大多都失败了
- 一个功能连接整个系统。如果你想知道系统是如何工作的 -去读它
这些架构相当枯燥且容易理解。任何人都可以不费太多脑力劳动就掌握它们。
让初级开发人员参与架构评审。他们将帮助你确定需要动脑力的领域。
熟悉项目中的认知负荷
如果您已经将项目的心理模型内化到您的长期记忆中,那么您就不会经历较高的认知负荷。
需要学习的心理模型越多,新开发人员创造价值所需的时间就越长。
一旦你让新人加入你的项目,试着衡量他们困惑的程度(结对编程可能会有所帮助)。如果他们连续困惑超过 40 分钟 - 你的代码就有需要改进的地方了。
如果您将认知负荷保持在较低水平,人们可以在加入公司的最初几个小时内为您的代码库做出贡献。
结论
我们应该减少任何超出我们工作本质的认知负荷。
网友:
作者在强调降低认知负荷过程中,无意将问题本身复杂性与解决方案复杂性混同在一起,如果业务本身很复杂,如同飞机制造厂不能于汽修厂同日而语,在汽修厂,任何一个工人可以俯瞰整个工厂流程,随时跳入任何一个流程环境熟练工作,但是在飞机制造厂不可能出现这种情况,如果单纯以某个工人的认知负荷作为判断整个架构依据,那么飞机可能制造不出来了。
在飞机制造厂这个环境上下文中,每个人只能熟悉他有关的生产环节,认知负荷造成了他的认知困境,无法跳出认知陷阱,其实也无需跳出陷阱的必要,做好本职工作即可,如同一个微服务,做好自己那点事情就好。