单一职责原则的概念——以用户类为例

单一职责原则(Single Responsibility Principle,SRP) 备受争议, 主要在对指责的定义,即什么是类的职责及怎么划分类的职责。

举例说明,在一个项目中有用户、机构、角色管理等模块,存在用户管理、修改用户信息、增加机构(一个人属于多个机构)、增加角色等信息和行为,如果全写到一个接口中,则如下图:
image.png
实际上,应该将用户信息抽取成一个BO(Bussiness Object,业务对象),把行为抽取成一个Biz(Bussiness Logic,业务逻辑),修正后类图如下:
image.png
拆分之后,一个接口就被拆分成两个接口:

  • IUserBO负责用户的属性,即收集和反馈用户的属性信息;
  • IUserBiz负责用户的行为,完成用户信息的维护和变更。

如此,在产生UserInfo对象后,可以当BO使用,也可以当作Biz使用,具体取决于用在什么地方。但在实际使用中,应该使用两个不同的类或接口IUserBO和IUserBiz。项目中常用的SRP类图如下:
image.png
如此将一个接口拆分成了两个接口,就依赖了单一职责原则。

单一职责原则:应该有且仅有一个原因引起类的变更(There should never be more than one reason for a class to change)。


类的职责划分及实现——以电话类为例

另一个例子,电话通话有4个过程:拨号、通话、回应、挂机,其接口类图如下:
image.png
代码如下:

  1. public interface IPhone {
  2. // 拨通电话
  3. public void dial(String phoneNumber);
  4. // 通话
  5. public void chat(Object o);
  6. // 通话完毕,挂电话
  7. public void hangup();
  8. }

这个接口接近完美,但还有一些问题。 单一职责原则要求一个接口或类只有一个原因引起变化,即一个接口或类只有一个职责(负责一件事情) ,但这个接口并不是。

IPhone接口包含了两个职责:协议管理和数据传送。dial()和hangup()实现协议管理,负责接通和挂机,chat()实现数据传送,即通话。协议的变化和数据传送方式的变化都会引起这个接口实现类的变化,且两个职责的变化互不影响,因此可以拆分成两个接口,类图如下:
image.png
但这种实现方式,一个手机类要将ConnectionManager和DataTransfer两个类组合在一起才能使用, 组合是强耦合关系,有共同生命周期 。这种实现方式还多了两个类,增加了类的复杂性。因此应做如下修改:
image.png
一个类实现两个接口 ,将两个职责融合在一个类中。

单一职责原则的优点:

  • 类的复杂性降低,实现的职责有了明确的定义;
  • 复杂性降低,可读性提高,可维护性提高;
  • 变更引起的风险降低,一个接口的修改只对相应的实现累有影响,对其他接口无影响。

注意 单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计的是否优良,但是“职责”和:变化原因“都是不可度量的,因项目、环境而异。


方法的单一职责原则——以修改用户信息方法为例

接口:设计时要做到单一职责。
实现类:需要多方面考虑。生搬硬套单一职责会引起类的剧增,增加系统复杂性,导致维护困难。

单一职责适用于接口、类,同时也 适用于方法 ,即一个方法尽可能做一件事。比如一个方法修改用户密码,不要放到“修改用户信息”方法中,如下图:
image.png
在IUserManager中定义一个changeUser方法,根据传递类型不同,将可变长度参数changeOptions修改到userBO对象上,并调用持久层的方法保存到数据库中。这种方法职责不清晰,不单一,需要让别人猜测方法用来处理什么逻辑。较好的设计如下图:
image.png
每个方法职责明确,开发简单且容易维护。


最佳实践

单一职责原则受到非常多因素的限制。这个原则在纯理论上十分优秀,但实现时有一定难度,要因实际情况而定。因此对于单一职责原则,建议 接口一定要做到职责单一,类的设计尽量做到只有一个原因引起变化