您的位置:首页 > 编程语言

【整洁代码之设计篇】Clean Design 1 - Single Responsibility Principle理论和实践

prenticebo 2014-04-17 18:00 30 查看

整洁代码之设计篇 - Clean Design 1: Single Responsibility Principle理论&实践

1. 前言

作为“整洁代码之设计篇”系列的第一篇文章,在这篇博客中,我将给予在公司中为Java工程师讲授的高级课程“Clean Design - SOLID”对Single Responsibility Principle的理论和实践进行较为详细的阐述。整个系列博客的定位不是讲网上或者经典书籍中现有的资料再一次汇总,相反,我将重点着力于这些“传说”中的设计原则在实际工作中的启示和使用。因此,如果你想获得关于这些SOLID原则深入和详细的理论论述,请参看Uncle Bob大叔的经典PPP(Agile Software
Development: Principle, Pattern and Practice);如果你想获得如何在实际项目中实践这些原则的建议,那么请继续读下去。

2. 单一职责原则:一句话概述

 



(Reference: Uncle Bob PPP)
如果用一句话来概括“单一职责”原则的话,那就是:任何一个类应该只有一个理由(或者源头)会让其产生改变“。

可能会有读者要问了:这个解释里面似乎确实了”职责”这个关键词啊?那么好,就让我们庖丁解牛,来好好分析一下这”单一“和”职责“的含义,以及上面这句概述的启示。

3. 什么是”职责”?

在“单一职责原则“这个语境中,职责(Responsbile)的含义是:改变的理由(a reason to change)。正如Webster词典里解释的一样:

 



 
(Webster dictionary)

如果对一个类(Class),或者一个模块(module),你能发现多余一个的理由,或者源头,或者用户能够让这个类或者模块发生改变,那么我们就说这个类或者模块承担了“多个职责”。

好了,让我们来看一个例子:

 



在这个简单的调制解调器的例子中,你能发现几个让其改变的理由?我看到的有:如果当调制解调建立链路的方式发生变化的时候,这个类的dial()和hangup()方法会收到影响而改变;同时,如果链路上数据传输的方式发生了变化,那么send()和recv()方法也会受到影响。因此,我们可以认为这个类有“多个”让其产生改变的理由,因此在“单一职责”原则这个角度,这个类有design smell。

让我们再看一个模块的例子:



 
在这个例子中,你能发现多少个Actor?同时,对于Rectangle这个类,你能指出多少个让其可能产生变化的理由或者源头?

在我看来,在这个例子中,至少有两个不同的角色:第一个是对矩形的面积(或者说数学上对矩形的建模)感兴趣的Actor;第二个是对如何将矩形绘制出来感兴趣的Actor。而这两个Actor同时都会直接和Rectangle这个类建立联系(依赖关系),那么对于Rectangle来说,可能的改变就会来自两个地方(例如:对矩形的建模方式可能会改变,对矩形绘制的具体策略也会发生改变)。那么根据我们上面的论述,这里的Rectangle就违法了”单一职责“原则。

4. 为什么承担多职责是坏事呢?

在生活中,能者多来是好事,但是为什么在软件建模中一个类或者模块承担了多个职责会是不好的设计呢?原因就在于因为有多个源头会引发改变,那么受影响的类或者模块就会有更多的可能需要改变;而改变意味着重新产生测试用例,重新编码,重新编译,重新验收测试,重新部署,重新…………这一切都会是对时间和精力的耗费(想一想上一次你重新把你在客户那里的DLL或者JAR包重新部署的过程和客户的抱怨)。

 



所以,尽可能的让已有模块保持稳定,限制变化的扩散和利用plugin模式进行系统的升级(而不是重新部署原有系统),就是“单一职责”原则背后的深意。

5. 怎样满足“单一职责”呢?

5. 1 招式1 (Kata 1) - 拆分多职责,接触强依赖

招式解释:通过将多个职责分解到不同的类或者模块,每次分解可以拆分一个职责,并将之前改变的扩散在一个方向上降低。
招式局限:单纯的职责拆分无法达到彻底隔离实现细节上的变化的影响扩散问题。
招式示例:还是利用上文中的矩形的例子,通过使用这个招式,我们可以将“数学上矩形的建模”和“绘制矩形”两个责任分解到两个模块,并让其中一个模块通过delegate的方式将两个功能聚合。如下图:



 
在这个设计中:
-  在“绘制矩形”这个领域的变化将会隔离在Graphical Applicaiton和Rectange两个类中,而GUI和Geometric Rectange,Computational Geometry Applicaiton将不受影响(他们是被依赖的对象)。而在原来的设计中这个变化会扩散到这些模块。所以,我们解决了一个方向上的变化的扩散。
-  但是,在“数学上对矩形的建模”的改变,还是会扩散到Rectangle和Graphical Application。因此,这个方向上的改变还是会扩散。

基于此,我们引入招式2.

5.2 招式2 (Kata 2)- 利用抽象层次进行接口隔离

招式解释:通过将不变的高层逻辑进行抽象为接口,而让具体的实现依赖于高层(相对稳定)的逻辑,从而达到分解职责和隔离双向变化引发的扩散。
招式局限:引入一定数量的接口
招式示例:继续上面的矩形的例子,为了将Geometric Rectangle上可能发生的变化不要传播到Rectangle这一端,我们可以将Geometric Rectangle中稳定的部分抽象成为一个接口(interface Shape)。同时,让Rectangle依赖于稳定的接口而非具体的实现,从而实现变化扩散的双向隔离。具体来说,Graphical Applicaiton和Rectange draw()的变化不会扩散到Shape和Geometric Rectangle这端;同理,Geometric
Rectangle的变化(只要没有影响Shape接口),就会被Shape接口隔离。



在这个设计改进中,通过拆分多个职责到不同的类和模块,同时通过加入抽象层来隔离改变可能产生的扩散,我们很好的达到了每个模块或者类只有一个改变的理由(responsbilility),并且让系统成为一个增量扩展系统(plugin模式,而不是redeploy模式)。

 
相关文章推荐