设计模式六大原则1:单一职责原则

单一职责原则,(Single Responsibility Principle).

1. 定义

There should never be more than one reason for a class to change,应该有且仅有一个原因引起类的变更。

职责:业务逻辑,或者对象能够承担的责任,并以某种行为方式来执行。


2.理解

该原则提出了对对象职责的一种理想期望。对象不应该承担太多职责,正如人不应该一心分为二用。唯有专注,才能保证对象的高内聚;唯有单一,才能保证对象的细粒度。对象的高内聚与细粒度有利于对象的重用。

一个庞大的对象承担了太多的职责,当客户端需要该对象的某一个职责时,就不得不将所有的职责都包含进来,从而造成冗余代码或代码的浪费。这实际上保证了DRY原则,即"不要重复你自己(Don't Repeat Yourself)",确保系统中的每项知识或功能都只在一个地方描述或实现。

单一职责原则还有利于对象的稳定。所谓"职责",就是对象能够承担的责任,并以某种行为方式来执行。对象的职责总是要提供给其他对象调用,从而形成对象与对象的协作,由此产生对象之间的依赖关系。对象的职责越少,则对象之间的依赖关系就越少,耦合度减弱,受其他对象的约束与牵制就越少,从而保证了系统的可扩展性。

单一职责原则并不是极端地要求我们只能为对象定义一个职责,而是利用极端的表述方式重点强调,在定义对象职责时,必须考虑职责与对象之间的所属关系。职责必须恰如其分地表现对象的行为,而不至于破坏和谐与平衡的美感,甚至格格不入。换言之,该原则描述的单一职责指的是公开在外的与该对象紧密相关的一组职责。

例如,在媒体播放器中,可以在MediaPlayer类中定义一组与媒体播放相关的方法,如Open()、Play()、Stop()等。这些方法从职责的角度来讲,是内聚的,完全符合单一职责原则中"专注于做一件事"的要求。如果需求发生扩充,需要我们提供上传、下载媒体文件的功能。那么在设计时,就应该定义一个新类如MediaTransfer,由它来承担这一职责;而不是为了方便,草率地将其添加到MediaPlayer类中。

单一职责适用于接口、类、同时也适用于方法。方法的粒度也不宜过粗。要尽量做到接口的单一职责!


3.问题由来

   类T负责两个不事的职责:职责P1、职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原来运行的职责P2功能发生故障。解决方法:分别建立两个类完成对应的功能。

所谓职责扩散,就是因为某种原因,职责P被分化为粒度更细的职责P1和P2。           

   比如:类T只负责一个职责P,这样设计是符合单一职责原则的。后来由于某种原因,也许是需求变更了,也许是程序的设计者境界提高了,需要将职责P细分为粒度更细的职责P1,P2,这时如果要使程序遵循单一职责原则,需要将类T也分解为两个类T1和T2,分别负责P1、P2两个职责。但是在程序已经写好的情况下,这样做简直太费时间了。所以,简单的修改类T,用它来负责两个职责是一个比较不错的选择,虽然这样做有悖于单一职责原则。

4.好处:

  • 类的复杂性降低,实现什么职责都有清晰明确的定义;
  • 可读性提高,复杂性降低,那当然可读性提高了;
  • 可维护性提高,那当然了,可读性提高,那当然更容易维护了;
  • 变更引起的风险降低,变更是必不可少的,接口的单一职责做的好的话,一个接口修改只对相应的实现类有影响,与其他的接口无影响,这个是对项目有非常大的帮助。

5.难点

5.1 职责划分无量化标准:学究理论还是工程应用?后者时,要考虑可变因素与不可变因素,以及相关的收益成本比率等。

5.2 单一职责妥协:项目中单一职责原则很少得以体现,或者非常难(囿于国内技术人员的地位、话语权、项目中的环境、工作量、人员的技术水平、硬件资源等,最终的结果就是常常违背单一职责原则)。

6.实践建议

6.1 接口一定要做到SRP,类的设计尽量做到只有一个原因引起变化

6.2 妥协原则:

A.只有逻辑足够简单,才可以在代码级别上违背SRP;
B.只有类中方法数量足够少,才可以在方法级别上违背SRP;
C.实际应用中的类都要复杂的多,一旦发生职责扩散而需要修改类时,除非这个类本身非常简单,否则还是要遵循SRP。

7.范例

7.1 职责分明示例(Role-Based Access Control,基于角色的访问控制)(属性与行为分离) 


项目中常用到 用户、机构、角色管理这些模块,基本上使用的都是RBAC模型(通过分配和取消角色来完成用户权限的授予与取消,使动作主体(用户)与资源的行为(权限)分离)。 但上述接口设计得有问题,用户的属性与用户的行为没有分开。

