It is a living document, last update: September 2024
这是一个活文档,最后更新:2024 年 9 月
There are so many buzzwords and best practices out there, but let's focus on something more fundamental. What matters is the amount of confusion developers feel when going through the code.
有很多流行词和最佳实践,但让我们关注一些更基本的东西。重要的是开发人员在阅读代码时感到的困惑程度。
Confusion costs time and money. Confusion is caused by high cognitive load. It's not some fancy abstract concept, but rather a fundamental human constraint.
混淆会浪费时间和金钱。混淆是由高认知负荷引起的。这不是某种华丽的抽象概念,而是一个基本的人类限制。
Since we spend far more time reading and understanding code than writing it, we should constantly ask ourselves whether we are embedding excessive cognitive load into our code.
由于我们花费更多时间阅读和理解代码而不是编写代码,我们应该不断问自己是否在代码中嵌入了过多的认知负担。
Cognitive load is how much a developer needs to think in order to complete a task.
认知负荷是开发人员完成任务所需思考的程度。
When reading code, you put things like values of variables, control flow logic and call sequences into your head. The average person can hold roughly four such chunks in working memory. Once the cognitive load reaches this threshold, it becomes much harder to understand things.
在阅读代码时,你需要将变量的值、控制流逻辑和调用序列等信息放入脑中。普通人可以在工作记忆中大约保持四个这样的块。一旦认知负荷达到这个阈值,理解事物就变得更加困难。
Let's say we have been asked to make some fixes to a completely unfamiliar project. We were told that a really smart developer had contributed to it. Lots of cool architectures, fancy libraries and trendy technologies were used. In other words, the author had created a high cognitive load for us.
假设我们被要求对一个完全不熟悉的项目进行一些修复。我们被告知有一位非常聪明的开发者为此做出了贡献。使用了许多酷炫的架构、花哨的库和时尚的技术。换句话说,作者给我们创造了很高的认知负担。
We should reduce the cognitive load in our projects as much as possible.
我们应该尽可能减少项目中的认知负担。
The tricky part is that the author may not have experienced a high cognitive load due to familiarity with the project.
棘手的部分是,作者可能由于对项目的熟悉而没有经历高认知负荷。
Familiarity vs Simplicity
熟悉度与简单性
The problem is that familiarity is not the same as simplicity. They feel the same — that same ease of moving through a space without much mental effort — but for very different reasons. Every “clever” (read: “self-indulgent”) and non-idiomatic trick you use incurs a learning penalty for everyone else. Once they have done that learning, then they will find working with the code less difficult. So it is hard to recognise how to simplify code that you are already familiar with. This is why I try to get “the new kid” to critique the code before they get too institutionalised!
问题在于,熟悉并不等同于简单。它们感觉相似——在一个空间中轻松移动而不需要太多的心理努力——但原因却截然不同。你使用的每一个“聪明的”(即:“自我放纵的”)和非惯用的技巧都会给其他人带来学习成本。一旦他们完成了这种学习,他们会发现与代码的工作变得不那么困难。因此,很难识别出如何简化你已经熟悉的代码。这就是为什么我试图让“新来的孩子”在他们过于适应之前对代码进行批评!
It is likely that the previous author(s) created this huge mess one tiny increment at a time, not all at once. So you are the first person who has ever had to try to make sense of it all at once.
很可能之前的作者们是一次一个小增量地创建了这个巨大的混乱,而不是一次性完成的。因此,你是第一个必须尝试一次性理清这一切的人。
In my class I describe a sprawling SQL stored procedure we were looking at one day, with hundreds of lines of conditionals in a huge WHERE clause. Someone asked how anyone could have let it get this bad. I told them: “When there are only 2 or 3 conditionals, adding another one doesn’t make any difference. By the time there are 20 or 30 conditionals, adding another one doesn’t make any difference!”
在我的课堂上,我描述了我们一天正在查看的一个庞大的 SQL 存储过程,其中在一个巨大的 WHERE 子句中有数百行条件。有人问,怎么会让它变得这么糟。我告诉他们:“当只有 2 或 3 个条件时,添加另一个条件没有任何区别。当有 20 或 30 个条件时,添加另一个条件也没有任何区别!”
There is no “simplifying force” acting on the code base other than deliberate choices that you make. Simplifying takes effort, and people are too often in a hurry.
代码库上没有其他“简化力量”在起作用,只有你所做的故意选择。简化需要努力,而人们往往太匆忙。
Thanks to Dan North for his comment above.
感谢丹·诺斯的上述评论。
Once you onboard new people on your project, try to measure the amount of confusion they have (pair programming may help). If they're confused for more than ~40 minutes in a row - you've got things to improve in your code.
一旦你在项目中引入新成员,尽量衡量他们的困惑程度(结对编程可能会有所帮助)。如果他们连续困惑超过大约 40 分钟 - 说明你的代码需要改进。
Intrinsic - caused by the inherent difficulty of a task. It can't be reduced, it's at the very heart of software development.
内在 - 由任务固有的难度引起。它无法减少,它是软件开发的核心。
Extraneous - created by the way the information is presented. Caused by factors not directly relevant to the task, such as smart author's quirks. Can be greatly reduced. We will focus on this type of cognitive load.
多余的 - 由信息呈现的方式造成。由与任务不直接相关的因素引起,例如聪明作者的怪癖。可以大大减少。我们将重点关注这种类型的认知负荷。
Let's jump straight to the concrete practical examples of extraneous cognitive load.
让我们直接跳到额外认知负荷的具体实际例子。
P.S. contributions are welcome!
P.S. 欢迎贡献!
We will refer to the level cognitive load as follows:
我们将以下列方式引用认知负荷水平:
🧠
: fresh working memory, zero cognitive load
🧠
: 新鲜的工作记忆,零认知负担
🧠++
: two facts in our working memory, cognitive load increased
🧠++
: 我们的工作记忆中有两个事实,认知负荷增加
🤯
: working memory overflow, more than 4 facts
🤯
: 工作内存溢出,超过 4 个事实
if val > someConstant // 🧠+
&& (condition2 || condition3) // 🧠+++, prev cond should be true, one of c2 or c3 has be true
&& (condition4 && !condition5) { // 🤯, we are messed up by this point
...
}
Introduce intermediate variables with meaningful names:
引入具有意义的中间变量名称:
isValid = val > someConstant
isAllowed = condition2 || condition3
isSecure = condition4 && !condition5
// 🧠, we don't need to remember the conditions, there are descriptive variables
if isValid && isAllowed && isSecure {
...
}
if isValid { // 🧠+, okay nested code applies to valid input only
if isSecure { // 🧠++, we do stuff for valid and secure input only
stuff // 🧠+++
}
}
Compare it with the early returns:
将其与早期的返回结果进行比较:
if !isValid
return
if !isSecure
return
// 🧠, we don't really care about earlier returns, if we are here then all good
stuff // 🧠+
We can focus on the happy path only, thus freeing our working memory from all sorts of preconditions.
我们可以只关注快乐路径,从而使我们的工作记忆摆脱各种前提条件。
We are asked to change a few things for our admin users: 🧠
我们被要求为我们的管理员用户更改一些内容: 🧠
AdminController extends UserController extends GuestController extends BaseController
Ohh, part of the functionality is in BaseController
, let's have a look: 🧠+
哦,部分功能在 BaseController
中,让我们来看看: 🧠+
Basic role mechanics got introduced in GuestController
: 🧠++
基本角色机制在 GuestController
: 🧠++
中引入
Things got partially altered in UserController
: 🧠+++
在 UserController
中部分更改了 : 🧠+++
Finally we are here, AdminController
, let's code stuff! 🧠++++
最后我们到了这里, AdminController
,让我们开始编码吧! 🧠++++
Oh, wait, there's SuperuserController
which extends AdminController
. By modifying AdminController
we can break things in the inherited class, so let's dive in SuperuserController
first: 🤯
哦,等等,还有 SuperuserController
扩展了 AdminController
。通过修改 AdminController
,我们可以破坏继承类中的内容,所以我们先深入 SuperuserController
: 🤯
Prefer composition over inheritance. We won't go into detail - there's plenty of material out there.
更倾向于组合而非继承。我们不打算详细讨论——外面有很多相关资料。
Method, class and module are interchangeable in this context
在此上下文中,方法、类和模块是可以互换的
Mantras like "methods should be shorter than 15 lines of code" or "classes should be small" turned out to be somewhat wrong.
像“方法应该少于 15 行代码”或“类应该很小”这样的口号实际上是有些错误的。
Deep module - simple interface, complex functionality
深度模块 - 简单接口,复杂功能
Shallow module - interface is relatively complex to the small functionality it provides
浅模块 - 接口相对于其提供的小功能来说相对复杂
Having too many shallow modules can make it difficult understand the project. Not only do we have to keep in mind each module responsibilities, but also all their interactions. To understand the purpose of a shallow module, we first need to look at the functionality of all the related modules. 🤯
拥有过多的浅层模块会使理解项目变得困难。我们不仅需要记住每个模块的职责,还要了解它们之间的所有交互。要理解一个浅层模块的目的,我们首先需要查看所有相关模块的功能。 🤯
Information hiding is paramount, and we don't hide as much complexity in shallow modules.
信息隐藏至关重要,我们在浅层模块中并没有隐藏太多复杂性。
I have two pet projects, both of them are somewhat 5K lines of code. The first one has 80 shallow classes, whereas the second one has only 7 deep classes. I haven't been maintaining any of these projects for one year and a half.
我有两个个人项目,它们的代码行数都在 5000 行左右。第一个项目有 80 个浅层类,而第二个项目只有 7 个深层类。我已经有一年半没有维护这些项目了。
Once I came back, I realised that it was extremely difficult to untangle all the interactions between those 80 classes in the first project. I would have to rebuild an enormous amount of cognitive load before I could start coding. On the other hand, I was able to grasp the second project quickly, because it had only a few deep classes with a simple interface.
一旦我回来,我意识到理清第一个项目中那 80 个类之间的所有交互是极其困难的。在我开始编码之前,我必须重建大量的认知负担。另一方面,我能够迅速掌握第二个项目,因为它只有几个深层类,且接口简单。
The best components are those that provide powerful functionality yet have simple interface.
最佳组件是那些提供强大功能但界面简单的组件。
John K. Ousterhout 约翰·K·奥斯特豪特
The interface of the UNIX I/O is very simple. It has only five basic calls:
UNIX I/O 的接口非常简单。它只有五个基本调用:
open(path, flags, permissions)
read(fd, buffer, count)
write(fd, buffer, count)
lseek(fd, offset, referencePosition)
close(fd)
A modern implementation of this interface has hundreds of thousands of lines of code. Lots of complexity is hidden under the hood. Yet it is easy to use due to its simple interface.
该接口的现代实现包含数十万行代码。许多复杂性隐藏在背后。然而,由于其简单的接口,使用起来很容易。
This deep module example is taken from the book A Philosophy of Software Design by John K. Ousterhout. Not only does this book cover the very essence of complexity in software development, but it also has the greatest interpretation of Parnas' influential paper On the Criteria To Be Used in Decomposing Systems into Modules. Both are essential reads. Other related readings: It's probably time to stop recommending Clean Code, Small Functions considered Harmful, Linear code is more readable.
这个深度模块示例摘自约翰·K·奥斯特豪特的书《软件设计的哲学》。这本书不仅涵盖了软件开发中复杂性的本质,还对帕纳斯的影响力论文《分解系统为模块时使用的标准》进行了最好的解读。这两本书都是必读书籍。其他相关阅读:可能是时候停止推荐《整洁代码》、《小函数有害》和《线性代码更易读》。
If you think we are rooting for bloated God objects with too many responsibilities, you got it wrong.
如果你认为我们支持责任过多的臃肿神对象,那你就错了。
All too often, we end up creating lots of shallow modules, following some vague "a module should be responsible for one, and only one, thing" principle. What is this blurry one thing? Instantiating an object is one thing, right? So MetricsProviderFactoryFactory
seems to be just fine. The names and interfaces of such classes tend to be more mentally taxing than their entire implementations, what kind of abstraction is that? Something went wrong.
我们常常创建许多浅层模块,遵循某种模糊的“一个模块应该只负责一件事”原则。这个模糊的“一件事”是什么?实例化一个对象算是一件事,对吧?所以 MetricsProviderFactoryFactory
似乎没问题。这种类的名称和接口往往比它们的整个实现更让人费脑,这是什么样的抽象?出了点问题。
We make changes to our systems to satisfy our stackeholders and users. We are responsible to them.
我们对我们的系统进行更改,以满足我们的利益相关者和用户。我们对他们负责。
A module should be responsible to one, and only one, user or stackeholder.
一个模块应该只对一个用户或利益相关者负责。
Uncle Bob 叔叔鲍勃
This is what this Single Responsibility Principle is all about. Simply put, if we introduce a bug in one place, and then two different business people come to complain, we've violated the principle. It has nothing to do with the number of things we do in our module.
这就是单一职责原则的全部内容。简单来说,如果我们在一个地方引入了一个错误,然后两个不同的业务人员来投诉,我们就违反了这个原则。这与我们在模块中做的事情数量无关。
But even now, this interpretation can do more harm than good. This rule can be understood in as many different ways as there are individuals. A better approach would be to look at how much cognitive load it all creates. It's mentally demanding to remember that change in one module can trigger a chain of reactions across different business streams. And that's about it.
但即使现在,这种解释可能弊大于利。这个规则可以被理解为与个体数量一样多的不同方式。更好的方法是考虑它所产生的认知负担有多大。记住一个模块中的变化可以引发不同业务流中的一系列反应,这在心理上是很有挑战性的。就这些。
This shallow-deep module principle is scale-agnostic, and we can apply it to microservices architecture. Too many shallow microservices won't do any good - the industry is heading towards somewhat "macroservices", i.e., services that are not so shallow (=deep). One of the worst and hardest to fix phenomena is so-called distributed monolith, which is often the result of this overly granular shallow separation.
这个浅深模块原则与规模无关,我们可以将其应用于微服务架构。过多的浅层微服务没有任何好处——行业正朝着某种“宏服务”发展,即服务不那么浅(=深)。最糟糕且最难修复的现象之一是所谓的分布式单体,这通常是过于细粒度的浅层分离的结果。
I once consulted a startup where a team of three developers introduced 17(!) microservices. They were 10 months behind schedule and appeared nowhere close to the public release. Every new requirement led to changes in 4+ microservices. Diagnostic difficulty in integration space skyrocketed. Both time to market and cognitive load were unacceptably high. 🤯
我曾咨询过一家初创公司,那里有一个由三名开发人员组成的团队引入了 17 个(!)微服务。他们的进度落后了 10 个月,似乎离公开发布还远得很。每一个新需求都导致 4 个以上微服务的变更。集成空间的诊断难度飙升。上市时间和认知负担都高得不可接受。 🤯
Is this the right way to approach the uncertainty of a new system? It's enormously difficult to elicit the right logical boundaries in the beginning. The key is to make decisions as late as you can responsibly wait, because that is when you have the most information on which to base the decision. By introducing a network layer we make our design decisions hard to revert right from the start. The team's only justification was: "The F(M)AANG companies proved microservices architecture to be effective". Hello, you got to stop dreaming big.
这是应对新系统不确定性的正确方法吗?在开始时,确定正确的逻辑边界是非常困难的。关键是尽可能晚地做出决策,因为那时你拥有最多的信息来支持决策。通过引入网络层,我们从一开始就使我们的设计决策难以回退。团队唯一的理由是:“F(M)AANG 公司证明了微服务架构是有效的。”你好,你得停止做白日梦。
The Tanenbaum-Torvalds debate argued that Linux's monolithic design was flawed and obsolete, and that a microkernel architecture should be used instead. Indeed, the microkernel design seemed to be superior "from a theoretical and aesthetical" point of view. On the practical side of things - three decades on, microkernel-based GNU Hurd is still in development, and monolithic Linux is everywhere. This page is powered by Linux, your smart teapot is powered by Linux. By monolithic Linux.
塔内鲍姆-托瓦尔兹辩论认为,Linux 的单体设计存在缺陷且过时,应该使用微内核架构。实际上,从“理论和美学”的角度来看,微内核设计似乎更优越。在实际方面——三十年过去了,基于微内核的 GNU Hurd 仍在开发中,而单体 Linux 无处不在。此页面由 Linux 提供支持,你的智能茶壶也由 Linux 驱动。由单体 Linux 提供支持。
A well-crafted monolith with truly isolated modules is often much more flexible than a bunch of microservices. It also requires far less cognitive effort to maintain. It's only when the need for separate deployments becomes crucial (e.g. development team scaling) that you should consider adding a network layer between the modules (future microservices).
一个精心构建的单体应用,具有真正隔离的模块,通常比一堆微服务更灵活。它所需的维护认知负担也要小得多。只有在对单独部署的需求变得至关重要时(例如,开发团队扩展),你才应该考虑在模块之间添加一个网络层(未来的微服务)。
We feel excited when new features got released in our favourite language. We spend some time learning these features, we build code upon them.
我们在最喜欢的语言中发布新功能时感到兴奋。我们花一些时间学习这些功能,并在其基础上编写代码。
If there are lots of features, we may spend half an hour playing with a few lines of code, to use one or another feature. And it's kind of a waste of time. But what's worse, when you come back later, you would have to recreate that thought process!
如果有很多功能,我们可能会花半个小时玩几行代码,以使用某个功能。这有点浪费时间。但更糟糕的是,当你稍后再回来时,你必须重新创建那个思维过程!
You not only have to understand this complicated program, you have to understand why a programmer decided this was the way to approach a problem from the features that are available. 🤯
您不仅需要理解这个复杂的程序,还需要理解为什么程序员决定以这种方式从可用的特性来解决问题。 🤯
These statements are made by none other than Rob Pike.
这些声明是由罗布·派克本人所作。
Reduce cognitive load by limiting the number of choices.
通过限制选择的数量来减少认知负担。
Language features are OK, as long as they are orthogonal to each other.
语言特性是可以的,只要它们彼此正交。
Thoughts from an engineer with 20 years of C++ experience ⭐️
拥有 20 年 C++经验的工程师的想法 ⭐️
I was looking at my RSS reader the other day and noticed that I have somewhat three hundred unread articles under the "C++" tag. I haven't read a single article about the language since last summer, and I feel great!
我前几天在看我的 RSS 阅读器时注意到,在“C++”标签下我有大约三百篇未读文章。自去年夏天以来,我没有读过关于这门语言的任何文章,我感觉很好!
I've been using C++ for 20 years for now, that's almost two-thirds of my life. Most of my experience lies in dealing with the darkest corners of the language (such as undefined behaviours of all sorts). It's not a reusable experience, and it's kind of creepy to throw it all away now.
我已经使用 C++ 20 年了,这几乎是我一生的三分之二。我的大部分经验都在处理语言中最黑暗的角落(例如各种未定义行为)。这不是一种可重用的经验,现在把它全部抛弃有点可怕。
Like, can you imagine, the token
||
has a different meaning in requires ((!P<T> || !Q<T>))
and in requires (!(P<T> || Q<T>))
. The first is the constraint disjunction, the second is the good-old logical OR operator, and they behave differently.就像,你能想象,令牌
||
在 requires ((!P<T> || !Q<T>))
和 requires (!(P<T> || Q<T>))
中有不同的含义。第一个是约束析取,第二个是老牌的逻辑或运算符,它们的行为不同。You can't allocate space for a trivial type and just
memcpy
a set of bytes there without extra effort - that won't start the lifetime of an object. This was the case before C++20. It was fixed in C++20, but the cognitive load of the language has only increased.您无法为一个简单类型分配空间并仅在其中
memcpy
一组字节而不付出额外的努力——这不会启动对象的生命周期。这在 C++20 之前是这样的。在 C++20 中修复了这个问题,但语言的认知负担只增加了。Cognitive load is constantly growing, even though things got fixed. I should know what was fixed, when it was fixed, and what it was like before. I am a professional after all. Sure, C++ is good at legacy support, which also means that you will face that legacy. For example, last month a colleague of mine asked me about some behaviour in C++03.
🤯
认知负荷不断增加,即使问题已经解决。我应该知道是什么被修复了,何时修复的,以及之前是什么样的。毕竟我是一名专业人士。当然,C++在遗留支持方面表现良好,这也意味着你将面临这些遗留问题。例如,上个月我的一位同事问我关于 C++03 的一些行为。
🤯
There were 20 ways of initialization. Uniform initialization syntax has been added. Now we have 21 ways of initialization. By the way, does anyone remember the rules for selecting constructors from the initializer list? Something about implicit conversion with the least loss of information, but if the value is known statically, then...
🤯
有 20 种初始化方式。已添加统一初始化语法。现在我们有 21 种初始化方式。顺便问一下,有人还记得从初始化列表中选择构造函数的规则吗?关于隐式转换时信息损失最小的某些内容,但如果值是静态已知的,那么……
🤯
This increased cognitive load is not caused by a business task at hand. It is not an intrinsic complexity of the domain. It is just there due to historical reasons (extraneous cognitive load).
这种增加的认知负担并不是由当前的业务任务引起的。它不是该领域内在的复杂性。它只是由于历史原因而存在(外部认知负担)。
I had to come up with some rules. Like, if that line of code is not as obvious and I have to remember the standard, I better not write it that way. The standard is somewhat 1500 pages long, by the way.
我不得不制定一些规则。比如,如果那行代码不够明显,我必须记住标准,我最好不要那样写。顺便说一下,标准大约有 1500 页长。
By no means I am trying to blame C++. I love the language. It's just that I am tired now.
我绝不是在指责 C++。我喜欢这门语言。只是我现在感到疲惫。
On the backend we return:
在后端我们返回:
401
for expired jwt token
401
用于过期的 jwt 令牌
403
for not enough access
403
访问权限不足
418
for banned users 418
对于被禁止的用户
The guys on the frontend use backend API to implement login functionality. They would have to temporarily create the following cognitive load in their brains:
前端的同事们使用后端 API 来实现登录功能。他们必须在脑中暂时创建以下认知负荷:
401
is for expired jwt token // 🧠+
, ok just temporary remember it
401
是用于过期的 jwt 令牌 // 🧠+
,好的,只是暂时记住它
403
is for not enough access // 🧠++
403
是因为访问权限不足 // 🧠++
418
is for banned users // 🧠+++
418
是针对被禁止用户的 // 🧠+++
Frontend developers would (hopefully) introduce some kind numeric status -> meaning
dictionary on their side, so that subsequent generations of contributors wouldn't have to recreate this mapping in their brains.
前端开发人员(希望如此)会在他们的端引入某种 numeric status -> meaning
字典,以便后续的贡献者不必在脑海中重新创建这种映射。
Then QA people come into play:
"Hey, I got 403
status, is that expired token or not enough access?"
QA people can't jump straight to testing, because first they have to recreate the cognitive load that the guys on the backend once created.
然后 QA 人员开始介入:“嘿,我得到了 403
状态,这是过期的令牌还是权限不足?”QA 人员不能直接进行测试,因为他们首先必须重现后端人员曾经创建的认知负担。
Why hold this custom mapping in our working memory? It's better to abstract away your business details from the HTTP transfer protocol, and return self-descriptive codes directly in the response body:
为什么要将这个自定义映射保存在我们的工作内存中?最好将您的业务细节从 HTTP 传输协议中抽象出来,并直接在响应体中返回自描述代码:
{
"code": "jwt_has_expired"
}
Cognitive load on the frontend side: 🧠
(fresh, no facts are held in mind)
前端的认知负荷: 🧠
(新鲜,没有事实被记住)
Cognitive load on the QA side: 🧠
QA 方面的认知负荷: 🧠
The same rule applies to all sorts of numeric statuses (in the database or wherever) - prefer self-describing strings. We are not in the era of 640K computers to optimise for memory.
相同的规则适用于所有类型的数字状态(在数据库或其他地方) - 更倾向于自描述字符串。我们不再处于 640K 计算机的时代,不需要为内存进行优化。
People spend time arguing between
401
and403
, making decisions based on their own mental models. New developers are coming in, and they need to recreate that thought process. You may have documented the "whys" (ADRs) for your code, helping newcomers to understand the decisions made. But in the end it just doesn't make any sense. We can separate errors into either user-related or server-related, but apart from that, things are kind of blurry.
人们花时间在401
和403
之间争论,根据自己的心理模型做出决策。新开发者正在加入,他们需要重新建立这种思维过程。您可能已经记录了代码的“原因”(ADRs),帮助新来者理解所做的决策。但最终这并没有任何意义。我们可以将错误分为用户相关或服务器相关,但除此之外,事情就有点模糊。
P.S. It's often mentally taxing to distinguish between "authentication" and "authorization". We can use simpler terms like "login" and "permissions" to reduce the cognitive load.
附注:区分“身份验证”和“授权”往往会给人带来心理负担。我们可以使用更简单的术语,如“登录”和“权限”,来减轻认知负担。
Do not repeat yourself - that is one of the first principles you are taught as a software engineer. It is so deeply embedded in ourselves that we can not stand the fact of a few extra lines of code. Although in general a good and fundamental rule, when overused it leads to the cognitive load we can not handle.
不要重复自己——这是作为软件工程师时你所学到的第一个原则之一。这个原则深深植根于我们心中,以至于我们无法忍受多出几行代码的事实。尽管通常这是一个良好且基本的规则,但过度使用时会导致我们无法承受的认知负担。
Nowadays, everyone builds software based on logically separated components. Often those are distributed among multiple codebases representing separate services. When you strive to eliminate any repetition, you might end up creating tight coupling between unrelated components. As a result changes in one part may have unintended consequences in other seemingly unrelated areas. It can also hinder the ability to replace or modify individual components without impacting the entire system. 🤯
如今,每个人都基于逻辑上分离的组件构建软件。这些组件通常分布在多个代码库中,代表不同的服务。当你努力消除任何重复时,可能会导致无关组件之间的紧密耦合。因此,某一部分的更改可能会对其他看似无关的领域产生意想不到的后果。这也可能妨碍在不影响整个系统的情况下替换或修改单个组件的能力。 🤯
In fact, the same problem arises even within a single module. You might extract common functionality too early, based on perceived similarities that might not actually exist in the long run. This can result in unnecessary abstractions that are difficult to modify or extend.
实际上,即使在单个模块内也会出现相同的问题。您可能会基于可能在长期内并不存在的表面相似性过早提取公共功能。这可能导致不必要的抽象,难以修改或扩展。
Rob Pike once said:
罗布·派克曾说过:
A little copying is better than a little dependency.
小的复制总比小的依赖要好。
We are tempted to not reinvent the wheel so strong that we are ready to import large, heavy libraries to use a small function that we could easily write by ourselves. Make informed decisions about when to import external libraries and when it is more appropriate to write concise, self-contained code snippets to accomplish smaller tasks.
我们常常被诱惑,不想重新发明轮子,以至于愿意导入大型、沉重的库来使用一个我们自己可以轻松编写的小函数。要明智地决定何时导入外部库,以及何时更适合编写简洁、自包含的代码片段来完成较小的任务。
Frameworks evolve at their own pace, which in most cases doesn't match the lifecycle of our project.
框架以自身的节奏发展,这在大多数情况下与我们项目的生命周期不匹配。
By relying too heavily on a framework, we force all upcoming developers to learn that framework first (or its particular version). Even though frameworks enable us to launch MVPs in a matter of days, in the long run they tend to add unnecessary complexity and cognitive load.
通过过度依赖框架,我们迫使所有即将到来的开发者首先学习该框架(或其特定版本)。尽管框架使我们能够在几天内推出最小可行产品,但从长远来看,它们往往会增加不必要的复杂性和认知负担。
Worse yet, at some point frameworks can become a significant constraint when faced with a new requirement that just doesn't fit the architecture. From here onwards people end up forking a framework and maintaining their own custom version. Imagine the amount of cognitive load a newcomer would have to build (i.e. learn this custom framework) in order to deliver any value. 🤯
更糟糕的是,在某些时候,当面临一个根本不符合架构的新需求时,框架可能会成为一个重要的限制。从这里开始,人们最终会分叉一个框架并维护自己的自定义版本。想象一下,新来者为了提供任何价值所需承担的认知负担(即学习这个自定义框架)。 🤯
By no means do we advocate to invent everything from scratch!
我们绝对不提倡从头开始发明一切!
We can write code in a somewhat framework-agnostic way. The business logic should not reside within a framework; rather, it should use the framework's components. Put a framework outside of your core logic. Use the framework in a library-like fashion. This would allow new contributors to add value from day one, without the need of going through debris of framework-related complexity first.
我们可以以一种相对框架无关的方式编写代码。业务逻辑不应存在于框架内;相反,它应使用框架的组件。将框架放在核心逻辑之外。以类库的方式使用框架。这将使新贡献者从第一天起就能增加价值,而无需先经历与框架相关的复杂性。
There is a certain engineering excitement about all this stuff.
关于这些东西,有一种特定的工程兴奋感。
I myself was a passionate advocate of Onion Architecture for years. I used it here and there and encouraged other teams to do so. The complexity of our projects went up, the sheer number of files alone had doubled. It felt like we were writing a lot of glue code. On ever changing requirements we had to make changes across multiple layers of abstractions, it all became tedious. 🤯
我自己多年来一直是洋葱架构的热情倡导者。我在这里和那里使用它,并鼓励其他团队这样做。我们的项目复杂性上升,文件数量翻了一番。感觉我们在编写大量的胶水代码。面对不断变化的需求,我们不得不在多个抽象层中进行更改,这一切变得乏味。 🤯
Jumping from call to call to read along and figure out what goes wrong and what is missing is a vital requirement to quickly solve a problem. With this architecture’s layer uncoupling it requires an exponential factor of extra, often disjointed, traces to get to the point where the failure occurs. Every such trace takes space in our limited working memory. 🤯
从一个调用跳到另一个调用,以便跟踪并找出问题出在哪里以及缺少什么,是快速解决问题的一个重要要求。由于这种架构的层解耦,需要大量额外的、通常是分散的追踪信息,才能找到故障发生的点。每个这样的追踪都占用我们有限的工作记忆空间。 🤯
This architecture was something that made intuitive sense at first, but every time we tried applying it to projects it made a lot more harm than good. In the end, we gave it all up in favour of the good old dependency inversion principle. No port/adapter terms to learn, no unnecessary layers of horizontal abstractions, no extraneous cognitive load.
这种架构最初看起来很直观,但每次我们尝试将其应用于项目时,造成的伤害远大于好处。最后,我们放弃了一切,选择了老旧的依赖倒置原则。没有需要学习的端口/适配器术语,没有不必要的水平抽象层,没有多余的认知负担。
Do not add layers of abstractions for the sake of an architecture. Add them whenever you need an extension point that is justified for practical reasons. Layers of abstraction aren't free of charge, they are to be held in our working memory.
不要为了架构而添加抽象层。只有在需要出于实际原因的扩展点时才添加它们。抽象层并不是免费的,它们会占用我们的工作记忆。
Even though these layered architectures have accelerated an important shift from traditional database-centric applications to a somewhat infrastructure-independent approach, where the core business logic is independent of anything external, the idea is by no means novel.
尽管这些分层架构加速了从传统的以数据库为中心的应用程序向某种基础设施独立的方法的重要转变,其中核心业务逻辑独立于任何外部因素,但这个想法绝非新颖。
These architectures are not fundamental, they are just subjective, biased consequences of more fundamental principles. Why rely on those subjective interpretations? Follow the fundamentals instead: dependency inversion principle, isolation, single source of truth, true invariant, complexity, cognitive load and information hiding.
这些架构并不是基本的,它们只是更基本原则的主观、偏见的结果。为什么要依赖这些主观的解释?请遵循基本原则:依赖倒置原则、隔离、单一真相来源、真正的不变性、复杂性、认知负荷和信息隐藏。
Domain-driven design has some great points, although it is often misinterpreted. People say "We write code in DDD", which is a bit strange, because DDD is about problem space, not about solution space.
领域驱动设计有一些很好的观点,尽管它常常被误解。人们说“我们在 DDD 中编写代码”,这有点奇怪,因为 DDD 是关于问题空间的,而不是关于解决方案空间的。
Ubiquitous language, domain, bounded context, aggregate, event storming are all about problem space. They are meant to help us learn the insights about the domain and extract the boundaries. DDD enables developers, domain experts and business people to communicate effectively using a single, unified language. Rather than focusing on these problem space aspects of DDD, we tend to emphasise particular folder structures, services, repositories, and other solution space techniques.
无处不在的语言、领域、边界上下文、聚合、事件风暴都与问题空间有关。它们旨在帮助我们了解领域的见解并提取边界。DDD 使开发人员、领域专家和商业人士能够使用一种统一的语言进行有效沟通。我们往往强调特定的文件夹结构、服务、存储库和其他解决方案空间技术,而不是关注 DDD 的这些问题空间方面。
Chances are that the way we interpret DDD is likely to be unique and subjective. And if we build code upon this understanding, i.e., if we create a lot of extraneous cognitive load - future developers are doomed. 🤯
我们对 DDD 的理解很可能是独特且主观的。如果我们在这种理解基础上编写代码,即如果我们创造了大量额外的认知负担——未来的开发者将会陷入困境。 🤯
Imagine for a moment that what we inferred in the second chapter isn’t actually true. If that’s the case, then the conclusion we just negated, along with the conclusions in the previous chapter that we had accepted as valid, might not be correct either. 🤯
想象一下,如果我们在第二章中推断的内容实际上并不正确。那么,刚刚被否定的结论,以及我们在前一章中接受为有效的结论,可能也不正确。 🤯
Do you feel it? Not only do you have to jump all over the article to get the meaning (shallow modules!), but the paragraph in general is difficult to understand. We have just created an unnecessary cognitive load in your head. Do not do this to your colleagues.
你感受到了吗?你不仅需要在整篇文章中跳来跳去才能理解意思(浅显的模块!),而且整体段落也很难理解。我们刚刚在你的脑海中增加了不必要的认知负担。不要这样对待你的同事。
We should reduce any cognitive load above and beyond what is intrinsic to the work we do.
我们应该减少任何超出我们所做工作的内在认知负担。
Follow on Twitter, GitHub or connect on LinkedIn
在 Twitter、GitHub 上关注或在 LinkedIn 上连接