问题由来
问题由来:类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。
解决方案:遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。
但是即便是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。为什么会出现这种现象呢?因为有职责扩散。所谓职责扩散,就是因为某种原因,职责P被分化为粒度更细的职责P1和P2。
比如:类T只负责一个职责P,这样设计是符合单一职责原则的。后来由于某种原因,也许是需求变更了,也许是程序的设计者境界提高了,需要将职责P细分为粒度更细的职责P1,P2,这时如果要使程序遵循单一职责原则,需要将类T也分解为两个类T1和T2,分别负责P1、P2两个职责。但是在程序已经写好的情况下,这样做简直太费时间了。所以,简单的修改类T,用它来负责两个职责是一个比较不错的选择,虽然这样做有悖于单一职责原则。(这样做的风险在于职责扩散的不确定性,因为我们不会想到这个职责P,在未来可能会扩散为P1,P2,P3,P4……Pn。所以记住,在职责扩散到我们无法控制的程度之前,立刻对代码进行重构。)
定义
一个类只负责一个功能领域中的相应职责,或者说可以定义为:就一个类而言,应该只有一个引起它变化的原因。
- 单一职责原则的核心思想是:一个类不能太累,在操作系统中,一个类(大到模块,小到方法)承担的责任越多,它被复用的可能性也就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。
- 单一职责适用于接口、类,同时也适用于方法。即一个方法尽可能做一件事情。
- 接口、方法一定要做到单一职责原则,类的设计尽量做到只有一个原因引起变化。
- 其实在软件设计中,要真正用好单一职责原则并不简单,因为遵循这一原则最关键的地方在于职责的划分,重点是职责的划分!重点是职责的划分!重点是职责的划分!重要的事情说三遍。而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。下面会给出一个案例来分析如何划分职责。
对职责的解释
什么是职责?行为;功能;角色;
单一职责:一个类只扮演一个角色,只做一件事情(精细分工是现代社会的标志之一)
使用SRP的目的:高内聚;低耦合
多个职责有什么不好?职责之间会相互影响;为使用某一功能而引入其他不需要的功能;对类的修改会牵涉太多(尽可能保持小、窄、轻、快;适当的粒度更利于复用)
如何区分不同的职责?
有些显而易见;更多的需要积累经验,很难较好划分。
(我们面对更多的是较为明确的需求;而职责的划分涉及了实现)单一职责:完全做一件事、做好这件事、只做这件事
经验之谈(Rule of thumb)
能用25个字就描述清楚一个类。
如果试图去做太多的事情,类、接口、函数总倾向于膨胀
保持代码简洁有效。onlyAndOnlyOnce,Do the simplest thing that could possibly work.
如何识别破坏了单一职责原则?
• 类有太多依赖
类的构造器有太多参数,意味着测试有太多依赖,需要制造mock太多测试输入参数,通常意味着已经破坏SRP了。
• 测试类变得复杂
如果测试有太多变量,意味着这个类有太多职责。
• 类或方法太长
如果方法太长,意味着内容太多,职责过多。一个类不超过 200-250
• 描述性名称
如果你需要描述你的类 方法或包,比如使用”xxx和xxx”这种语句,意味着可能破坏了SRP.
• 低聚合Cohesion的类
聚合Cohesion是一个很重要的概念,虽然聚合是有关结构概念,但是聚合和SRP非常相关,如前面论坛案例,如果一个类不代表一个高聚合,意味着低凝聚low Cohesion,它就可能意味破坏SRP。一个低凝聚的特点:一个类有两个字段,其中一个字段被一些方法使用;另外一个字段被其他方法使用。
• 在一个地方改动影响另外一个地方
如果在一个代码地方加入新功能或只是简单重构,却影响了其他不相关的地方,意味着这个地方代码可能破坏了SRP.
• 猎枪效果Shotgun Effect
如果一个小的改变引起一发动全身,这意味SRP被破坏了。
• 不能够封装模块
比如使用Spring框架,你使用@Configuration or XML 配置,如果你不能在一个配置中封装一个Bean。意味着它有太多职责,Spring配置应该隐藏内部bean,暴露最少接口,如果你因为多个原因需要改变Spring配置,可能破坏了SRP.
优缺点
优点:
可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
提高类的可读性,提高系统的可维护性、可扩展性;
变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。
一个非常大的优点是:便于类和方法的重用。
缺点:
单一职责原则提出了一个编写程序的标准,用”职责“或”变化原因“来衡量接口或类设计是否优良,但是”职责“和”变化原因“都是不可度量的,因项目、环境而已。
多数工业代码的类都是违反单一职责原则的。因为类的单一职责受多种因素的影响。
案例
例1:
举例说明,用一个类描述动物呼吸这个场景:
class Animal{
public void breathe(String animal){
System.out.println(animal+"呼吸空气");
}
}
public class Client{
public static void main(String[] args){
Animal animal = new Animal();
animal.breathe("牛");
animal.breathe("羊");
animal.breathe("猪");
}
}
运行结果:
牛呼吸空气
羊呼吸空气
猪呼吸空气
程序上线后,发现问题了,并不是所有的动物都呼吸空气的,比如鱼就是呼吸水的。修改时如果遵循单一职责原则,需要将Animal类细分为陆生动物类Terrestrial,水生动物Aquatic,代码如下:
class Terrestrial{
public void breathe(String animal){
System.out.println(animal+"呼吸空气");
}
}
class Aquatic{
public void breathe(String animal){
System.out.println(animal+"呼吸水");
}
}
public class Client{
public static void main(String[] args){
Terrestrial terrestrial = new Terrestrial();
terrestrial.breathe("牛");
terrestrial.breathe("羊");
terrestrial.breathe("猪");
Aquatic aquatic = new Aquatic();
aquatic.breathe("鱼");
}
}
运行结果:
牛呼吸空气
羊呼吸空气
猪呼吸空气
鱼呼吸水
我们会发现如果这样修改花销是很大的,除了将原来的类分解之外,还需要修改客户端。而直接修改类Animal来达成目的虽然违背了单一职责原则,但花销却小的多,代码如下:
class Animal{
public void breathe(String animal){
if("鱼".equals(animal)){
System.out.println(animal+"呼吸水");
}else{
System.out.println(animal+"呼吸空气");
}
}
}
public class Client{
public static void main(String[] args){
Animal animal = new Animal();
animal.breathe("牛");
animal.breathe("羊");
animal.breathe("猪");
animal.breathe("鱼");
}
}
可以看到,这种修改方式要简单的多。但是却存在着隐患:有一天需要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,则又需要修改Animal类的breathe方法,而对原有代码的修改会对调用“猪”“牛”“羊”等相关功能带来风险,也许某一天你会发现程序运行的结果变为“牛呼吸水”了。这种修改方式直接在代码级别上违背了单一职责原则,虽然修改起来最简单,但隐患却是最大的。还有一种修改方式:
class Animal{
public void breathe(String animal){
System.out.println(animal+"呼吸空气");
}
public void breathe2(String animal){
System.out.println(animal+"呼吸水");
}
}
public class Client{
public static void main(String[] args){
Animal animal = new Animal();
animal.breathe("牛");
animal.breathe("羊");
animal.breathe("猪");
animal.breathe2("鱼");
}
}
可以看到,这种修改方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然也违背了单一职责原则,但在方法级别上却是符合单一职责原则的,因为它并没有动原来方法的代码。这三种方式各有优缺点,那么在实际编程中,采用哪一中呢?其实这真的比较难说,需要根据实际情况来确定。
例如本文所举的这个例子,它太简单了,它只有一个方法,所以,无论是在代码级别上违反单一职责原则,还是在方法级别上违反,都不会造成太大的影响。实际应用中的类都要复杂的多,一旦发生职责扩散而需要修改类时,除非这个类本身非常简单,否则还是遵循单一职责原则的好。
例2:
男生一般是站着尿尿,所以代码是:
function Male (name,age) {
this.name = name;
this.age = age;
this.sex = 'male';
}
Male.prototype = {
coustructor: Male,//尿尿的行为
pee: function () {
console.log('站着尿尿');
}
};
女生一般是蹲着尿尿,所以代码是:
function FeMale (name,age) {
this.name = name;
this.age = age;
this.sex = 'female';
}
FeMale.prototype = {
coustructor: FeMale,//尿尿的行为
pee: function () {
console.log('蹲着尿尿');
}
};
所以结果男生lilei是站着尿尿的,女生hanmeimei是蹲着尿尿的
var lilei = new Male('lilei',20);
//站着尿尿
lilei.pee();
var hanmeimei = new FeMale('hanmeimei',20);
//蹲着尿尿
hanmeimei.pee();
这一切是不是看起来好像很完美? 但是lilei虽然是男生,但是他却喜欢蹲着尿尿(或者都喜欢女生行为),这时候lilei的行为就与自己的性别产生了耦合,其实性别与行为分别负责不同的职责
/** * 人类的基类 * @param person: 人类的各种属性,包括姓名、年龄、性别等 * behavior: 人类的行为 */
var Human = function (person,behavior) {
this.person = person;
this.behavior = behavior;
}
//人的属性
var Person = function (name,age,sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
//行为类
var Behavior = function () {
}
Behavior.prototype = {
courstructor: Behavior,//尿尿的行为
pee: function () {
}
};
//一般男生的行为
var MaleBehaior = function (){
}
MaleBehaior.prototype = Object.create(Behavior.prototype,{
pee: {
writable:true,configurable:true,value: function () {
console.log('站着尿尿');
}
}
});
//一般女生的行为
var FeMaleBehaior = function (){
}
FeMaleBehaior.prototype = Object.create(Behavior.prototype,value: function () {
console.log('蹲着尿尿');
}
}
});
基本类都构造完毕了,现在看下lilei是怎么实例出来的:
var lilei = new Human(new Person('lilei',20,'male'),new FeMaleBehaior());
//此时,lilei就是蹲着尿尿的,lilei虽然是男生,但是他喜欢女生的各种行为。
lilei.behavior.pee();
var hanleilei = new Human(new Person('lilei','female'),new FeMaleBehaior());
//hanleilei是萌妹子
hanleilei.behavior.pee();
var peter = new Human(new Person('lilei',new MaleBehaior());
//perter是纯爷们
perter.behavior.pee();
此时,职责就分明了,Person负责个人的基本属性,Behaior负责行为,各自直接就完全解耦了。
虽然上面例子解耦了属性与行为,但是如果粗粒度来分析,Male类与Female类其实也符合单一职责原则,Male负责男性个体,Female负责女性个体,并没有违反单一职责原则,所以我们在开发的时候,单一职责原则不能死搬书上的例子,要根据项目的大小与计算以后维护成本来衡量是否要更细粒度的解耦,实现粒度更细的单一职责。