应将其拆分为两个接口,IUserBO负责用户的属性,也即收集和反馈用户的属性信息;IUserBiz负责用户的行为,完成用户信息的维护与变更。



代码清单1-1 分清职责后的代码示例

[java]  view plain copy
  1. .......  
  2. IUserBiz userInfo = new UserInfo();  
  3. //我要赋值了,我就认为它是一个纯粹的BO  
  4. IUserBO userBO = (IUserBO)userInfo;  
  5. userBO.setPassword("abc");  
  6. //我要执行动作了,我就认为是一个业务逻辑类  
  7. IUserBiz userBiz = (IUserBiz)userInfo;  
  8. userBiz.deleteUser();  
  9. .......  

实际上,在项目中,更倾向于使用如下的结构图:


7.2 职责分明的电话类图(行为与行为相分离)

举个电话的例子,电话通话的时候有4个过程发生:拔号、通话、回应、挂机,写一个接口,其类图如下:


copy

    public interface IPhone {  
  1.     //拨通电话  
  2.     void dial(String phoneNumber);  
  3. //通话  
  4. void chat(Object o);  
  5. //回应,只有自己说话而没有回应,那算啥?!  
  6. void answer(Object o);  
  7. //通话完毕,挂电话  
  8. void huangup();  
  9. }  

IPhone这个接口包含了两个职责:一个是协议管理,由dial()与 hangup()两个方法实现;一个是数据传输,由chat()与 answer()实现。

考虑:

A.协议接通的变化会引起这个接口或实现类的变化吗? 会的!  数据传输(电话不仅仅通话,还可以上网)的变化也会引起其变化!这两个原因都引起了类的变化。B.电话拔通还用管使用什么协议吗?电话连接后还需要关心传递什么数据吗?

都不,即这两个职责的变化不相互影响,那就考虑拆分成两个接口,类图如下:



这个类图略有些复杂,一个手机类需要把两个对象组合在一起才能用。组合是一种强耦合关系,还不如使用接口实现的方式呢。修改如下:



7.3 职责分明到接口

修改用户信息方法 ----〉 职责分明的方法



上述写法很糟(职责不清晰,不单一),改了吧。。。




7.4 分层架构模式(一个较大的单一职责示例)

层架构模式实际上也体现了这一原则,它将整个系统按照职责的内聚性分为不同的层,层内的模块与类具有宏观的内聚性,它们关注的事情应该是一致的。例如,领域逻辑层就主要关注系统的业务逻辑与业务流程而数据的持久化与访问则交由数据访问层来负责。以订单的管理为例,我们在领域逻辑层中定义如下的类OrderManager:

[java]  view plain copy
    class OrderManager    
  1. {    
  2.     private IOrderRepository repository =     
  3.   RepositoryFactory.CreateOrderRepository();    
  4. void Place(Order order)    
  5.     {    
  6.         if (order.IsValid())    
  7.         {    
  8.             repository.Add(Order);    
  9.         }    
  10. else    
  11.       throw new InvalidOperationException("Order can't be placed. ");    
  12.     }    
  13.     void Cancel(Order order)    
  14.     {    
  15.         if (order.IsValid() && order.CanCancel(DateTime.Now))    
  16.         {    
  17.             repository.Remove(Order);    
  18.         }    
  19. else    
  20.             new InvalidOperationException("Order can't be canceled. ");    
  21.     }    
  22. }    
  23. static class RepositoryFactory    
  24. {    
  25.      static IOrderRepository CreateOrderRepository()     
  26.      {    
  27.           return new OrderRepository();    
  28.      }    
  29. }   

OrderManager类的实现体现了单一职责原则的思想。

  • 首先,OrderManager类中的Place()和Cancel()方法均属于订单管理的业务逻辑,与领域逻辑层关注的事情是一致的。
  • 在这两个方法的实现中,我们需要检验订单的正确性(检验订单是否包含了必要的信息,如联系人、联系地址与联系电话),以及判断当前时间是否在允许取消订单的时间范围内。虽然它们仍然属于订单处理的业务逻辑,但拥有这些检查信息的是Order对象,而不是OrderManager,即Order对象是检查订单的信息专家 。因此,IsValid()和CanCancel()方法应该被定义在Order类中。
  • 至于添加和移除订单的操作,虽然保证了下订单和取消订单的业务逻辑实现,但其实现却属于数据访问层的范畴,因而该职责被委派给了OrderRepository类 。
  • 至于RepositoryFactory类,则是负责创建OrderRepository对象的工厂类。

这些类的职责以及协作关系如图2-4所示。


