前端内参
搜索文档…
陆.1.1 单一职责原则

01.什么是单一职责原则

单一职责原则,英文缩写SRP,全称Single Responsibility Principle
原英文定义:
There should never be more than one reason for a class to change。
翻译:应该有且仅有一个原因引起类的变更。简而言之:
一个类,最好只负责一件事,只有一个引起它变化的原因。

02.对定义的详细分析

上面的定义不难理解,引起类变化的原因不能多于一个。也就是说每一个类只负责自己的事情,此所谓单一职责。
我们知道,在OOP里面高内聚、低耦合是软件设计追求的目标,而单一职责原则可以看做是高内聚、低耦合的引申。将职责定义为引起变化的原因,以提高内聚性,以此来减少引起变化的原因。职责过多,可能引起变化的原因就越多,这将是导致职责依赖,相互之间就产生影响,从而极大的损伤其内聚性和耦合度。单一职责通常意味着单一的功能,因此不要为类实现过多的功能点,以保证实体只有一个引起它变化的原因。
不管是从官方定义,还是对“单一职责”名称的解释,都能很好的理解单一职责原则的意义。其实在软件设计中,要真正用好单一职责原则并不简单,因为遵循这一原则最关键的地方在于职责的划分;而职责的划分是根据需求定的,同一个类(接口)的设计,在不同的需求里面,可能职责的划分并不一样。
为什么这么说呢?我们来看下面的例子。

03.用JAVA做一段OOP的示例

关于单一职责原则的原理,我们就不做过多的解释了。重点是职责的划分!重点是职责的划分!重点是职责的划分!重要的事情说三遍。下面根据一个示例场景来看看如何划分职责。
假定现在有如下场景:国际手机运营商那里定义了生产手机必须要实现的接口,接口里面定义了一些手机的属性和行为,手机生产商如果要生产手机,必须要实现这些接口。

1). 初始设计

我们首先以手机作为单一职责去设计接口,方案如下:
1
/// <summary>
2
/// 充电电源类
3
/// </summary>
4
public class ElectricSource{
5
}
Copied!
1
public interface IMobilePhone
2
{
3
//运行内存
4
string RAM { get; set; }
5
6
//手机存储内存
7
string ROM { get; set; }
8
9
//CPU主频
10
string CPU { get; set; }
11
12
//屏幕大小
13
int Size { get; set; }
14
15
//手机充电接口
16
void Charging(ElectricSource oElectricsource);
17
18
//打电话
19
void RingUp();
20
21
//接电话
22
void ReceiveUp();
23
24
//上网
25
void SurfInternet();
26
}
Copied!
然后我们的手机生产商去实现这些接口:
1
//具体的手机示例
2
public class MobilePhone:IMobilePhone
3
{
4
public string RAM
5
{
6
get{throw new NotImplementedException();}
7
set{throw new NotImplementedException();}
8
}
9
public string ROM
10
{
11
get{throw new NotImplementedException();}
12
set{throw new NotImplementedException();}
13
}
14
public string CPU
15
{
16
get{throw new NotImplementedException();}
17
set{throw new NotImplementedException();}
18
}
19
public int Size
20
{
21
get{throw new NotImplementedException();}
22
set{throw new NotImplementedException();}
23
}
24
public void Charging(ElectricSource oElectricsource)
25
{
26
throw new NotImplementedException();
27
}
28
public void RingUp()
29
{
30
throw new NotImplementedException();
31
}
32
public void ReceiveUp()
33
{
34
throw new NotImplementedException();
35
}
36
public void SurfInternet()
37
{
38
throw new NotImplementedException();
39
}
40
}
Copied!
这种设计有没有问题呢?这是一个很有争议的话题。
单一职责原则要求一个接口或类只有一个原因引起变化,也就是一个接口或类只有一个职责,它就负责一件事情。原则上来说,我们以手机作为单一职责去设计,也是有一定的道理的,因为我们接口里面都是定义的手机相关属性和行为,引起接口变化的原因只可能是手机的属性或者行为发生变化,从这方面考虑,这种设计是有它的合理性的,如果你能保证需求不会变化或者变化的可能性比较小,那么这种设计就是合理的。
但实际情况我们知道,现代科技日新月异,科技的进步促使着人们不断在手机原有基础上增加新的属性和功能。比如有一天,我们给手机增加了摄像头,那么需要新增一个像素的属性,我们的接口和实现就得改吧,又有一天,我们增加移动办公的功能,那么我们的接口实现是不是也得改。由于上面的设计没有细化到一定的粒度,导致任何一个细小的改动都会引起从上到下的变化,有一种“牵一发而动全身”的感觉。所以需要细化粒度,下面来看看我们如何变更设计。

