警惕单一目标的人
纯粹得让人心疼
单一职责原则听起来非常合理,以至于它可能绕过你的判断。
做一件事。把它做好。保持模块专注。给代码一个改变的理由。好建议。
然后有人把这条建议变成了一把尺子,开始宣称任何超过五行的函数都是代码坏味道。
问题不在于SRP。问题在于把“小”当作“内聚”的替代品。
此时你遇到了单一目的人群:那些开发者并非在模块化上错了,而是混淆了有用的边界与最大化的碎片化。

I. 底层的实用思想
理想情况下,在表单中添加一个复选框应该只影响一个文件。而不是跨越5个目录的8个文件……我说的就是你,React/Redux。
当SRP被明智地应用时,它是有帮助的。专注于单一概念任务的代码单元更容易理解。测试可以针对合理的边界行为。清晰的模块使得更改系统的一部分更容易,而不会把应用的其余部分拖进来。
即使是经典的Unix例子也比口号所暗示的更务实。ls列出文件,是的,但它也协调像opendir、readdir、closedir和stat这样的调用。有用的单元不是尽可能小的操作。有用的单元是解决任务的最小内聚事物。
最初的Unix哲学是关于组合和简单性,而不是把一切都简化成一个函数或文件。
这个区别很重要。“一个职责”不等于“一行行为”。
II. 过度抽象:当简单变成混乱
我们的架构师坚持认为任何超过5行的函数都是‘代码坏味道’。我们的代码库现在隐约散发着无知的绝望气息。
失败模式很容易被发现——在它已经让你的这一周变得更糟之后。
代码库有更多文件,但更少结构。每个辅助函数都有自己的辅助函数。每个概念都被拆分到以技术角色命名的文件夹中,而不是产品含义。添加一个复选框需要触及一个组件、一个钩子、一个选择器、一个动作、一个reducer、一个常量、一个测试夹具,以及一个主要为了让导入路径看起来不那么有罪的桶导出。

所有这些纯粹性换来了什么?
- 文件系统碎片: 源码目录膨胀成噩梦般的景观,充斥着无数微小文件,每个文件往往只包含一个孤零零的函数。导航变成了一场洞穴探险。
- 依赖缠结: 导入和导出的网络如此密集,以至于追踪执行流程需要一块大白板和比该功能本身更多的耐心。那些只被导入一次的文件,假装自己可复用。
- 测试陷阱: 测试变得脆弱,成为守护微小实现细节的超具体哨兵。改个函数签名?看着几十个测试像古老陶器一样碎裂。测试套件从安全网变成了雷区。
- 速度消失: 简单的改动扩散成跨多文件的修改史诗。新开发者入职需要花几周时间给他们地图和指南针,只是为了找到这周
UserProfile组件实际在哪里。在“组织”的沉重负担下,前进速度慢如地质运动。
我曾凝视过代码库的深渊:一个直截了当的 100 行功能被分割到 15 个以上的文件中,每个文件都是“纯粹”的小天使,包含一两个函数。试图在脑海中理清那团乱麻的认知爆炸半径,完全抵消了分离带来的任何理论收益。它并没有更简单,只是更分散了。
III. 完美的代价:对开发者的影响
我们花在争论文件结构和命名规范上的时间,比实际交付功能还要多。这就是敏捷?

这种病态的碎片化不仅仅是美学问题。它改变了开发者分配注意力的方式:
生产力黑洞: 别说什么技术债务了;这是通过强迫症式的目录嵌套积累的组织债务。每一次微小的调整都变成穿越抽象层的考古挖掘。时间消失在 cd .. 和 grep 的黑洞里。
测试税: 测试套件非但没有提供信心,反而成为摩擦的来源。数小时的时间浪费在修复因琐碎重构而损坏的测试上——这些测试与它们本该验证的微观细节耦合得太紧。
认知负荷: 人脑能同时处理的不连续信息量是有硬性上限的。强迫开发者从十几个分散的文件中拼凑程序流程,会主动阻碍理解,并使自信的变更更加困难。
IV. 拥抱实用主义:一个务实的替代方案
我建议把两个相关的函数放在同一个文件里。全场反应就像我提议删除预发布环境。 —— 一位正在康复的纯粹主义者读者
逃生出口不是放弃 SRP。答案是在有意义的层次上应用它。
在实践中是这样的:
- 关注内聚性,而非原子性: 将那些一起变化且概念上属于一起的东西分组。一个模块可能处理用户认证的几个相关方面。这没问题。很可能比六个各自包含一个与登录状态相关函数的独立文件更好。
- 保持同类相近: 除非有极其明显、切实的好处——比如实际中真正的可复用性,而不是某个永远不会到来的假设未来——否则不要拆分相关代码。邻近性对理解至关重要。
- 让现实驱动: 根据应用程序的实际功能和流程来组织,而不是根据某种抽象的功能纯粹性理想³。这种结构是让某人更容易还是更难理解和修改
功能X? - 记住人脑: 想想可怜的开发者。什么样的组织能最小化处理代码所需的心智杂耍?为人类理解优化。
- 测试重要的东西: 在合理的边界上编写验证行为的测试,而不是与每个微小函数内部布线紧密焊接的测试。目标是信心,而不仅仅是覆盖率百分比的表演。
目标不是值得写博士论文的理论完美;而是创建你的同事(以及未来的你)能够导航、理解和修改,而不会想放火烧楼的代码。
有时这意味着一个文件是 200 行而不是 50 行。有时一个函数既负责获取数据又负责轻微转换。有时一个类有两个紧密耦合、应该共存的职责。如果这能让整个系统更容易使用,那很可能就是正确的选择。
始终专注于实际问题:
- 新人能找对路吗?
- 我们能修改
X而不破坏不相关的Y吗? - 这个测试真的能告诉我功能是否正常吗?
- 我们是在交付价值,还是仅仅在重新排列文件夹?
V. 结论:培养内聚且可维护的代码
单一职责原则是一个有用的工具。但它不是将代码库粉碎成原子尘埃的指令。像任何工具一样,其价值取决于使用者的判断。
因此,当你遇到那些准备对任何超过三行的函数发动战争的“单一职责之人”时,深吸一口气。记住那个12文件的复选框。
我们的工作不是构建理论上完美无瑕的雪花函数。我们的工作是构建能工作、能解决问题、并且不会惩罚下一个必须修改它的人的软件。
保持务实。关注结果。不要让追求完美纯洁成为可维护代码的敌人。你的理智,以及团队的速度,都依赖于它。
¹ 讽刺的是,在最低层级实现真正的单一职责需要隐藏在其表面之下的巨大复杂性。
² 我们这里讨论的是概念上的纯粹性:即一个函数在逻辑上应该只做“一件事”的想法。不要将其与函数式编程中“纯函数”(无副作用)的概念混淆,这是不同的(尽管有时相关)概念。