包组织原则
- 将在功能上有紧密联系的、垂直或水平的切片打包
- 将一族接口打包
- 将一组不稳定的类打包
- 提取独立的类型
- 利用工厂(factory)来降低实体包之间的依赖
- 不要在包中出现回路
提示:功能性相关的类。
一般说来,决定类之间是否有功能性联系,可以看是否满足以下条件:
- 在一个类里发生变化会影响到另一个类;
- 移除一个类将会影响另一个类;
- 两个类之间有复杂的内部交互或是互相传递大量的信息;
- 如果一个边界类(boundary class)中的一个函数是用来呈现某个实体类的,那么这个边界类将和这个实体类有功能性关联;
- 两个类被同一个因素影响或产生交互;
- 两个类之间有关系;
- 一个类创建另一个类的实体;
判断两个类不应该放在同一个包里,可以观察是否满足以下条件之一:
- 两个类与不同的因素有关系;
- 一个可选类和一个强制类不应该放在同一个包里;
包元素的可视性。一个包里,只有pulic属性的类可以和外部进行交互。
包设计原则
包的设计原则可分为两大类:
一类是间隔尺度,规定了包的内聚度,包括1)发布和重用等价原则,REP(The Release-Reuse Equivalency Principle);2)全部重用原则,CRP( The Common Reuse Principle);3)公共闭合原则,CCP(The Common Closure Principle)。
另一类是稳定性,规定了包之间的耦合度,包括1)非循环依赖原则,ADP(The Acyclic Dependencies Principle);2)稳定依赖原则,SDP(The Stable Dependencies Principle);3)稳定抽象原则,SAP(The Stable Abstractions Principle)。
发布重用原则(REP)
REP规定,
The granule of reuse is the granule of release,即重用粒度等于发布粒度。它包含了以下几个意思:
- 包通常是一个发布版本中的基本单位;
- 为了提供重用所需要的保证,开发人员必须将他们的软件分成一些可重用的包,然后跟踪这些包,再发布新的版本;
- 我们重用的任何东西都必须被发布并且跟踪;
- 一个包中的元素或类要么都可重用,要么都不可重用。这是因为如果一个包里包含了某个可以重用的软件,它就不应该再包括不是用于重用的软件。
重用发布等价原则为我们指明了包的设计方针:
一个包中的元素(类)要么都可重用,要么都不可重用。
全部重用原则(CRP)
CRP规定,
The classes in a package are reused together. If you reuse one of the classes in a package, you reuse them all.也就是说,
包的所有类被一起重用。如果你重用了其中的一个类,就重用全部。换一种通俗的说法:
Classes that aren’t reused together should not be grouped together,
没有被一起重用的类不应该被组合在一起。
CRP帮助我们决定哪些类应该放在同一个包里:那些将会一起重用的类应该放在同一个包里。
CRP更告诉我们哪些类不应该放在同一个包里:那些不是紧密联系的类不应该放在同一个包里。可以独立使用或在不同情形下使用的类型应该放在不同的包里。
依赖一个包就是依赖这个包所包含的一切。当一个包发生了改变,并发布新的版本,使用这个包的所有用户都必须在新的包环境下验证他们的工作,即使被他们使用的部分没有发生任何改变。
因为如果包中包含有未被使用的类,即使用户不关心该类是否改变,但用户还是不得不升级该包并对原来的功能加以重新测试。
公共闭合原则(CCP)
CCP规定,
The classes in a package should be closed together against the same kinds of changes. a change that affects a package affects all the classes in that package,
一个包中所有的类应该对同一种类型的变化关闭。一个变化影响一个包,便影响了包中所有的类,而且不会影响其他包。
一个更简短的说法是:
Classes that change together, belong together.
一起修改的类,应该组合在一起(同一个包里)。
CCP跟开闭原则(OCP: Open Closed Principle) 有着很深的渊源关系,CCP的“关闭”(closure)就是OCP所提倡的:classes should be closed for modification but open for extension. 类应该对修改关闭,对扩展开放。但我们知道,100%的“关闭”是不现实的,我们在设计系统时,只能尽量地保持对大多数可预见的修改关闭。
CCP延伸了OCP的“关闭”概念,当因为某个原因需要修改时,把需要修改的范围限制在一个最小范围内的包里。
非循环依赖原则(ADP)
ADP规定,The dependency structure between packages must be a directed acyclic graph (DAG). That is, there must be no cycles in the dependency structure.包之间的依赖结构必须是一个直接的无环图形(DAG)。也就是说,在依赖结构中不允许出现环(循环依赖)。
如果出现了包循环依赖问题,如包A依赖包B,包B依赖包C,而包C又依赖包A,那么我们修改了B并需要发布B的一个新的版本,因为B依赖C,所以发布时应该包含C,但C同时又依赖A,所以又应该把A也包含进发布版本里。也就是说,依赖结构中,出现在环内的所有包都不得不一起发布。它们形成了一个高耦合体,当项目的规模大到一定程度,包的数目变多时,包与包之间的关系便变得错综复杂,各种测试也将变得非常困难,常常会因为某个不相关的包中的错误而使得测试无法继续。而发布也变得复杂,需要把所有的包一起发布,无疑增加了发布后的验证难度。
为了打破这种循环依赖,有两种解决方法:
1)找出两个包里参与这个循环的因素,并把它们组合成一个新的包。
例如,若存在以下依赖关系。
包C要依赖包A,必定A中包含有A,C共通使用的类,把这些共同类抽出来放在一个新的包D里。这样就把C依赖A变成了C依赖D以及A依赖D,从而打破了循环依赖关系。如图:
这样,包的依赖关系就从A->B->C->A变成了:
A->B->C->D
A->D
2)利用接口。ISP(接口分隔原则)可以剔除没用到的接口。DIP(依赖倒置原则)在类的调用之间引入抽象层。
例如,若存在左下图的循环依赖。
那么A必然使用了B中的某个类,这样我们可以吧这个类分离成一个接口,并让B实现这个接口并依赖于它,同时让包A也依赖于这个接口,就可以打破这种循环。
又例如,三个循环依赖的类Myapplication、MyTasks和MyDialoges,如左下图。如果用方法一,我们可以将Myapplication和MyDialoges共同使用的类分离成一个新的包,如右下图所示;如果用方法二,MyDialoges中的类X使用了Myapplication中的类Y,我们可以为类Y设计一个接口IX,并把它放在包MyDialogues里,再让Myapplication中的Y实现并该包依赖于它。
稳定依赖原则(SDP)
SDP规定,
The dependencies between packages in a design should be in the direction of the stability of the packages. A package should only depend upon packages that are more stable that it is.
一个设计中的包之间的依赖应该朝着稳定的方向进行。一个包只应该依赖那些比自己更稳定的包。
换成另一个说法是:
Depend in the direction of stability.
朝着稳定的方向进行依赖。
也就是说,包应该依赖比自己更稳定的包。因为如果依赖一个不稳定的包,那么当这个不稳定的包发生变化时,本身稳定的包也不得不发生变化,变得不稳定了。
所谓稳定,在现实生活中是指一个物体具有稳固不变的属性使它很难发生变化。应用到软件概念上,我们认为一个软件是稳定的,是因为这个软件很难发生改变,或更确切地说,是不需要发生改变。一个设计良好,能应对各种变化不需要修改的软件当然是稳定的了,但事实上,往往一个软件常常需要对应某个事先没有预测到的用户需求而不得不发生改变,当这种改变发生时,能把修改控制在最小的范围之内,并能稳定的工作(包括软件本身以及依赖它的其它软件实体等),我们也会认为该软件是相对稳定的。
那么,怎么判断一个包是否稳定呢?我们可以通过下面的方法来判断一个包的稳定系数:
- Ca:Afferent Coupling。向心耦合。依赖该包(包含的类)的外部包(类)的数目(i.e. incoming dependencies)。
- Ce: Efferent Coupling。离心耦合。被该包依赖的外部包的数目(i.e. outgoing dependencies)。
I: Instability。不稳定性。I=Ce/(Ce+Ca)。它的值处于[0,1]之间。I值越小越稳定。
如图1,X的Ce=0,所以不稳定性I=0,它是稳定的。相反,如图2,Y的Ce=3,Ca=0,所以它的不稳定性I=1,它是不稳定的。
SDP要求一个包的不稳定性I要大于它所依赖的包的不稳定性。“Depend upon packages whose I metric is lower than yours.”
换句话说,沿着依赖的方向,包的不稳定性应该逐渐降低,稳定性应该逐渐升高。
例如,左下图中一个稳定的包依赖了一个不稳定的包,就违反了SDP原则。
那么我们怎样打破这种不稳定关系呢?我们可以使用
DIP(依赖倒置原则)在类的调用之间引入抽象层。如右上图中,我们从不稳定的包中分离出一个接口IU,并让不稳定包中的类C实现这个接口,再让U依赖这个接口。(因为接口是抽象的,而抽象的往往都是稳定的。)
稳定抽象原则(SAP)
SAP原则规定,
Packages that are maximally stable should be maximally abstract. Instable packages should be concrete. The abstraction of a package should be in proportion to its stability.
最稳定的包应该是最抽象的包。不稳定的包应该是具体的包。包的抽象程度跟它的稳定性成正比。
一个包的抽象程度越高,它的稳定性就越高。反之,它的稳定性就越低。一个稳定的包必须是抽象的,反之,不稳定的包必须是具体的。
稳定的包的构成
抽象类或接口通过子类继承扩展行为,这表示抽象类或接口比它们的子类更具有稳定性。总之,为了构成稳定的包,应该提高包内的抽象类或接口的比率;它们的子类可以放在另一个不稳定的包内,该包依赖上述稳定的包,从而遵循了稳定依赖原则(SDP)。
理想的体系结构应该是:
不稳定的(容易改变的)包处于上层
- 它们是具体的包实现
稳定的(不容易改变的)包处于下层
- 不容易改变,但容易扩展
- 接口比实现(具体的运行代码)在内在特性上更具有稳定性
因此,我们可以1)将具有功能性关联的接口放在同一个包里,并与其实现分离开来;2)利用工厂来降低具体的包之间的依赖,一种提高包的稳定性的方法是减少它对其他包中具体的包类的依赖。
我们通常使用工厂(factory)来降低包之间的耦合度。
参考资料:包的设计原则 http://www.uml.org.cn/mxdx/200912233.asp