什么是单一职责原则
在程序设计领域, SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)是由罗伯特·C·马丁在21世纪早期 引入的记忆术首字母缩略字,指代了面向对象编程和面向对象设计的五个基本原则。
“SOLID”原则中的 S 指代了单一功能原则,单一职责原则英文是 Single Responsibility Principle,缩写为 SRP。
在面向对象编程领域中,单一功能原则规定每个类都应该有一个单一的功能,并且该功能应该由这个类完全封装起来。所有它的(这个类的)服务都应该严密的和该功能平行(功能平行,意味着没有依赖)。
单一职责原则定义很简单,不难理解,但是很多人对于此原则,一看就会,一学就废。一个类只负责完成一个职责或者功能。换言之,不要设计大而全的类,上帝类,类中包含俩个或以上不相干的功能,我们就可以认定它违反单一职责原则。
职责,可以理解为改变原因,一个类或者模块应有且仅有一个原因。
具体举一个例子,现在有一个类,类中实现了编辑文章和格式化文章的功能,此时类中存在俩个原因:文章内容通过编辑改变、文章编排通过格式化改变。这俩方面因不同的因而发生,本质上是有区别的,从单一原则的角度分析,这应该是俩个分离的功能,职责不单一,把有不同的改变原因的事物耦合在一起的设计是糟糕的,应该将它拆分成俩个功能更单一、粒度更细的类。
而且,保持类专注于单一职责,也是为了使类更为健壮。
引申上面的例子,若编辑文章的流程发生变更,此时俩个功能依然耦合,此时的修改会存在极大的危险,假如修改编辑文章流程,使得类中公共状态或者依赖关系发生变更,会导致格式化文章功能因此不work。
单一职责原则的评判标准
评判类与模块的职责是否足够单一,有木有一个比较明确的可以量化的标准呢?就好比,下厨房,一条鱼需要蒸几分钟?放盐多少?老师傅都只能给一个大概的指标。这里也有异曲同工之妙,夹杂着主观。实际上,在实际的生产中,没必要过度设计,未雨绸缪,刚开始粗粒度,随着业务的增长,再拆分成细粒度,持续重构。
你可能会问,那究竟标准是什么?我怎么去衡量?
下面有几个可执行性的建议:
- 类中的代码行数(建议200-500)、函数或属性(10个以内)过多,会影响代码的可读性和可维护性;
- 类依赖过多,不符合高内聚、低耦合的设计思想;
- 类中大量的方法都是集中操作类中的某几个属性,可考虑将这几个属性和对应的方法拆分出来;
- 类的定义较困难,难以概括,只能用宽泛的词形容,如 Manager、Context 之类,说明类的职责定义得可能不够清晰;
单一职责原则是否越单一越好?
单一职责,提高代码的可读性、可维护性、健壮性,符合高内聚、低耦合的设计思想。既然单一职责那么好,那么需要将单一职责践行到底吗?我表示不赞同。下面我们看一个例子:
public abstract class StringUtils {
private static final String joinKey = ";";
/**
* 将数组拼接成字符串
*/
public static String join(String[] array) {
StringBuilder s = new StringBuilder();
if(array != null) {
for(int i = 0; i < array.length; i++) {
Object obj = array[i];
s.append(obj);
if(i != array.length - 1) {
s.append(joinKey);
}
}
}
return s.toString();
}
/**
* 将字符串切割成数组
*/
public static String[] parse(String s) {
return s.split(joinKey);
}
}
如果想让类的职责更加单一,我们要将 StringUtils
做更细粒度的拆分,分成只负责拼接工作的 StringJoinUtils
和只负责切割 StringParseUtils
。
public abstract class StringJoinUtils {
private static final String joinKey = ";";
/**
* 将数组拼接成字符串
*/
public static String join(String[] array) {
StringBuilder s = new StringBuilder();
if(array != null) {
for(int i = 0; i < array.length; i++) {
Object obj = array[i];
s.append(obj);
if(i != array.length - 1) {
s.append(joinKey);
}
}
}
return s.toString();
}
}
public abstract class StringParseUtils {
private static final String joinKey = ";";
/**
* 将字符串切割成数组
*/
public static String[] parse(String s) {
return s.split(joinKey);
}
}
拆分后,类的职责更加单一了,但随之也出现了新的问题。如果 joinKey
发生变更时,我们需要同时维护StringJoinUtils
和 StringParseUtils
,若有一个类忘记维护了,那么转化就会出现不匹配问题,降低了代码的可维护性了。
因此,不管是设计原则还是设计模式,最终的目的还是提高代码的可读性、可扩展性、复用性、可维护性等。我们不能生搬硬套,教条主义,要合理。