2). 二次变更

我们将接口细化:
1
  //手机属性接口
2
public interface IMobilePhoneProperty
3
{
4
//运行内存
5
string RAM { get; set; }
6
7
//手机存储内存
8
string ROM { get; set; }
9
10
//CPU主频
11
string CPU { get; set; }
12
13
//屏幕大小
14
int Size { get; set; }
15
16
//摄像头像素
17
string Pixel { get; set; }
18
}
19
20
//手机功能接口
21
public interface IMobilePhoneFunction
22
{
23
//手机充电接口
24
void Charging(ElectricSource oElectricsource);
25
26
//打电话
27
void RingUp();
28
29
//接电话
30
void ReceiveUp();
31
32
//上网
33
void SurfInternet();
34
35
//移动办公
36
void MobileOA();
37
}
Copied!
实现类:
1
//手机属性实现类
2
public class MobileProperty:IMobilePhoneProperty
3
{
4
public string RAM
5
{
6
get{ throw new NotImplementedException();}
7
set{ throw new NotImplementedException();}
8
}
9
public string ROM
10
{
11
get{ throw new NotImplementedException();}
12
set{ throw new NotImplementedException();}
13
}
14
public string CPU
15
{
16
get{ throw new NotImplementedException();}
17
set{throw new NotImplementedException();}
18
}
19
public int Size
20
{
21
get{throw new NotImplementedException();}
22
set{throw new NotImplementedException();}
23
}
24
public string Pixel
25
{
26
get{throw new NotImplementedException();}
27
set{throw new NotImplementedException();}
28
}
29
}
30
31
//手机功能实现类
32
public class MobileFunction:IMobilePhoneFunction
33
{
34
public void Charging(ElectricSource oElectricsource)
35
{
36
throw new NotImplementedException();
37
}
38
public void RingUp()
39
{
40
throw new NotImplementedException();
41
}
42
public void ReceiveUp()
43
{
44
throw new NotImplementedException();
45
}
46
public void SurfInternet()
47
{
48
throw new NotImplementedException();
49
}
50
public void MobileOA()
51
{
52
throw new NotImplementedException();
53
}
54
}
55
56
//具体的手机实例
57
public class HuaweiMobile
58
{
59
private IMobilePhoneProperty m_Property;
60
private IMobilePhoneFunction m_Func;
61
public HuaweiMobile(IMobilePhoneProperty oProperty, IMobilePhoneFunction oFunc)
62
{
63
m_Property = oProperty;
64
m_Func = oFunc;
65
}
66
}
Copied!
对于上面题的问题,这种设计能够比较方便的解决,如果是增加属性,只需要修改IMobilePhoneProperty和MobileProperty即可;如果是增加功能,只需要修改IMobilePhoneFunction和MobileFunction即可。貌似完胜第一种解决方案。那么是否这种解决方案就完美了呢?答案还是看情况。原则上,我们将手机的属性和功能分开了,使得职责更加明确,所有的属性都由IMobilePhoneProperty接口负责,所有的功能都由IMobilePhoneFunction接口负责,如果是需求的粒度仅仅到了属性和功能这一级,这种设计确实是比较好的。反之,如果粒度再细小一些呢,那我们这种职责划分是否完美呢?比如我们普通的老人机只需要一些最基础的功能,比如它只需要充电、打电话、接电话的功能,但是按照上面的设计,它也要实现IMobilePhoneFunction接口,某一天,我们增加了一个新的功能玩游戏,那么我们就需要在接口上面增加一个方法PlayGame()。可是我们老人机根本用不着实现这个功能,可是由于它实现了该接口,它的内部实现也得重新去写。从这点来说,以上的设计还是存在它的问题。
那么,我们如何继续细化接口粒度呢?

3). 最终成型

