EnriqueMark
设计模式与架构

单一职责

2026-07 简体中文

我对单一职责(SRP)的启发式理解是「能单测,没有副作用,一个函数只做一件事」,当然我知道这不完全严谨。但作为快速判断我觉得是好的。

能单测。意味着这个函数不依赖外部状态,没有额外的规则,在纯粹的情况下就可以测试,往往也暗示了此函数已经分离到了相当的程度。

没有副作用。这点不总是绝对的,比如有些函数的合法职责就是带副作用的,像打log或者查数据库。核心点在于——除非这真的是这个函数的合法职责,否则它不应该影响到本该由其他函数做的事情,此时采用委派是最好的。即,委派给一个专职做此时的函数,而不是自己亲自去进行。

只做一件事。这里的一件事,本身不能从字面上理解,而是要考虑抽象的层次。比如在更高的抽象层次,这里的「一件事」能涵盖很多,不如说是一种「职责」。这里的「职责」本来就是蛮抽象的东西,我对其的理解是「职能」。此函数在某一地方发挥的最小的单一功能。

更明确的SRP信号其实是「改变」。比如当未来我进行修改的时候,是不是只会因为一个「原因」去改变他?或者更确切的说,我该这个函数的时候,不会改到不相干的东西,比如改一个查数据库的函数还得估计别把http请求改坏了——拿着就是职责混同。正是因为混了不同的东西,才会因为不同原因来这里改这个函数。而反过来,这种函数也是最难单测试的,因为为了测试它,你得构建不同的环境。所以「按变化原因切分代码」是更强烈的SRP信号,关键在于维护的边界。

还有一种函数的职责是「编排」。对这种函数来说,它就像最终的执行者一样,把好几个不同职责的函数打包在一起执行。要注意,业务逻辑和编排是不一样的。编排只是按照特定的顺序执行,但业务逻辑则会承担诸多状态控制和判断分支,对于后者来说,这完全就是职责不清晰。虽然同样是委派,但如果掺杂了单纯执行之外的其他因素,就可能变成一种包装——本质上只是把业务逻辑扔了出去而已,实际上还是耦合的。

关键在于「状态」。如果你的状态本身是别人的,那么这一部分的职责最好就是回到它发生的地方。如果一段代码主要都是在处理别人的状态,就说明它该拆分和重构到其状态所属之处了——这意味着当前的代码是耦合的,耦合与他处的状态。「依恋情结」正是这样一种坏味道,全在处理别人家的状态。如果对方只有一个,那这一整部分应该移到那边;如果有多个,这里就分两种情况:1.多个一起变,而且是为了一个目的变,每次都固定搭配,而且缺一不可,那就说明这一部分是一个单独的职责,应该给这个单独给一个方法或者对象,2.多个分开变,那就得拆出来,作为单独的方法存在;以及纯粹的排序,比如固定的生命周期,而不是彼此搭配的状态操作,那就是我们上面说的编排。

区分在于职责,如果某个职责本身已经有人——或者现在还没人——那就应该由它去做。而不是由职责不同的另一个方法去做。重点是分辨三个:委托、依恋和编排。这里有个地方很难搞清楚,那就是「自己的状态」——到底是啥?这里的核心就是,那些固定搭配,反复操作的状态,就是「自己」,这个「自己」在我们给他封装之前可能是不存在的,但一旦包装好,就可以统一把这谢谢状态给他自己管理了,此时它再修改的就是自己的状态,自己负责。当我不是单纯在这里进行一次的操作(这是委派)而是每次都在做一个流程(一组操作)的时候,这说明我可能越界了。