优美的包裹——面向包和组件设计的架构模式原则
之前写了一篇关于面向类(对象)的几个设计模式原则,但随着应用程序规模和复杂度的增加,我们需要更高层次的包和组件来对其之间的依赖关系进行组织管理。
首先明确一下本文中谈到的包或组件的概念,因为通常软件开发中关于包这个术语都有各自的说法,所以在本文中需要明确说明,在此定义为一种能够被独立部署的二进制单元。而在.NET中,包通常是一个被称为程序集assembly的动态链接库DLL,也可以是子系统、库或组件。
大型系统的设计好坏依赖于是否有好的包或组件设计。那么我们应该使用什么设计原则来管理包之间的关系呢?
很幸运,Bob大叔已经为我们做好了规划:
1、重用-发布等价原则(Reuse-Release Equivalence Principle, REP)
2、共同重用原则(Common-Reuse Principle, CRP)
3、共同封闭原则(Common-Closure Principle, CCP)
4、无环依赖原则(Acyclic-Dependencies Principle, ADP)
5、稳定依赖原则(Stable-Dependencies Principle, SDP)
6、稳定抽象原则(Stable-Abstractions Principle, SAP)
上述6个设计原则,描述了包的内容和相互管理的关系。
前三个原则(REP、CRP、CCP)用来指导如何把类划分到包中,属于包的内聚性设计要求(package cohesion),考虑的是粒度;
后三个原则(ADP、SDP、SAP)用于处理包之间的关系,属于包的耦合性设计要求(package coupling),考虑的是稳定性。
内聚性:粒度
包的内聚性原则是开发者决定如何把类划分到不同包中的指导,它是一种“自底向上”的思想。
内聚性不单单是指一个模块执行一项且仅单独一项功能,它还需要考虑到可重用性(reusability)和可开发性(developability),及其之间的相互作用力和需求之间的平衡关系。
REP
The granule of reuse is the granule of release.
重用的粒度就是发布的粒度。
当开发人员重用一个类库时,都希望有清楚的文档说明,稳定的代码功能,清晰的接口格式等。但优秀的开发人员有更高的期望:首先类库的作者能够保证在相对长的时间内持续维护这些代码,与其将来要自己要花时间去维护这些代码,我相信你更愿意自己去花时间设计更好的组件;其次是保证该组件的兼容性,谁都不愿意使用甚至忍受反复无常的变化,至少要保证一段时间内(一个月?一个季度?半年?)的支持,当然这方面可以通过行政上的手段获得的支持。
所以REP指出:一个组件的重用粒度(granule of reuse)可以和发布粒度(granule of release)一样大。我们所重用的任何包、组件、类库都必须同时被发布和跟踪。建立一个跟踪系统,为潜在的使用者提供所需要的变更通知、安全性和支持,让重用真正的成为可能。
如果一个包中的软件是用来重用的,那么就不能再包含任何不是为了重用目的而设计的软件。也就是说,一个包中的类要么都是可重用的,要么都不是可重用的。另外,一个包中所有类对于同一类用户来说都应该是可重用的。不要将为不同类型用户设计的类放入同一个包中。
CRP
The classes in a package are reused together. If you reuse one of the classes in a package, you reuse them all.
一个包中的所有类应该是共同重用的。如果重用了包中的一个类,那么就要重用包中所有类。
这个原则规定了:(1)、趋向于共同重用的类应该属于同一个包;(2)、相互之间没有紧密联系的类不应该在同一个包中。
它不仅帮助我们决定哪些类需要且应该放进同一个包中,还告诉开发人员什么类不可以放在一起。如果类之间的关系是紧密耦合的,可重用的类需要与作为该可重用抽象的一部分的其他类协作,那么很明显这些类应该在同一个包中。如果一个包仅仅是使用了另外一个包中的一个类,然而事实上并无法削弱这两个包之间的依赖关系,所以每当依赖于一个包时,应该依赖于其中的每一个类。
确保包中的所有类是不可分开的,避免不必要的重新验证和重新部署。
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.
包中所有类对于同一种性质的变化应该是共同封闭的。一个变化若对一个封闭的包产生影响,则将对该包中的所有类产生影响,而对于其他包则不造成任何影响。
该原则规定:一个包不应该包含多个引起变化的原因。 PS:似曾相识啊
在实际的应用程序开发过程中,大部分软件对可维护性的要求往往要高于可重用性。可维护性的重要性更大。所以软件中涉及到必须更改代码的情况,开发人员肯定更希望是尽量在同一个包中进行修改。对于一些确定变化类型的开放类或可能由于同样原因而产生变更的所有类共同组织在同一个包中,进行策略性的封闭,将变化限制在最小数量的包中,有助于减少软件的重新发布。
耦合性:稳定
包的耦合性反应了不同包之间的依赖关系。其受影响的因素有很多,如可开发性、逻辑设计、技术路线和行政力量等。我们可以通过依赖性管理度量去测试和判断一个设计的依赖性与抽象结构模式间的匹配程度。
ADP
The dependency structure between packages must be a directed acyclic graph (DAG). That is, there must be no cycles in the dependency structure.
在包的依赖关系图中不允许存在环。
次晨综合症(morning-after syndrome):忙碌了一天,终于开发、测试、提交完成了某项功能,第二天回到公司却发现昨天那项完成的功能却不能用了,或是无法正常运行了,o(︶︿︶)o, 多么熟悉的场景啊~想必有过这样经历的人还不在少数,而且他们明确的知道,肯定是有人更改了该项功能所依赖的组件中的某些代码。
针对这个问题,目前主要形成了两个解决方案:每周构建和ADP。
顾名思义,每周构建的工作方式为一周的前4天所有开发人员互不干扰的各自独立开发,并在周五进行集成。它的好处在于前四天的高效无干扰工作,不利之处是每周五将要付出巨大的集成代价。更糟糕的是,随着项目的增长,集成的工作量会不断增加,造成团队的效率随之下降。所以,每周构建适合于中小型规模的项目开发。
ADP旨在消除依赖环和解除依赖环。
我们可以通过把开发环境划分成可发布的组件来解决消除依赖环的问题。需要注意的是,首先,组件的依赖关系中不能有环;其次,无论从哪个组件开始,都无法沿着依赖关系绕回到这个组件。
保证组件的依赖关系结构是一个有向无环图(DAG)。
那么,我们如何解除组件之间的依赖环并把依赖关系图恢复为一个DAG呢?主要有两个方法:
(1)、使用依赖倒置原则(DIP)解除依赖环:开发者应该从客户、使用者的角度出发来命名接口。
(2)、使用新组件解除依赖环:容易导致依赖关系结构增长。
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.
朝着稳定的方向进行依赖。
要使设计可维护,某种程度的可变性和易变性是必要的,而组件的稳定性更是至关重要的。
什么是稳定性?稳定性和更改所需要的工作量有关。且所需工作量越大,其稳定性越高。影响一个组件更改难易程度的因素有很多,比如软件规模、复杂性、清晰度等等。这里我们抛开上述这些因素不谈,关注以下简单、可行的方法:让其他许多软件组件依赖于它。
聪明的开发者早已洞穿一切:具有多输入依赖关系的组件是非常稳定的。
通过计算进、出该组件的依赖关系的数目,可以计算该组件的位置稳定性(position stability)。
□ (Ca)输入耦合度(afferent coupling):处于该组件的外部,并依赖于该组件内的类的数目。
□ (Ce)输出耦合度(efferent coupling):处于该组件的内部,并依赖于该组件外的类的数目。
□ (不稳定性I) I = Ce /(Ca+Ce)
这个度量I 的取值范围是[0,1]。
I=0表示该组件具有最大的稳定性(负有责任且无依赖性);
I=1表示该组件具有最大的不稳定性(不承担责任且有依赖性)。
该原则规定:一个组件的I 度量值应该大于它所依赖的组件的I 度量值,I 度量值应该顺着依赖的方向减少。
事实上,组件稳定性是多样的,如果一个系统中所有的组件都是最大稳定的,那么意味着该系统不能改变,显然不符合实际生产环境。系统中组件的理想配置应该是可改变的组件位于顶部并依赖于底部稳定的组件。此外,应该把封装系统高层设计的软件放进稳定的组件中,并通过抽象保证其灵活性。
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.
包的抽象程度应该与其稳定程度一致。
该原则把组件的稳定性和抽象性联系在了一起。它规定:一个稳定的组件应该是抽象的,而一个不稳定的组件应该是具体的。
A度量值是一个测量组件抽象程度的度量标准,Bob大叔给出了一个抽象性度量的计算方法:
□ Nc 代表组件中类的总数
□ Na 代表组件中抽象类的数量(抽象类是一个至少具有一个抽象方法的类,不能被实例化)
□ 抽象性 A = Na / Nc
这个度量A的取值范围是[0,1]。A=0表示该组件中无任何抽象类;A=1表示该组件中只包含抽象类。
AI组合拳:主序列(Main sequence)的距离和分析
DIP是一个处理类的原则。类没有灰度(the shades of grey)的概念,一个类要么是抽象的,要么不是。
而SDP和SAP的结合是处理包(组件)的,并且允许一个包(组件)是部分抽象、部分稳定的。
至此,有了上述SDP中的稳定性和SAP中的抽象性概念和计算方式,创建一个以A为纵轴I为横轴的二维坐标图:
从上图可以清楚的看到:最稳定、最抽象的组件位于左上角(0,1) 处,最不稳定、最具体的组件位于右下角(1,0)处。
痛苦地带(Zone of Pain):在(0,0)附近的是一些具有高度稳定性且具体的组件,但是这种组件僵化程度很高,因为它是具体的,无法对其进行扩展,又因为它是高度稳定的,因此很难对它进行更改。简单来说,位于此处的组件,它会被很多其他组件所依赖(稳定性),但是它很难扩展和修改(具体性)。
无用地带(Zone of Uselessness):在(1,1)附近的组件虽然具有最低的稳定性和最高的抽象性,但正是由于其稳定性低,导致没有其他组件依赖它们,即使具有很高的抽象性也不能得到使用。
主序列(Main sequence):连接(1,0)和(0,1)点的线。虽然组件的理想位置是主序列的两个端点,也就是说,组件要么是最稳定最抽象的,要么是最不稳定最具体的,但是这毕竟只是理想情况,现实很残酷,由于位于主序列上或者靠近主序列的组件都具有一定程度的稳定性和抽象性,所以在实际项目中,大部分组件能够位于主序列上或者主序列附近就已经算是不错的了。
为了更好地度量一个系统的组件设计是否足够好,最后再介绍一个度量:到主序列的距离。
到主序列的距离(简称距离)是指一个组件到主序列之间的距离(D),计算公式如下:
该度量值的取值范围是[0,0.707]。
除此之外,还可以用一种规范化距离(D')来表示,计算公式如下:
D'的取值范围是[0,1],0表示组件正好位于主序列上,1表示组件到主序列的距离最远。
通过使用这个度量,我们可以全面分析一个组件设计方案和主序列之间的一致性。首先可以计算出每个组件的D值,然后对所有D值不在0附近的组件进行复查和调整。这将有助于设计者确定哪些组件更容易维护,哪些组件对变化不敏感、更加稳定。
此外,还可以对组件设计方案进行统计分析或是通过绘制每个组件的D'度量值的时间分布图。
总结
Bob大叔说:组件的依赖是有好坏之分的,度量并不是万能的。应用这个六个原则能帮助并改进组件的设计,最终找到优美的包裹~
笔者个人观点:
(1)、小的软件程序不需要也确实没必要去花费大量的时间进行包的设计,过犹不及;
(2)、再小的团队也需要对源代码进行划分管理,避免造成源文件的堆积,最终导致软件的腐化。
是否真正需要对包或组件的结构进行设计和管理,取决于软件程序的规模和开发团队的规模。
内心另一种声音渐起,取决于做产品还是做项目...
仁者见仁智者见智,唯此而已。
作者: 辰希小筑 http://iPragmatic.cnblogs.com/
本文版权归作者和博客园共有,欢迎转载。转载须保留此段声明,并在明显位置给出署名和原文连接。
如果觉得有帮助的话,欢迎点击右下角的【推荐一下】,希望能够持续的跟大家分享更多有益的文章!