设计模式六大原则之--单一职责原则SRP

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

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

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


2.理解

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

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

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

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

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

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


3.问题由来

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


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 分清职责后的代码示例


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


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

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


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

考虑:

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

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



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



7.3 职责分明到接口

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



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




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

层架构模式实际上也体现了这一原则,它将整个系统按照职责的内聚性分为不同的层,层内的模块与类具有宏观的内聚性,它们关注的事情应该是一致的

例如,领域逻辑层就主要关注系统的业务逻辑与业务流程而数据的持久化与访问则交由数据访问层来负责

以订单的管理为例,我们在领域逻辑层中定义如下的类OrderManager:


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

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


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


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

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

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

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


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


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


可以看到,这种修改方式要简单得多。但是却存在着隐患:有一天需要将鱼分为呼吸淡水和海水的鱼,则又要修改Animal类的breathe方法,而对原有的代码修改会对调用“羊”等相关功能带来风险也许某一天,你会发现程序运行的结果变为“羊呼吸水”了 。这种修改方式直接在代码级别上违背了SRP,虽然修改起来最简单,但隐患却最大。还有一种修改方式: [修改方式三]

可以看出,这种修改没有改动原来的方法,而是在类中添加了一个方法,这样虽然也违背了SRP,但在方法级别上却是符合SRP的,因为它并没有改动原来方法的代码。这三种方式各有优缺点,那么在实际编程中,采用哪一种呢?这需要根据实际情况而定:建议:
A.只有逻辑足够简单,才可以在代码级别上违背SRP;
B.只有类中方法数量足够少,才可以在方法级别上违背SRP;

例如本文所举的这个例子,它太简单,它只有一个方法,所以,无论在代码还是在方法级别违背SRP,都不会造成太大的影响。 实际应用中的类都要复杂的多,一旦发生职责扩散而需要修改类时,除非这个类本身非常简单,否则还是要遵循SRP。

转自: http://www.jb51.cc/article/p-zylfnqvs-re.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
迭代器模式提供了一种方法,用于遍历集合对象中的元素,而又不暴露其内部的细节。
外观模式又叫门面模式,它提供了一个统一的(高层)接口,用来访问子系统中的一群接口,使得子系统更容易使用。
单例模式(Singleton Design Pattern)保证一个类只能有一个实例,并提供一个全局访问点。
组合模式可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
装饰者模式能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。
观察者模式(Observer Design Pattern)定义了对象之间的一对多依赖,当对象状态改变的时候,所有依赖者都会自动收到通知。
代理模式为对象提供一个代理,来控制对该对象的访问。代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
工厂模式(Factory Design Pattern)可细分为三种,分别是简单工厂,工厂方法和抽象工厂,它们都是为了更好的创建对象。
状态模式允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
命令模式将请求封装为对象,能够支持请求的排队执行、记录日志、撤销等功能。
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 基本介绍 **意图:**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为
享元模式(Flyweight Pattern)(轻量级)(共享元素)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结