DanLevy.net

警惕单一目标的人

纯粹得让人心疼

单一职责原则听起来非常合理,以至于它可能绕过你的判断。

做一件事。把它做好。保持模块专注。给代码一个改变的理由。好建议。

然后有人把这条建议变成了一把尺子,开始宣称任何超过五行的函数都是代码坏味道。

问题不在于SRP。问题在于把“小”当作“内聚”的替代品。

此时你遇到了单一目的人群:那些开发者并非在模块化上错了,而是混淆了有用的边界与最大化的碎片化。

软件架构中的暴力
组件,无处不在的组件

I. 底层的实用思想

理想情况下,在表单中添加一个复选框应该只影响一个文件。而不是跨越5个目录的8个文件……我说的就是你,React/Redux。

当SRP被明智地应用时,它是有帮助的。专注于单一概念任务的代码单元更容易理解。测试可以针对合理的边界行为。清晰的模块使得更改系统的一部分更容易,而不会把应用的其余部分拖进来。

即使是经典的Unix例子也比口号所暗示的更务实。ls列出文件,是的,但它也协调像opendirreaddirclosedirstat这样的调用。有用的单元不是尽可能小的操作。有用的单元是解决任务的最小内聚事物。

最初的Unix哲学是关于组合简单性而不是把一切都简化成一个函数或文件。

这个区别很重要。“一个职责”不等于“一行行为”。

II. 过度抽象:当简单变成混乱

我们的架构师坚持认为任何超过5行的函数都是‘代码坏味道’。我们的代码库现在隐约散发着无知的绝望气息。

失败模式很容易被发现——在它已经让你的这一周变得更糟之后。

代码库有更多文件,但更少结构。每个辅助函数都有自己的辅助函数。每个概念都被拆分到以技术角色命名的文件夹中,而不是产品含义。添加一个复选框需要触及一个组件、一个钩子、一个选择器、一个动作、一个reducer、一个常量、一个测试夹具,以及一个主要为了让导入路径看起来不那么有罪的桶导出。

无法逃脱这种无限工作模式
组件,无处不在的组件

所有这些纯粹性换来了什么?

我曾凝视过代码库的深渊:一个直截了当的 100 行功能被分割到 15 个以上的文件中,每个文件都是“纯粹”的小天使,包含一两个函数。试图在脑海中理清那团乱麻的认知爆炸半径,完全抵消了分离带来的任何理论收益。它并没有更简单,只是更分散了。

III. 完美的代价:对开发者的影响

我们花在争论文件结构和命名规范上的时间,比实际交付功能还要多。这就是敏捷?

乱到近乎艺术
乱到近乎艺术

这种病态的碎片化不仅仅是美学问题。它改变了开发者分配注意力的方式:

生产力黑洞: 别说什么技术债务了;这是通过强迫症式的目录嵌套积累的组织债务。每一次微小的调整都变成穿越抽象层的考古挖掘。时间消失在 cd ..grep 的黑洞里。

测试税: 测试套件非但没有提供信心,反而成为摩擦的来源。数小时的时间浪费在修复因琐碎重构而损坏的测试上——这些测试与它们本该验证的微观细节耦合得太紧。

认知负荷: 人脑能同时处理的不连续信息量是有硬性上限的。强迫开发者从十几个分散的文件中拼凑程序流程,会主动阻碍理解,并使自信的变更更加困难。

IV. 拥抱实用主义:一个务实的替代方案

我建议把两个相关的函数放在同一个文件里。全场反应就像我提议删除预发布环境。 —— 一位正在康复的纯粹主义者读者

逃生出口不是放弃 SRP。答案是在有意义的层次上应用它。

在实践中是这样的:

目标不是值得写博士论文的理论完美;而是创建你的同事(以及未来的你)能够导航、理解和修改,而不会想放火烧楼的代码。

有时这意味着一个文件是 200 行而不是 50 行。有时一个函数既负责获取数据又负责轻微转换。有时一个类有两个紧密耦合、应该共存的职责。如果这能让整个系统更容易使用,那很可能就是正确的选择。

始终专注于实际问题:

V. 结论:培养内聚且可维护的代码

单一职责原则是一个有用的工具。但它不是将代码库粉碎成原子尘埃的指令。像任何工具一样,其价值取决于使用者的判断。

因此,当你遇到那些准备对任何超过三行的函数发动战争的“单一职责之人”时,深吸一口气。记住那个12文件的复选框。

我们的工作不是构建理论上完美无瑕的雪花函数。我们的工作是构建能工作、能解决问题、并且不会惩罚下一个必须修改它的人的软件。

保持务实。关注结果。不要让追求完美纯洁成为可维护代码的敌人。你的理智,以及团队的速度,都依赖于它。

¹ 讽刺的是,在最低层级实现真正的单一职责需要隐藏在其表面之下的巨大复杂性。

² 我们这里讨论的是概念上的纯粹性:即一个函数在逻辑上应该只做“一件事”的想法。不要将其与函数式编程中“纯函数”(无副作用)的概念混淆,这是不同的(尽管有时相关)概念。