《你必须知道的.NET》网站 | Anytao技术博客
发布日期:2008.12.02 作者:Anytao
© 2008 Anytao.com ,Anytao原创作品,转贴请注明作者和出处。
本文将介绍以下内容:
说在,开篇之前 |
在老子的“小国寡民”论中,提出了一种理想的社会状态:民至老死,不相往来。这是他老人家的一种社会理想,老死不相往来的人群呈现了一片和谐景象。因为不发生瓜葛,也就无所谓关联,进而无法倒置冲突。这是先祖哲学中的至纯哲理,但理想的大同总是和现实的生态有着或多或少的差距,人类社会无法避免联系的发生,所以小国寡民的理想成为一种美丽的梦想,不可实现。同样的道理,映射到软件“社会”中,也就是软件系统结构中,也预示着不同的层次、模块、类型之间也必然存在着或多或少的联系,这种联系不可避免但可管理。正如人类社会虽然无法实现小国寡民,但是理想的状态下我们推崇和谐社会,把人群的联系由复杂变为简单,由曲折变为统一,同样可以使得这种关联很和谐。所以,软件系统的使命也应该朝着和谐社会的目标前进,对于不同的关系处理,使用一套行之有效的哲学,把复杂问题简单化,把僵化问题柔性化,这种哲学或者说方法,在我看来就是:依赖的哲学,也就是本文所要阐释的中心思想。 *Hot:《你必须知道的.NET》 |
1 引言
因为在公司内部进行设计原则和设计模式的培训,我的第一个任务就是和大家就依赖倒置原则进行沟通。作为5大设计原则之一的DIP原则,单纯的由概念而实例在我认为并不能完全阐释清楚:
这几个关键的问题,所以我决定不单纯的通过DIP而DIP,而是从依赖这个最原始的概念讲起,来了解在面向对象软件设计体系中,关于“关系的处理”,也就是“依赖的哲学”。对,依赖就是关系,处理依赖也就意味着处理关系。因为,我们人类是最善于搞关系的动物,所以原本可以简单的理论,在人类的意识哲学中变得复杂而多变,以至于我们本应简单的道理变得如此复杂,这就是依赖。那么,从依赖讲起来了解依赖倒置原则,我觉得首先应该回到以下的问题:
带着对这些问题的思考和思索,Anytao带领大家就依赖这个话题开始一次循序渐进的面向对象之旅,以解答这些从一开始就有足够吸引力的问题,从原理到实例,从关系到异同,我期待这篇文章能带来一些认知的变革。
2 什么是依赖,什么是抽象
2.1 关于依赖和耦合:由小国寡民到和谐社会
在老子的“小国寡民”论中,提出了一种理想的社会状态:民至老死,不相往来。这是他老人家的一种社会理想,老死不相往来的人群呈现了一片和谐景象。因为不发生瓜葛,也就无所谓关联,进而无法倒置冲突。这是先祖哲学中的至纯哲理,但理想的大同总是和现实的生态有着或多或少的差距,人类社会无法避免联系的发生,所以小国寡民的理想成为一种美丽的梦想,不可实现。同样的道理,映射到软件“社会”中,也就是软件系统结构中,也预示着不同的层次、模块、类型之间也必然存在着或多或少的联系,这种联系不可避免但可管理。正如人类社会虽然无法实现小国寡民,但是理想的状态下我们推崇和谐社会,把人群的联系由复杂变为简单,由曲折变为统一,同样可以使得这种关联很和谐。所以,软件系统的使命也应该朝着和谐社会的目标前进,对于不同的关系处理,使用一套行之有效的哲学,把复杂问题简单化,把僵化问题柔性化,这种哲学或者说方法,在我看来就是:依赖的哲学,也就是本文所要阐释的中心思想。
因为,“耦合是不可避免的”,所以我们首先就从认识依赖和耦合的概念开始,来一步步阐释我们的依赖哲学思想:
依赖,就是关系,代表了软件实体之间的联系。软件的实体可能是模块,可能是层次,也可能是具体的类型,不同的实体直接发生依赖,也就意味着发生了耦合。所以,依赖和耦合在我看来是对一个问题的两种表达,依赖阐释了耦合本质,而耦合量化了依赖程度。因此,我们对于关系的描述方式,就可以从两个方面的观点来分析:
从依赖的角度而言,可以分类为:
从耦合的角度而言,可以分类为(此处回归到具体的代码级耦合概念,以方便概念的阐释):
不同的耦合,代表了依赖程度的差别,我们以“粒度”为概念来分析其耦合的程度。引用中间层来分离耦合,可以使设计更加的优雅,架构更加的柔性,但直接的依赖也存在其市场,过度的设计也并非可取之道。因为效率与性能同样是设计需要考量的因素,过多的不必要分离会增加调用的次数,造成效率浪费。在下文分析依赖倒置原则的弊端之一正是对此问题的进一步阐述。
那么,软件实体之间的耦合是如何产生呢?回归我们每天挥洒的代码片段,其实我们在重复的创造着耦合,并且得益于对这种耦合带来的数据通信。如果我们将历史的目光回归到软件设计之初,人类以简单的机器语言来实现最简单的逻辑,给一个输入,实现一个输出,可以表达为:
随着软件世界的革命,业务逻辑的复杂,以上的简单化处理已经不足以实现更复杂的软件产品,在系统内部的复杂度成为一个超越人脑可识别的程度时,例如:
因此,人类开始发挥重组和简单化处理的优势,我们不得不在软件设计上做出平衡。平衡的结果就是通过对复杂的系统模块化,把复杂问题简单处理,从而达到能够被人脑识别的目的。基于这种指导原则,随着复杂度的增加模块的划分更加朝着精细化发展,尤其是面向对象程序设计理论的出现,使得对复杂的处理实现了更科学的理论基础。然而,复杂的问题可以通过划分实现简单的功能模块或者技术单元,但由此应运而生的子单元会越来越多,而且越来越多的子单元必须发生数据的通信才能完成统一的业务处理,所以产生的数据通信管理也越来越多。对于子单元的管理,也就是我们本文关注的核心概念-依赖,成为新的软件设计问题,那么总结前人的经验,提炼今人的智慧,我们对耦合的产生做以如下归纳:
了解了耦合发生的一般方式,我们就可以进入了核心思想的讨论,那就是在认识依赖和了解依赖的基础上,我们最终追求的目标。
讨论了半天,终于是时候对依赖和耦合进行一点儿总结了,也是该进行一点目标诉求了。在软件设计领域,有那么几个至高原则值得我们深刻心中,它们是:
对了,就是这些平凡的字眼,汇集了面向对象思想的核心内容,也是本文力求阐释的禅意心经。关于面向抽象编程和封装变化,我们会在后面详细阐释,在此我们需要将注意力关注于“低耦合,高内聚”这一目标。
低耦合,代表了实现最简单的依赖关系,尽可能的减少类与类、模块与模块、层次与层次、系统与系统之间的联系。低耦合,体现了人类追求简单操作的理想状态,按照软件开发的基本实现技巧来追求软件实体之间的关系简单化,正是大部分设计模式力图追求的目标;低耦合,降低了一个类或一个模块发生修改对其他类或模块造成的影响,将影响范围简单化。在我们阐释的依赖关系方式中,实现单向的依赖,实现抽象的耦合,都是实现低耦合的基础条件。
高内聚,一方面代表了职责的统一管理,一方面体现了关系的有效隔离。例如单一职责原则其实归根结底是对功能性的一种指导性体现,将功能紧密联系的职责封装为一个类(或模块),而判断的准则正是基于引起类变化的原因。所以,封装离不开依赖,而抽象离不开变化,二者的概念和本质都是相对而言的。因此,高内聚的目标体现了以隔离为目标进行统一管理的思想。
那么,为了达到低耦合、高内聚的目标,通常意义上的设计原则和设计模式其实都是朝着这个方向实现的,因此我们仅仅小结并非普遍意义的规则:
2.2 关于抽象和具体
什么是抽象呢?我们首先不必澄清什么是抽象,而从什么算抽象说起,稳定的、高层的则代表了抽象。就像一个公司,最好保证了高层的稳定,才能保证全局的发展。在进行系统设计时,稳定的抽象接口和高层逻辑,也代表了整个系统的稳定与柔性。兵熊熊一窝,将良良一窝,系统的逻辑也正如着代表打仗,良好的设计都是自上而下的。而对具体的编程实践而言,接口和抽象类则代表了语言层次的抽象。
追溯概念的分析,我们一一过招,首先来看依赖于具体:
因此,为了分离这种紧耦合,最好的办法就是隔离,引入中间层来分离变化,同时确保中间层本身的稳定性,因此抽象的中间层是最佳的选择。
例如:
public interface IUserService
{
}
public class UserService : IUserService
{
}
下面依赖于具体:
public class UserManager
{
private UserService service = null;
}
下面依赖于抽象:
public class UserManager
{
private IUserService service = null;
}
二者的区别仅在于引入了接口IUserService,从而使得UserManager对于UserService的依赖由强减弱。这种方式也在我们的Ezsocio项目中进行service层的设计方式。然而对于依赖的方式并非仅此一种,设计模式中的智慧正是通过各章编程技巧进行依赖关系的设计,值得我们关注和学习,本文也在下文进行相关设计模式的讨论。
对WCF熟悉的读者一定不难看出这种实现方式如此类似于WCF的推荐模式,这是契约编程的基本思想。关于WCF及SOA的相关内容,本文将在后文进行相关的讨论。
总结一番,什么是抽象,什么是具体?在我看来,抽象就是系统中对变化封装的战略逻辑,体现了系统的必然性和稳定性,能够被具体层次复用和覆写;而具体则包含了与具体实现相关的逻辑,体现了系统的动态性和变动性。因此,抽象是稳定的,而具体是变动的。
Bob大叔在《敏捷》一书直言,程序中所有的依赖关系都应终止于抽象类或者接口,就是对面向抽象编程一针见血的回应,其原因归根结底源自于我们对抽象和具体的认知和分解:关联应该终止于抽象,而不是具体,保证了系统依赖关系的稳定。具体类发生的修改,不会影响其他模块或者关系。那么如何做到这种理想的依赖于抽象的设计呢?
将复杂的问题简单化,是人类思维的一般智慧,也自然而然是实现软件设计的基本思路。而将复杂的业务需求通过建模过程的抽象化提炼,去粗取精,去伪存真,凡此种种。而抽象的过程,其目标之一就是形成对于复杂问题简单化的处理过程,只有形成层次简单的逻辑才能将复杂需求中的关系梳理清晰,而依赖的本质正如上文所言,不就是处理关系吗?
所以,清晰的层次划分,进而形成的模块化,是实现系统抽象的必经之路。
由需求而设计的过程,就是一个分散集中化的过程,把需求相关的业务通过开发流程的需求分析过程进行整理,逐步形成需求规格说明、概要设计和详细设计等基本流程。分散集中化,是一个梳理需求到形成设计的过程,因此对于把握系统中的抽象和具体而言,是一个重要的分析过程和手段。现代软件工程已经对此形成了科学的标准化流程处理逻辑,例如可以借助UML更加清晰的设计流程、分析设计要素,进行标准化沟通和交流。
将具体问题抽象化,是本节关注的要点,而处理的方法是什么呢?答案就在设计模式,设计模式是前辈智慧的总结和实践,所以熟悉和学习设计模式,是学习和实践设计问题的必经之路。然而,没有哪个问题是由设计模式全权解决,也没有那个模式能够适应所有的问题,因此我们要努力的是尽量积累更多的模式来应对多变的需求。作为软件设计话题中最重量级的话题,我也会在以后的岁月中对设计模式问题进行一些探讨。
总的来说,抽象和变化就像一对孪生兄弟,将具体的变化点隔离出来以抽象的方式进行封装,在变化的地方寻找抽象是面对抽象最理想的方式。所以,如何去寻找变化是设计要解决的首页问题,例如工厂模式的目标是封装对象创建的变化,桥接模式封装的是对象间的依赖关系变化等等。23个经典的设计模式,从某种角度来看,正是对不同变化点的封装角度提出的不同解决方案。
这一设计原则中我们还将之称为SoC(Separation of Concerns)原则,定义了对于实现理想的高耦合、低内聚目标的统一规则。
2.3 设计的哲学
之所以花如此篇幅来讲述一个看似简单的问题,其实最终理想是回归到软件设计目标这个命题上。如果悉心钻研就可发现,设计的最后就是对关系的处理,正如同生活的意义在于对社会的适应一样。因此,回归到设计的目标上我们就可知,完美的设计过程就是对关系的处理过程,也就是对依赖的梳理过程,并最终形成一种合理的耦合结果。
所以,面向对象并不神秘,我们以生活的现实眼光来看更是如此。把面向对象深度浓缩起来,我觉得可以概括为:
其实,就是这么简单。在这种意义上来说,面向对象思想是现代软件架构设计的基础。下面我们以三层架构的设计为例,来进一步感受这种依赖哲学的具体应用。关于依赖的抽象和对变化隔离的基本思路,其实也是实现我们典型三层架构(或者)多层架构的重要基础。只要使各个层次之间依赖于较稳定的接口,才能使得各个层次之间的变化被隔离在本层之内,不会造成对其他层次的影响,这完全符合开放封闭原则追求的优良设计理念。将这种思路表达为设计,可以表示为:
在此,IDataProvider作为隔离业务层和数据层的抽象,IService作为隔离业务层和表现层的抽象,保证了各个层次的相对稳定和封装。而体现在此的设计逻辑,就正是我们对于抽象和耦合基本目标概念的体现,例如作为重用的单元,抽象隔离保证了对外发布接口的单一和稳定,所以达到了最高限度的重用;通过引入中间的稳定的接口,达到了不同层次的有效隔离,层与层之间体现为轻度耦合,业务层只持有IDataProvider就可以获取数据层的所有服务,而表现层也同样如此;最后,这种方式显然也直接实践了面向接口编程,面向抽象编程的经典理念。
同样的道理,对于架构设计的很多概念,放大可以扩展为面向服务设计所借鉴,放小这正是我们反复降调的依赖倒置原则在类设计中的基本思想。因此,牢记对我影响至深的一位大牛的说法:软件设计的任何问题,都可以通过引入中间逻辑了解决。而这个中间逻辑,很多时候被封装为抽象,是最为合理和智慧的解决方案。
让我们再次高颂《老子》的小国寡民论,来回味关于依赖哲学中,我们如何实现更好的和谐统一,如何遵守科学的软件管理思想:"邻国相望,鸡犬之声相闻,民至老死,不相往来。"
3 认识依赖倒置原则(DIP)
3.1 什么是依赖倒置?
Bob大叔在《Agile Principles, Patterns, and Practices》一书中对依赖倒置原则进行了精辟的总结为:
我规规矩矩一字不差的把上述真言放在心里,却发现大师的牛论实在有点故作玄虚,就像欣赏Bob在论述DIP时的插画一样费解不讨好:
其实著名的好莱坞原则更形象的阐述了这一思想:你不要调我,我来调你。不管是通俗的还是高尚的,却都不约而同的揭示了依赖倒置原则的最核心思想就是:
依赖于抽象,对接口编程,对抽象编程!
相较而言,从实际的生活中来看依赖倒置,就像下面这个示例揭示的一样。
3.2 从实例开始
综合对依赖倒置的认识,结合到具体的程序实现而言,依赖倒置预示着程序中的依赖关系不应是具体的类型,而是归咎于抽象类和接口。下面我们通过一个简单的实例来分析符合依赖倒置和违反依赖倒置,对于系统设计的影响和区别。我们的需求是为某个遥控器生产商,实现一个万能遥控器,该遥控器可以对当前市场上的很多电子设备进行“打开”和“关闭”的操作,例如你可以使用Anytao牌遥控器打开海尔电视、创维电视等等,当然更理想的状态是可以打开电冰箱、电灯还有门窗等等,总之凡是可以互联的设备都是未来万能遥控器的新需求。
那么该遥控器厂商在设计之初,该如何去考虑实现一个可以打开任何设备的遥控器呢?这一重责首先落在了一位年轻气盛的小王设计师身上,因为遥控器厂家当前的直接客户只有海尔电视一家,所以他轻松的实现了下面的设计,并且兴高采烈的进行了大批量生产:
随后,厂商多了一个重量级客户长虹,所以小王不得不对初试设计进行了改造,勉强适应了新的需求,如下:
虽然小王应付了这次需求变动,但是原本的设计显然已经捉襟见肘。正当小王绞尽脑汁进行改造的同时,新的需求接踵而来:新飞冰箱、飞利浦照明、盼盼防盗门,一个接一个。小王的最终设计变成了这般摸样:
哎,真是太累了。每一次的需求变更都伴随着小王对遥控器Remote的再次摧残,Remote内部不断增加新的引用和操作处理,显然一个if/else式的判断布满了整个Open和Close的操作中,这种设计显然无法满足OCP对扩展开放、对修改封闭的要求。显然,如果想让卖出去的遥控器也适应新的需求,在小王当前的设计实现方案中是根本无法实现的,遥控器厂商总不能召回已经售出所有的遥控器,再拆开进行重新改造吧。
一筹莫展的小王,终于在崩溃之际想起了经验丰富的前设计师老王,并立即请教如何解决当前问题的思路。而老王也毫不含糊,给出了一个初步的实现:
在当前的设计中,老王的思路是让遥控器厂切断和各个厂家的直接联系,而是寻找所有电视厂商的领导(例如,电视机协会),请电视机协会制定所有电视机厂商必须遵守的打开和关闭等操作的契约,遥控器厂和电视机协会建立直接的联系而不是各个具体的电视厂商,于是便有了上述设计思路。而新的需求来临时,因为各个厂商必须遵守TurnOn和TurnOff的契约,所以轻松的万能遥控器可以应付所有的电视机品牌,实现的具体操作已经由遥控器转移到具体的厂商手上(顺便说说这也是所有权的倒置体现),轻松的小王终于大呼一口气。并且再接再厉修改了更完善的版本:
现在,遥控器基本实现了万能的要求,任何新的需求或者修改都可以轻松胜任。小王终于解决了原本设计的所有问题,带着感激盛情邀请老王吃饭致谢。席间就坐,小王请教老王二次设计的秘诀,老王神秘一笑沾酒在桌子上写了几个大字:依赖倒置。经历此次设计重构洗礼的小王,也在实战中体味了设计的精妙,看着依赖倒置几个字小王也会心的笑了。
万能遥控器的故事,是一个系统实现中经常的事儿。而这些设计在Ezsocio项目中有广泛的应用,例如对于DataProvider和Service的处理方式,正是一种典型的遵循DIP原则的设计思路。
3.3 为什么依赖倒置?
依赖倒置原则揭示了面向对象思想中一个最基本而最核心的话题,那就是:面向抽象编程。任何对依赖倒置原则的违反都不同程度的偏离了面向对象设计思想的轨道,所以如果你想自己的程序是否足够的OO,透彻的了解依赖倒置是必不可少的。
所以,要问答为什么依赖倒置这个话题,我觉得可以从以下几个方面来阐释:
综上而言,依赖倒置是对软件实体关系处理的基本思想原则,也是其他设计原则与设计模式的基础之一,因此遵守依赖倒置是实现OO的基本原则,是我们必须了解的基础性原则。下面,我们对此进行详细的说明和举例。
3.4 为什么是倒置?
鲁迅先生有云:其实地上本没有路,走的人多了也便成了路。对依赖倒置原则中的“倒置”二字而言,其实也类似于一条被很多人走过的路,因为习惯性的称呼走过的为“路”,所以只好把违反习惯的东西称为“倒置的路”。这倒置的含义,正基于此。
对于从结构化编程走过的人来说,基于软件复用的考虑,侧重于对具体模块的复用,因为也就习惯了从高层模块出发了构建系统流程的思维模式,所以那时的高手一出手就实现了高层依赖于底层的典型套路,例如:
高层模块通过自上而下的实现来完成系统功能的调用,将这种方式表达为代码就是:
// Release : code01, 2008/11/02
// Author : Anytao, http://www.anytao.com
public static void Main()
{
try
{
//Do something here.
}
catch
{
Log(true, "XMLLog");
}
}
public static void Log(bool isRead, string logType)
{
if (isRead)
ReadLog(logType);
else
WriteLog(logType);
}
然而,当软件设计的模式发展到面向对象阶段时,我们发现原来习惯的世界了已经变了。基于高层依赖于底层的弊政,也越来越被可扩展性的系统需求折磨的面目全非,例如如果日志记录的载体发生变化,当前设计中需要同时自上而下的修改实现的逻辑,同时避免出现越来越多的if/else结构。所以当新的依赖关系从传统的方式被完全扭转时,“倒置”二字就此诞生了。我们修改Log实现的设计思路,将可能变化的逻辑封装为抽象接口,使得高层依赖发生转换:
程序实现的逻辑早已被面向对象的设计思想所取代,我们新的实现变成了:
// Release : code02, 2008/11/02
// Author : Anytao, http://www.anytao.com
public class Client
{
public static void Main()
{
ILog myLogger = new XMLLog();
try
{
}
catch
{
myLogger.Write();
}
}
}
public interface ILog
{
void Read();
void Write();
}
public class XMLLog : ILog
{
public void Read()
{
}
public void Write()
{
}
}
所以,了解了历史才能正视现实,对于软件设计同样如此,只有认清楚依赖倒置产生的历史背景,我们才能更加熟练的驾驭倒置含义本身带来的误解,而将中心思想牢牢的把握在依赖倒置最核心的设计思想上,那还是万变不离其宗的:依赖于抽象,这简单的5个字上。
对于所属权关系的依赖问题上,我们看到,只有倒置的才是面向对象的,没有倒置的还是面向结构的。如果你的系统中存在着不合理的依赖关系,那么依赖倒置将是检查系统设计最好的标尺,这也是我们把握这一原则的实际意义之一。
3.5 如何依赖倒置?
如何依赖倒置的关键,还是体现在如何对抽象和具体的封装和分离,实践的基本思路就是封装变化。这正如我们在单一职责原则中反复强调,对一个类只有一个引起它变化的原因。我们实践依赖倒置,仍然可以从关注变化开始,详细的分析和预测系统中的变化点,然后针对每个可能的变化抽象出相对稳定的约束,这是我们实践依赖倒置原则最基本的方法步骤。
就原理而言,依赖倒置要求我们的设计:
就实践而言,经典的软件设计实践为我们提出了很多值得借鉴的思路,例如每个设计模式就是对一种特定情况的实践总结,在此我们继续列出一些经典的大师忠言,Bob大叔在《Agile Principles, Patterns, and Practices》一书对此进行了3点总结:
实际上,在实际的设计过程中要完全遵守这几点要求是有难度的,所以如何既能很好的遵守设计原则,又能很好的适应代码情况,是值得权衡的问题,需要我们不断的积累和实践。另外,还有几个经验只谈:
如何依赖倒置,我们阐释了一点原则还有一点方法,算是对实现依赖倒置的一点小结。然而,在实际的开发过程中,并没有一成不变的规则,当前的面向对象语言本身就提供了对抽象和封装的支持,为实现面向对象设计提供了基础机制。回顾软件开发的历史,我们不难看出依赖和封装哲学的发展轨迹,在结构化编程中函数是封装的基本单元;随着面向对象的发展C++/C#高级语言以类为基本单元,第一次将数据和行为有机的组合为一个逻辑单元,于是有了对于不同类之间的关系处理哲学;而SOA中封装的单元上升为service,是一种更高意义的逻辑封装,实现了更优良的逻辑封装和松散耦合关系。同样的道理,也体现在三层架构的分割和通信中,体现在ORM对表现层和领域层的分离中。
因此,依赖倒置是一种高度的智慧和经验总结,如何实现依赖倒置也是一种积累和不断的学习。
3.6 也有弊端
然而,一味的遵守原则,就等于没有原则。重要的是,我们需要把握其平衡,在进行开发中适当的把握其程度。Bob在《敏捷》中也提到这个问题,他总结了依赖倒置的两个弊端,同样需要我们必要的关注:
所以,学习模式或者原则必须把握灵活处理,不能一味强行。
下集预告 |
在下篇中,我们将继续对依赖相关的问题进行讨论,基本的内容还包括:
近期发布,敬请期待。 *Hot:《你必须知道的.NET》 |
参考文献
系列导航
[目录导航]
[第一回:设计,应该多一点]
[第二回:对象的旅行---对象和人,两个世界,一样情怀]
[第三回:设计的分寸]
anytao | © 2008 Anytao.com
2008/12/02 | http://anytao.cnblogs.com/
本文以“现状”提供且没有任何担保,同时也没有授予任何权利。 | This posting is provided "AS IS" with no warranties, and confers no rights.
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。