最后两个组件设计原则将会结合软件度量来进行介绍,将引入一些软件度量因子,对组件设计进行定量的分析与研究。
稳定依赖原则(The Stable-Dependencies Principle, SDP)
Depend in the direction of stability.
朝着稳定的方向进行依赖。
稳定性与依赖性
随着需求的明确和系统的演化,组件不可能一成不变,必要的修改是肯定的。根据前面所介绍的共同封闭原则(CCP),我们需要创建一些对某一(某些)类型的变化敏感的组件,这些组件应该是可以变化的,而且有时候我们需要它改变。由于组件之间存在依赖关系,可能会导致某些本来很容易修改的组件因为依赖它的组件而变得难以修改,SDP原则正是从这个角度出发,用于保证那些原本易于更改的组件不会被那些比它们更难更改的组件所依赖。在Robert C. Martin的ASD一书中,使用定量的方法来研究如何设计组件之间的依赖关系,在此,Sunny对该方法本身不加以太多评论,先介绍下Bob大叔到底是如何定义和实施SDP的,还是挺有意思的。
在讲解组件稳定性之前,我想先介绍一下组件之间依赖性产生的原因。众所周知,在一个系统中肯定会包含多个类,无论我们如何来组织类,降低组件之间的耦合度,不同组件中的类之间难免还是存在一些关系,例如关联、继承(实现)或依赖关系等,在组件层次,我们可以认为两个组件中的类之间如果存在关联、继承、依赖等关系,那么这两个组件之间就存在依赖关系,将组件之间的各种关系统一称为依赖关系。
接下来再介绍组件的稳定性,何谓稳定性,根据韦氏词典的定义,稳定性是指某物品“不容易被移动”。在软件中,组件的稳定性是指改变它的难易程度。要想提高一个组件的稳定性,使得它难以修改,一个最常用的方法是让更多其它的组件依赖它。依赖一个组件的其他组件越多,它的修改所造成的影响也就越大,修改所带来的工作量也越大,我们认为它就越稳定(如果这个设计方案出自一个菜鸟之手,另当别论,,可能会因为一些不必要的依赖导致系统的耦合度增加,让系统难以扩展和维护。此处所涉及的一些依赖都是必须的,不存在设计上的缺陷和失误)。
如图1所示,组件X被组件L、M和N所依赖,因此,我们至少有三个合理的理由来不对X进行修改,可以称组件X对组件L、M和N负责。此外,X并没有依赖任何其他组件,因此,外部环境的影响对X而言不会产生任何改变,此时可以称X是无依赖性的。
图1 X是一个无依赖性的稳定的组件
在图2中,组件Y依赖组件L、M和N,它是一个很不稳定的组件,没有任何组件依赖Y,因此它不承担任何责任。此外,Y依赖三个外部组件,L、M和N的修改都将对Y造成影响,此时可以称Y是有依赖性的。
图2 Y是一个有依赖性的不稳定的组件
为了能够对组件的稳定性进行量化,Bob引入了几个数值来计算组件的位置稳定性(Positional Stability)。
稳定性度量 (Stability Metrics)
为了度量组件的稳定性,引入了如下几个度量因子:
(1) Ca (afferent couplings):输入耦合度,是指位于一个组件外部,需要依赖于组件中的类的其他类的数量(The number of classes outside this component that depend on classes within this component.)。在图1中,组件X的输入耦合度为3,在图2中,组件Y的输入耦合度为0。
(2) Ce (efferent couplings):输出耦合度,是指位于一个组件内部,需要依赖组件外的其他类的类的数量(The number of classes inside this component that depend on classes outside this component.)。在图1中,组件X的输出耦合度为0,在图2中,组件Y的输出耦合度至少为1(Y中至少有一个类依赖外部类,也可能有多个类,而且可能不止3个类)。
(3) I (Instability):不稳定性因子,计算公式如下:
不稳定性因子I的取值范围为:[0,1]。I = 0表示一个组件具有最大的稳定性,不稳定性为0,也就是说Ce等于0,即输出耦合度为0,只存在别的组件依赖它,它不依赖别的组件,如图1中的组件X;I = 1表示一个组件具有最大的不稳定性,也就是Ca等于0,即输入耦合度为0,没有组件依赖它,它却依赖别的组件,如图2中的组件Y。通过计算和一个组件内的类具有依赖关系的组件外类的个数,就可以计算出Ca和Ce。
下面通过一个示例来进一步说明这些度量值的计算,本示例来自ASD一书:
图3 组件依赖关系示例
在图3中,组件Pc外部有3个类(Pa两个,Pb一个)依赖于Pc中的类,因此,Ca = 3,此外,Pc中有一个类u依赖外部组件Pd,因此,Ce = 1,I = Ce / (Ca + Ce) = 1/4。
在C++中,依赖关系通常会通过#include语句来体现,在Java中,通常通过import语句以及类的修饰名称(包含包名的完整名称)来体现,在C#中,通常通过using语句来体现。如果在源代码中每一个文件中只有一个类,那么计算I就非常简单了,我正准备做一个小工具来自动计算这些度量因子,。
SDP规定一个组件的I度量值应该大于它所依赖的组件的I度量值,也就是说,I度量值应该顺着依赖的方向减小。
当一个组件的I值为1时,说明没有任何其他组件依赖它(Ca = 0),而它却依赖其他组件(Ce > 0),这是一个最不稳定的状态。由于没有组件依赖它,因此它就没有不发生改变的理由,而它所依赖的组件会给它提供大量的更改理由。当一个组件的I值为0时,说明其他组件会依赖于该组件(Ca > 0),但是该组件却不依赖其他任何组件(Ce = 0)。这种组件达到最大程度的稳定性,由于它的依赖者的存在,导致它本身难以改变,拥有不去发生改变的理由,而且依赖这个组件的其他组件越多,不去修改该组件的理由也就越多,改变带来的影响也越大。
简单来说,SDP要求我们将I值小的组件放在底层,将I值大的组件放在顶层,因为I值越小意味着依赖它的外部组件越多,它的改变所带来的影响越大,越底层的模块应该越稳定,因此,不稳定因子I应该越小。沿着依赖链,I值应该逐步减小。
可变的组件稳定性
组件的稳定性并不是不能发生变化的,如果我们发现一个系统的组件设计违反了SDP,也就是依赖链并不是按照I值递减的次序构建的,我们可以需要对组件之间的依赖关系进行调整,使系统尽量满足SDP。可改变的组件位于顶部并且依赖于底部那些稳定的组件(The changeable components are on top and depend on the stable component at the bottom.)。
下面通过一个实例来讲解如何通过调整组件之间的依赖关系,使之满足SDP。
图4 某系统组件依赖关系示例
在图4中,组件ComponentA和ComponentB的I值为1,ComponentC的I值为1/3,ComponentD的I值为2/3,ComponentE和ComponentF的I值为0,由此可见,ComponentC和ComponentD之间的依赖关系违反了SDP。
问题来了,如何改进,使之满足SDP?
通过分析,我们发现,ComponentC与ComponentD之间的依赖关系是因为ComponentC中的类X依赖ComponentD中的类Y,如何消除这两个类之间的依赖关系成为关键所在?
解决方法很简单,与之前的ADP一样,通过依赖倒转原则DIP来实现,具体做法如下:
(1) 提供一个Y的抽象类IY,并引入一个新的组件AbstractComponent,将IY增加到AbstractComponent中;
(2) 类X依赖于AbstractComponent中的抽象类IY,而不再直接依赖于Y,将Y作为IY的子类,因此ComponentC将依赖于AbstractComponent,ComponentD也将依赖于AbstractComponent。
重构之后的组件结构如图5所示:
图5 重构之后的组件结构
看到这里,大家是不是觉得这个结构似曾相识,很多系统的架构与图5非常相似,是的,这就是我们常常会使用的引入抽象层的分层结构,不过在此我将每一层都封装在一个组件中。这个结构是严格满足SDP的,无论哪一条依赖链都是按照I值递减的次序来设计的。
事实上,我们在做构件的架构设计时,不应该经常改变的组件通常代表着系统的高层架构和设计决策,应该是稳定的,需要把这些与系统的高层设计有关的类放进稳定的组件中(如I值为0的组件),不稳定的组件(如I值为1的组件)中应该只包含那些很可能会发生改变的类。
但是,如果我们把高层设计放进这些稳定的组件,那么会导致高层设计的源代码难以修改,将使得设计失去灵活性。如何让一个稳定性高的组件(I = 0)具有足够的灵活性,能够适应需求的变化,并能够满足开闭原则OCP呢?答案是:抽象的组件,包含抽象类(接口)的组件。SDP实际上就是组件设计中的OCP和DIP,稳定的组件通常会是一些抽象组件,这也对设计抽象组件的架构师和设计师提出了更高的要求。
简言之,从实施的角度来看,SDP要求我们合理地引入一些抽象组件,从而保证系统的稳定性和灵活性,使得系统能够符合SDP,更根本的目标还是符合OCP,。
本文可能会有点太过理论,而且很多朋友可能平时很少接触软件度量方面的东东,不过看一看对研究和深入理解组件设计应该还是会有所帮助的,呵呵!
除了本文所提到的Bob大叔所引入的稳定性度量方法以外,不知道大家在日常工作中是否还有用一些其他衡量组件稳定性和易变性的方法?欢迎大家与我交流和讨论,。
【作者:刘伟 http://blog.csdn.net/lovelion】