单一职责原则的定义
鲍勃大叔( Robert C. Martin)这样定义单一职责原则:
A class should have only one reason to change
一个类应该只有一个发生变化的原因
定义非常简洁。然而,使用这一原则却不像看起来那么容易。
这样理解SRP对吗?
记得前一段时间读过一篇批评SRP的文章,大意是说SRP是一个错误的想法,并不能帮助程序员理解高内聚(high cohesion)的概念,并举了一个例子来证明。例子是这样的:
class AwsOcket {
boolean exists() { /* ... */ }
void read(final OutputStream output) { /* ... */ }
void write(final InputStream input) { /* ... */ }
}
显然,这个类有三个职责:
检查AWS S3中对象是否存在;
把对象内容写入输出流;
把输入流的对象写到AWS S3。
这显然违反了SRP原则。根据SRP来重构一下,拆分出三个类:ExistenceChecker、ContentReader和ContentWriter。重构前,为了读取内容并将其打印到控制台,代码大概这样:
if (ocket.exists()) {
ocket.read(System.out);
}
重构后,代码这样:
if (new ExistenceChecker(ocket.aws()).exists()) {
new ContentReader(ocket.aws()).read(System.out);
}
显然,重构后反而变糟糕了。对SRP的批评看起来似乎没毛病。鲍勃大叔错了吗?没有。是文章作者曲解了SRP。
如何理解SRP?
首先,鲍勃大叔是理解高内聚的。他在《Agile Software Development, Principles, Patterns, and Practices》中SRP一章开篇就说:
The principle was described in the work of Tom Demarco and Meilir Page-Jones. They called it cohesion … In this chapter, we modify that meaning bit and relate cohesion to the forces that cause a module, or a class, to change.
Tom Demarco 和 Meilir Page-Jones曾经在他们的著作里描述这个原则。他们称之为内聚性……本章中,我们稍微改一下它的含义,把内聚性和引起一个模块或者类改变的作用力联系起来。
显然,鲍勃大叔不是抛弃内聚性,而是换个角度看问题。他在书中进一步说:
In the context of SRP, we define a responsibility to be a reason for change.
SRP上下文中,我们把职责定义为变化的原因。这样,SRP的定义就等同于“一个类应该只有一个职责”,所以SRP叫单一职责原则。
如何理解职责就变成了理解SRP的关键。上面提到的文章显然把一个职责等同于一个功能了。这是不符合鲍勃大叔的本意。鲍勃大叔这样解释一个职责:
So a responsibility is a family of functions that serves one particular actor.
一个职责是一个服务于某个特定参与者的功能集
既然一个职责服务于一个特定的参与者,那么参与者就是职责变化之源:
An actor for a responsibility is the single source of change for that responsibility.
考察一个职责,必然要考察参与者,从参与者的角度识别职责。
举个栗子
如鲍勃大叔书中所提,混合持久化逻辑和业务逻辑是常见的违反SRP的情况。例如:
对于Book而言,有两个截然不同的参与者:Reader和FilePersistence。Reader需要通过get方法获取书的标题和内容,Book通过FilePersistence存储自己。Reader和FilePersistence任何一个需求的变化都会带来Book的变化,即变化原因不唯一。正确的做法是把Save方法从Book中剥离出去:
总结
SRP看似简单,正确使用却难。
单一职责不意味着一个类只有一个方法,而是一组高内聚的方法。
从参与者角度去考虑职责会有很大帮助。
—————END—————
关注夫子程序社,更多精彩