将代码放置在尽可能靠近它所相关的地方
翻译自 Kent C. Dodds 发表的 Colocation。
我们都想拥有易于维护的代码库,因此我们开始好心好意地使我们的代码库(或代码库中属于我们的角落)可维护且易于理解。随着时间的推移,随着代码库的增长,管理依赖项(JS、CSS、图像等)可能变得越来越困难。
随着项目的增长,越来越多的代码变成“部落知识”(只有你或其他几个人知道的知识),而这种知识又导致“技术债务”(无论该术语是否准确)。
我喜欢保持我的代码库易于管理,不仅对我(编写它的人),还有我的队友、未来的维护者以及 6 个月后的我自己。我想我们大家都同意这是一个伟大的理想,我们应该在我们的代码库中去为之努力。 有很多不同的工具和技术可供我们使用来做到这一点。
让我们谈谈代码注释
我并不想讨论是否应该为你的代码写注释(你应该的),以及你的注释应该写些什么(你应该在注释中解释为什么你在做一些预期之外的事情,以便后来的人可以理解这些意外或奇怪的代码背后所做的决定)。
(好吧,也许我确实还是想谈一点)。我想关注的是,那些代码注释放在哪里。我们通常将这些注释与它们所解释的代码“共置”,也就是尽可能靠近相关代码放置。
考虑一下,假如我们换种方式。假如我们把那些注释放在一个完全独立的文件中。一个巨大的 DOCUMENTATION.md
文件,或者甚至一个跟 src/
目录映射对应的 docs/
目录。听上去有意思吗?对吧,我也觉得没意思。不将我们的注释与它解释的代码共置,会让我们遇到一些严重的问题。
- 可维护性: 他们会更快地失去同步或过时(比现在还快)。我们会移动或删除
src/
里的文件文件而不更新相应的docs/
文件。 - 适用性: 在
src/
中查看代码的人可能会错过 docs/ 中的重要注释,或者他们会不注释他们自己的代码,因为他们没有意识到他们编辑的src/
文件有一个对应的docs/
文件存在。 - 易用性: 在这种设置下,从一个位置到下一个位置的上下文切换也将是一种挑战。不得不处理多个文件的位置可能难以确保你拥有维护一个组件所需要的所有东西。
我们当然也可以为这种代码注释想出一个约定风格,但我们为什么要这样做?把注释跟它们所解释的代码放在一块不是更简单吗?
所以呢?
现在,你可能在想:“是的,呃,这就是为什么没有人做 docs/
这个东西,每个人都将他们的评论与代码就放在一起。这很明显。你的观点是什么?” 我的观点是共置的好处无处不在。
HTML/视图
以 HTML 为例。共置在注释中的所有好处也体现在我们的模板中。在像 React 这样的现代框架之前,你的视图逻辑和视图模板位于完全不同的目录中。这成为上述问题的牺牲品。如今,更常见的做法是将这些东西放在同一个文件中,例如通过 React 和 Vue。在 Angular 中,如果它不在同一个文件中,模板文件至少是紧邻与之关联的 JS 文件的。
CSS
另一个适用的概念是 CSS。我不打算和你争论关于 CSS-in-JS 的优点(它太棒了),但它真的棒出天际。 点此了解更多。
测试
这种文件共置的概念也适用于单元测试。代码库有一个 src/
目录又有一个满是单元测试的 test/
目录试图镜像 src/
目录是不是很常见?
所有上面描述的隐患也适用于此。我大概不至于把单元测试完全放在同一个文件中,但我并不完全排除它是一个有趣的想法(实现留给读者作练习)。
为了帮助实现更易于维护的代码库,我们应该将我们的测试文件与他们正在测试的文件/文件们共置。
这确保了当新的人们(或 6 个月后的我)来到代码面前,他们可以立即看到该模块已经过测试,并可以使用这些测试作为参考来了解这个模块。 当他们进行变更时,它会提醒他们更新(添加/删除/修改)测试来解释它们的变更。
状态
应用/组件状态感受到相同的好处。你的状态越是疏远使用它的 UI,它就越难维护。
状态本地化的收益不止于可维护性,它还可以提高应用程序的性能。
应用组件树中一个角落的状态变化比树顶的状态变化引起的重渲染要少得多。本地化你的状态。
“可复用的”工具文件
这也适用于“工具”文件和函数。
想象你正在写一个组件并看到一些代码可以提取成自己的函数。你把它提取出来,想道:“嗯…肯定很多人都可以使用它。”所以你把它拉出来放到你应用的 utils/ 目录中然后继续你的生活。
后来,你的组件被删除了,但是你写的工具函数却没人看见,没人想起,被留在那里了(连同它的测试一起),因为删除它的人假设它被更多地方使用到了。多年来,工程师们努力确保函数及其测试运行正常,却完全没有意识到它根本不再需要了。浪费精力和认知负荷。
相反,如果你就把这个函数直接留在使用它的文件中,故事会完全不同。我并不是说不要对复杂的工具函数进行单元测试(是要测的),但是把它们放在更接近它们使用的地方能帮助你避免问题。
看在上帝的份上,请删除这条 ESLINT 规则和所有类似的规则。
原则
共置的概念可以归结为这个基本原则:
将代码放置在尽可能靠近它所相关的地方
你也可以说:“一起变化的东西应该在合理范围内尽可能靠近。”(Dan Abramov 曾对我说了类似的话)。
开源变得(更)容易
除了避免前面讨论的问题外,以这种方式构筑你的项目还有其他好处。
把一个组件转换为开源项目往往只需要将文件夹复制/粘贴到另一个项目并将其发布到 npm。
然后你只需将它安装在你的项目中并更新你的 require/import 语句就可以了。
例外
当然,也有很好的论证关于跨越整个系统/部分系统的文档以及事物如何集成在一起。还有,你把跨组件的集成测试或端到端测试放在哪里?
你可能认为那些是例外,但它们实际上也可以很好地订阅上面提到的原则。
如果我的应用程序的一部分与用户身份验证相关并且我想为该流程写文档,我可以将 README.md 文件放在包含所有与用户身份验证相关的模块的文件夹中。如果我需要编写集成测试该流程,我可以将这些测试的文件也放在这个文件夹中。
对于端到端测试,它们通常放在项目根部更说得通。它们超越了项目本身并进入了系统的其他部分,所以对我来说,将它们放在一个单独的目录中是说得通的。
它们其实并不映射到 src/
中的文件上。事实上,E2E 测试其实完全不关心 src/
是如何组织的。重构和移动 src/
目录中的文件根本不应该需要变更 E2E 测试。
结论
我们的目标是构建尽可能易于维护的软件。我们从共置我们的注释中获得的可维护性、适用性和易用性上的好处,我们也在共置其他事物中获得。如果你从未尝试过,我建议你试一试。
P.S. 如果你担心违反“关注点分离”,我建议你看看 Pete Hunt 的 这段讲话并重新评估它意味着什么 😀。
P.P.S. 我还应该指出,这同样适用于图像以及其他任何资源。当你使用像 webpack 这样的工具,共置这些资源还非常容易。老实说,我认为这是 webpack 的核心价值主张之一。