单一职责原则(SRP)
在所有 SOLID 原则中,单一职责原则(SRP) 可能是最被误解的一个。原因很可能在于它的名字取得特别不恰当。程序员太容易一听到这个名字,就想当然地认为:每个模块只应该做一件事。别搞错,确实有这么一条原则:一个函数应该只做一件事,并且只做这一件事。我们在把大函数拆成小函数时会用到它 —— 这是最底层、代码级别的原则。但它不属于 SOLID 原则,也不是 SRP。

历史上来看,SRP准则可以被这么描述:
一个模块只能有一个理由去改变

为了满足用户和股东,软件系统会改变,为了用户和股东而改变就是唯一原因去改变,这就是准则的内容。尽管如此,我们可以再解释这个准则如下:
一个模块需要对一个东西负责-用户或者股东

遗憾的是,在这里使用 “用户” 和 “利益相关者” 这两个词并不准确。因为往往会有多个用户或利益相关者,希望以同一种方式修改系统。
实际上,我们真正指的是一个群体:一个或多个要求进行同一项变更的人。我们把这个群体称为一个 参与者(actor)。因此,SRP 的最终定义是:一个模块应该只对一个、且仅一个参与者负责。那么,我们所说的模块(module)是什么意思?最简单的定义就是:一个源码文件。大多数情况下,这个定义都适用。不过,有些语言和开发环境并不使用源码文件来组织代码,这时模块就是一组内聚的函数和数据结构。内聚(cohesive) 这个词本身就体现了 SRP。内聚,是将为同一个参与者负责的代码绑定在一起的力量。
想要理解这条原则,最好的方式或许是:看看违反它会出现哪些症状。

症状一:意外重复

我最喜欢的例子是薪资系统里的 Employee 类
它有三个方法:
calculatePay()(计算工资)、reportHours()(上报工时)、save()(数据保存)。

这个类违反了 SRP
因为这三个方法分别对应三个完全不同的参与者

  • calculatePay()财务部制定,向 CFO 负责。
  • reportHours()人力资源部制定和使用,向 COO 负责。
  • save()DBA 数据库团队制定,向 CTO 负责。

把这三段代码放进同一个 Employee 类,
等于把这三方强行耦合在一起
这种耦合会导致:CFO 团队的改动,可能意外影响到 COO 团队依赖的功能。

举个例子:
假设 calculatePay()reportHours() 共用一段计算正常工时的算法。
程序员为了不重复代码,把它抽成了一个公共函数 regularHours()

后来,CFO 团队要求修改正常工时的计算方式
但 HR(COO 团队)完全不想改,因为他们有别的用途。

某个开发人员去改代码,
只看到 calculatePay() 调用了 regularHours()
没注意到 reportHours() 也在用它

改完、测试、上线,CFO 那边没问题。
但 HR 完全不知情,
他们继续用 reportHours() 生成报表,
结果数据全错了,最后给公司造成巨大损失。

我们都见过类似的事情。
这类问题的根源就是:
把不同参与者依赖的代码放在了一起。

SRP 就是要让我们:
把不同参与者所依赖的代码分离开。

症状二:代码合并冲突

不难想象,一个包含很多不同方法的源文件,
合并冲突会非常频繁
尤其是这些方法对应不同参与者的时候。

比如:

  • CTO 的 DBA 团队想改员工表的库表结构。
  • COO 的 HR 团队想改工时报表格式。

两个不同团队、不同开发人员,同时修改 Employee 类。
结果就是:冲突 + 必须合并代码

合并永远是有风险的,
再好用的工具也不能保证 100% 不出问题。
在这个例子里,CTO、COO、甚至 CFO 都可能被牵连。

其他症状还有很多,
但本质都一样:
多伙人,因为不同原因,去改同一个文件。

避免问题的方法只有一个:
把支持不同参与者的代码分开。

解决方案

有很多种方案,核心都是:
把函数分到不同的类里。

方案一:数据与行为分离

把数据单独放到 EmployeeData 里(只有属性,没有方法),
然后拆成三个类:

  • 计算工资的类
  • 生成报表的类
  • 保存数据的类

这三个类互相不知道对方的存在
从根本上避免意外影响。

缺点是:要实例化、管理三个类。

方案二:使用外观模式(Facade)

建一个 EmployeeFacade 外观类,
几乎不含业务逻辑,只负责实例化三个类并做委托调用。

方案三:把最重要的业务逻辑留在主类

把最重要的方法留在原来的 Employee 里,
再让它充当外观,调用其他次要功能。

你可能会说:“那每个类不就只有一个方法吗?”
其实不是。
计算工资、生成报表、保存数据,
每一个背后都可能有一大堆逻辑和私有方法,
足够形成一个独立的类族。

每个这样的类都是一个独立作用域,
外部完全看不到它的私有成员。

结论

单一职责原则(SRP)不只适用于函数和类,
它还会在更高层级以不同形式出现:

  • 组件层面,它变成共同闭合原则(CCP)
  • 架构层面,它变成变化轴,用来创建架构边界