单一职责原则(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)。
- 在架构层面,它变成变化轴,用来创建架构边界。
浙公网安备 33010602011771号