接口细化粒度设计如下:
1
//手机基础属性接口
2
public interface IMobilePhoneBaseProperty
3
{
4
//运行内存
5
string RAM { get; set; }
6
7
//手机存储内存
8
string ROM { get; set; }
9
10
//CPU主频
11
string CPU { get; set; }
12
13
//屏幕大小
14
int Size { get; set; }
15
}
16
17
//手机扩展属性接口
18
public interface IMobilePhoneExtentionProperty
19
{
20
//摄像头像素
21
string Pixel { get; set; }
22
}
23
24
//手机基础功能接口
25
public interface IMobilePhoneBaseFunc
26
{
27
//手机充电接口
28
void Charging(ElectricSource oElectricsource);
29
30
//打电话
31
void RingUp();
32
33
//接电话
34
void ReceiveUp();
35
}
36
37
//手机扩展功能接口
38
public interface IMobilePhoneExtentionFunc
39
{
40
//上网
41
void SurfInternet();
42
43
//移动办公
44
void MobileOA();
45
46
//玩游戏
47
void PlayGame();
48
}
Copied!
实现类和上面类似:
1
//手机基础属性实现
2
public class MobilePhoneBaseProperty : IMobilePhoneBaseProperty
3
{
4
5
public string RAM
6
{
7
get{throw new NotImplementedException();}
8
set{throw new NotImplementedException();}
9
}
10
11
public string ROM
12
{
13
get{throw new NotImplementedException();}
14
set {throw new NotImplementedException();}
15
}
16
17
public string CPU
18
{
19
get{throw new NotImplementedException();}
20
set{ throw new NotImplementedException();}
21
}
22
23
public int Size
24
{
25
get{ throw new NotImplementedException();}
26
set{ throw new NotImplementedException();}
27
}
28
}
29
30
//手机扩展属性实现
31
public class MobilePhoneExtentionProperty : IMobilePhoneExtentionProperty
32
{
33
34
public string Pixel
35
{
36
get{ throw new NotImplementedException();}
37
set{ throw new NotImplementedException();}
38
}
39
}
40
41
//手机基础功能实现
42
public class MobilePhoneBaseFunc : IMobilePhoneBaseFunc
43
{
44
public void Charging(ElectricSource oElectricsource)
45
{
46
throw new NotImplementedException();
47
}
48
49
public void RingUp()
50
{
51
throw new NotImplementedException();
52
}
53
54
public void ReceiveUp()
55
{
56
throw new NotImplementedException();
57
}
58
}
59
60
//手机扩展功能实现
61
public class MobilePhoneExtentionFunc : IMobilePhoneExtentionFunc
62
{
63
64
public void SurfInternet()
65
{
66
throw new NotImplementedException();
67
}
68
69
public void MobileOA()
70
{
71
throw new NotImplementedException();
72
}
73
74
public void PlayGame()
75
{
76
throw new NotImplementedException();
77
}
78
}
Copied!
此种设计能解决上述问题,细分到此粒度,这种方案基本算比较完善了。能不能算完美?这个得另说。接口的粒度要设计到哪一步,取决于需求的变更程度,或者说取决于需求的复杂度。

04.JavaScript示例:万能函数

接着,我们暂且抛开JAVA语言和OOP,从JavaScript函数的角度来讨论单一职责原则。
还记得吗?新手程序员我们往往很喜欢写“功能超级强大的、复杂的万能函数”,以体现自己的编程能力之不俗。
1
//这是一个功能“超级强大”的函数
2
function aBigFunc(param){
3
if(param == 0 ){
4
//这里有一坨上百行的代码,执行任务
5
}else if(param == 1){
6
//另一坨上百行的代码,执行任务
7
}else if(param ==3){
8
//又一坨代码,执行任务
9
}else if{
10
//最后一坨代码,执行任务
11
}
12
}
Copied!
上面这种多功能函数,理论上只要敢写if分支,什么任务都完成,俗称“万能函数”。几乎每一个程序员都写过万能函数。万能函数确实能解决问题,而且看上去似乎很强大,甚至还能让总体代码的行数更少一点儿。很多新手程序员包括笔者刚学编程时,也常常以此为借口堂而皇之地大写特写万能函数。
然而,当我们需要修改、新增功能的时候,或者我们接手维护别人的万能函数时,这些万能函数动辄几百行的代码,经常会让我们自己读得头晕眼花,往往是新写的代码只要几分钟,阅读代码却花掉十几分钟;而且随着项目工程变大,代码行数的增加,耗费在读代码上的时间会越来越多。良好的习惯是这样的,将万能函数拆分成功能单一的函数:
1
function doQuestA(){
2
//执行任务A
3
}
4
function doQuest(){
5
//执行任务
6
}
7
function doQuest(){
8
//执行任务
9
}
10
function doQuest(){
11
//执行任务
12
}
13
14
function doQuest(param){
15
if(param==0)doQuestA();
16
else if(param==1)doQuestB();
17
else if(param==2)doQuestC();
18
else if(param==3)doQuestD();
19
}
Copied!
优化之后,复杂的万能函数被分割成很多小函数,以前复杂的细节代码被一个个函数隐藏起来。这样我们以后修改/新增这个做任务的函数时候,就可以避免被细节牵绊,大大降低出错的概率;还可以利用IDE的一些语法结构分析功能,很快速地找到具体的函数位置进行添加/修改,无需耗费大量的时间查找、阅读复杂的代码。

05.总结

使用单一职责原则时,没有最合理,只有最合适。理解单一职责原则,最重要的就是掌握好职责划分的粒度,而粒度则取决于需求的粒度。

06.参考文献

最近更新 1yr ago