新文章的骨架素材,请大家预览并提出宝贵建议(这是素材
,还没有串接成文)。
文章的标题拟为:Thinking in PPP: DIP, Adapter and TDD
首先的问题是,DIP是什么?
依赖反转原则:高层模块不应该依赖底层模块,两者都应该依赖抽象;而抽象不应该依赖细节,反之细节应该依赖于抽象。
引用针对包的DIP原则:SDP+SAP 也即朝着稳定和抽象的方向进行依赖。
如果X->Y,那么我们需要评估Y的稳定性以确定该依赖的稳定程度:
class X
{
?public void F()
?{
? ?Y y = new Y();
? ?y.G();
?}
}
class Y
{
?public void G() {}
}
因为依赖者的稳定性不可能超过被依赖者的稳定性,如果被依赖者不太稳定,则依赖他的其他类也就不可能太稳定,这不是我们希望得到的结构。
如果被依赖者的稳定程度非常之高,则这个依赖也是可以接受的;否则,有必要提取一个相对稳定的抽象以改善这个依赖。
通过运用重构的提取接口技术,从Y提取出I,并让Y实现I——此时,Y通过realize关系静态依赖于I,其稳定程度不会高于I;
class Y: I
{
?void I.G() {}
}
interface I
{
?void G();
}
接下来,改变X以使之从直接依赖于Y变为通过I间接的依赖Y:
class X
{
?public void F()
?{
? ?I i = new Y();
? ?i.G();
?}
}
为什么说X是间接的依赖Y?因为如果没有Y,X连编译都不能通过。
怎么办?通过使用运行时动态对象工厂来消除X对Y的静态依赖(这里图省事,我们直接使用.NET
Framework中的Activator来创建对象——实践中一般都是引入一个专用的组件工厂类):
class X
{
?public void F()
?{
? ?I i = (I)Activator.CreateInstance("Y, Demo");
? ?i.G();
?}
}
现在就算你把整个Y注释掉,程序的静态结构也是完整的——也就是说,我们已经消除了X和Y之间的静态依赖,降低了X和Y之间的耦合。
现在的结构是:X->I<-Y。被依赖的核心变成了I——再次评估此结构,我们发现X和Y的稳定性都不会高于I,因此如果I不稳定,一切都是徒劳。那么I到底稳定吗?要看I是如何而来。因为I是从原本不稳定的Y中提取的,如果抽象的合理,抽象出来的I确实代表了Y的稳定的本质,那么I就有一定的稳定性,那么该结构就相对合理了。可是如何能够保证I一定是合理的抽象呢?这就引出一个问题,I到底是属于谁的?
请问X->I<-Y到底是(X->I)<-Y,还是X->(I<-Y)呢?
事实上,对抽象的定义实际上是由对抽象的依赖者来确定的,该抽象的实现者只能去无条件的接受这个抽象,提供对这个抽象的实现——比较经典的说法也就是:接口是属于它的客户而不是具体实现的。
从包的层次讲,如果你需要分离X和Y到两个包中,那么你会把X和I放在一个包中呢,还是把Y和I放在一个包中?显然
,X应该是和I在一个包中,也只有这样才能保证即便没有Y的存在X也是可以编译通过而独立存在的。此时,Y的包需要通过引用X(实际上只是I)所在的包即可为之提供一个具体的实现。
问题又来了,如果Y并不在我的控制内,也就是说即便我抽象出了I,Y也不可能实现这个接口(比如说Y是一个第三方组件)——这时候ADAPTER就是你的救星了。通过编写一个适配器A,使之对X提供I的实现,并利用Y来提供这个实现,我们可以达到这样的结构:X->I<-A->Y。如果要分离包,原理和前面是一样的。
很多的ORM工具,可以帮你由数据库结构导出一个对应的对象结构,然后你的应用程序就去依赖这个对象结构进行开发了。问题是,本来数据库结构的稳定性就不好,由这个不稳定的东西"直接"映射出一个静态结构,其稳定程度不可能高于它的源,因此你去依赖这样一个同样不稳定的对象结构,除了编程上面可能要略为方便一些以外,从结构的角度看简直就是一塌糊涂——因为这违反了DIP原则。
从大的架构来看,一个企业级应用程序是依赖于特定的数据库的(这里的特定有两层含意:一是提供数据库功能的服务器;而是数据库本身的数据结构和功能)。而大量的开发经验告诉我们,数据库是非常不稳定的环节之一。如果整个应用程序都依赖这个不稳定的数据库,则整个应用程序的稳定性就好不到哪儿去。怎么办?通过引入稳定的抽象反转这种依赖关系。我们将应用程序对数据的所有需求都封装到一个稳定抽象的接口中,然后令数据库去提供这个抽象接口的一个具体实现。可是数据库是一个外部软件实体,我们不能让它实现我们提供的抽象接口,于是我们写了一系列的所谓数据访问组件,里应外合,对应用程序提供一个满足其所指定的抽象接口的具体实现,而这里的实现是委托给后面的特定数据库来完成的。结构:(App->DAI)<-DAC->DB。
再来看应用程序使用WS的例子:VS.NET可以帮你从WSDL生成一个PROXY(在应用架构中也即Service
Agent,SA)。众所周知,每当WSDL变化的时候(你说了,不是说服务接口发布之后就不许变了吗?!那当然是比较理想了,然而现实就是这么无奈啊!),你需要Refresh
Web Reference,那么VS.NET就会把这些变化重新反映到为你生成的SA中,而你的应用程序可是直接依赖于这个SA的!因此,你不得不去更改你的客户代码。
应用本文所述原理,应用程序应该首先确定为实现自身功能所需要的抽象,然后让VS.NET从WSDL中创建一个PROXY出来。但是这个PROXY并不能以应用程序所定义的抽象来提供具体实现,于是我们可以写一个Service
Agent,让他来适配两边不一致的接口。
OOAD就是通过分析一个可以工作的动态逻辑(dynamic
logic,也可以称之为一个算法)所需的超越本质逻辑的软件特性(如可维护性、可扩展性、灵活性等等)通过重构将其转化为一个可以达到逻辑等价的动态逻辑的静态对象结构。
无论结构变得有多复杂,其运行时的动态逻辑不应该发生超乎本质的改变;
通过重构而带来的结构复杂性一般而言都会在不改变本质动态逻辑的前提下增加开销,因此软件设计人员的主要能力也是去正确的评估增加的开销和带来的利益的关系;
DIP带来很多好处
首先,它帮助上层模块满足了OCP(开闭原则),这样上层逻辑就能够避免受到具体实现细节变动的侵扰而保持更高的稳定性。
DIP在软件组件之间引入稳定的抽象从而降低模块之间的耦合,大大提高了软件组件的可测试性;
考虑DIP使得需求抽象先于实现细节而确定,有助于实现并行开发,有效的增强了团队的协作程度,提到了开发效率。
TDD帮助开发满足DIP要求的软件
因为TDD强调从客户的角度考虑代码的编写,这和DIP中由分析客户代码需求而提取其所依赖的核心抽象是一致的。
留言反馈