将数据访问的逻辑从领域对象中分离出去是有道理的,因为数据访问逻辑的变化方向与订单业务逻辑的变化方向是不一致的,引起职责发生变化的原因也不相同。这也是单一职责原则的核心思想。遵循该原则,就能够有效地分离对象的变与不变,将变化的职责以抽象的方式独立于原对象之外,原对象就更加稳定。Martin Fowler认为,设计一个模型时应使该模型中最频繁修改的部分所影响的类型数量达到最少。我们对访问Order数据表的逻辑进行了封装与抽象,以隔离数据访问逻辑的变化,即使数据访问逻辑发生变化,它影响到的只是OrderRepository类而已。


7.5  妥协示例(项目中常见的单一职责违背可接受示例)

所谓职责扩散,就是因为某种原因,职责P被分化为粒度更细的职责P1和P2.

比职:类T只负责一个职责P,这样设计是符合SRP的。后来由于某种原因,需要将职责P细分为粒度更细的P1与P2,这时如果要遵循SRP,需要将类T也分解为两个类T1和T2,分别负责P1、P2这两个职责。但是在程序已经写好的情况下,这样做简直太费时间了。所以,简单的修改类T,用它来负责两个职责是一个比较不错的选择,虽然这样做有悖于SRP。

如,用一个类描述动物呼吸这个场景:

copy
    class Animal{  
  1. void breathe(String animal){  
  2.         System.out.println(animal+"呼吸空气");  
  3.     }  
  4. }  
  5. class Client{  
  6. void main(String[] args){  
  7.         Animal animal = new Animal();  
  8.         animal.breathe("羊");  
  9. 运行结果:  
  10. 羊呼吸空气  

程序上线后,发现问题了,并不所有动物都呼吸空气的,如鱼是呼吸水的。  修改时如若遵循SRP,则需将Animal类细分为陆生动物类Terrestrial ,水生动物 Aquatic,代码如下: [修改方式一]

copy
    class Terrestrial{  
  1. class Aquatic{  
  2. void breathe(String animal){  
  3.         System.out.println(animal+"呼吸水");  
  4.     }  
  5. }  
  6. class Client{  
  7. void main(String[] args){  
  8.         Terrestrial terrestrial = new Terrestrial();  
  9.         terrestrial.breathe("羊");  
  10.         Aquatic aquatic = new Aquatic();  
  11.         aquatic.breathe("鱼");  
  12.   
  13. 羊呼吸空气  
  14. 鱼呼吸水  

我们发现这样修改花销是很大的,除了将原来的类分解之外,还需要修改客户端。直接修改类Animal来达成目的,虽然违背SRP,但花销却小的多,代码如下: [修改方式二]

copy
    if("鱼".equals(animal)){  
  1.             System.out.println(animal+"呼吸水");  
  2.         }else{  
  3.             System.out.println(animal+"呼吸空气");  
  4.         }  
  5.         animal.breathe("鱼");  
  6. 鱼呼吸水  

可以看到,这种修改方式要简单得多。但是却存在着隐患:有一天需要将鱼分为呼吸淡水和海水的鱼,则又要修改Animal类的breathe方法,而对原有的代码修改会对调用“羊”等相关功能带来风险也许某一天,你会发现程序运行的结果变为“羊呼吸水”了 。这种修改方式直接在代码级别上违背了SRP,虽然修改起来最简单,但隐患却最大。还有一种修改方式: [修改方式三]
copy
    class Animal{  
  1. void breathe(String animal){  
  2.         System.out.println(animal+"呼吸水");  
  3.     }  
  4. void breathe2(String animal){  
  5.         System.out.println(animal+"呼吸空气");  
  6.     }  
  7. }  
  8.    
  9. class Client{  
  10. void main(String[] args){  
  11. new Animal();  
  12.         animal.breathe("羊");  
  13.         animal.breathe2("鱼");  
  14. 运行结果:  
  15.     羊呼吸空气  
  16.     鱼呼吸水  

可以看出,这种修改没有改动原来的方法,而是在类中添加了一个方法,这样虽然也违背了SRP,但在方法级别上却是符合SRP的,因为它并没有改动原来方法的代码。这三种方式各有优缺点,那么在实际编程中,采用哪一种呢?这需要根据实际情况而定:建议:
A.只有逻辑足够简单,才可以在代码级别上违背SRP;
B.只有类中方法数量足够少,才可以在方法级别上违背SRP;
例如本文所举的这个例子,它太简单,它只有一个方法,所以,无论在代码还是在方法级别违背SRP,都不会造成太大的影响。 实际应用中的类都要复杂的多,一旦发生职责扩散而需要修改类时,除非这个类本身非常简单,否则还是要遵循SRP。

相关文章

什么是设计模式一套被反复使用、多数人知晓的、经过分类编目...
单一职责原则定义(Single Responsibility Principle,SRP)...
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强...
适配器模式将一个类的接口转换成客户期望的另一个接口,使得...
策略模式定义了一系列算法族,并封装在类中,它们之间可以互...
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,...