文章图片标题

依赖倒转原则(DIP)

分类:架构设计 作者:阳光倾城 评论:0 点击: 429 次 日期:2016-08-24

实现“开-闭”原则的关键是抽象化,并且从抽象化导出具体化实现。如果说“开-闭”原则是面向对象设计的目标的话,依赖倒转原则就是这个面向对象设计的主要机制。

要依赖于抽象,不依赖于具体。

为什么要使用“倒转(Inversion)”一词,依赖倒转(Dependence Inversion)的意义是什么?

传统的过程性系统的设计方法倾向于使高层次的模块依赖于低层次的模块;抽象层次依赖于具体层次。倒转原则是要把这个错误的依赖关系倒转过来,这就是“依赖倒转原则”的来由。

抽象层依赖于具体层次的含义是什么呢?抽象层次包含的是应用系统的商务逻辑和宏观的,对整个系统来说重要的战略性决定,是必然性的体现;而具体层次则含有一些次要的与实现有关的算法和逻辑,以及战术性的决定,带有相当大的偶然性选择。具体层次的代码是会经常变动的,不能避免出现错误。抽象层次依赖于具体层次,使许多具体层次的细节的算法变化立即影响到抽象层次的宏观的商务逻辑,导致微观决定宏观,战术决定战略,偶然决定必然。

image

抽象层次含有宏观的和重要的商务逻辑,难道不应当由它来决定具体层次的实现和具体算法的改变吗?抽象层次含有战略的决策,难道不应当由它来决定具体层次的战术的决策吗?抽象层次含有必然性的选择,难道不应当由它来指导具体层次的偶然性选择吗?

抽象层次依赖于具体层次显然是不对的。依赖倒转原则(Dependence Inversion Principle 或简称为DIP),就是要把错误的依赖关系再倒转过来。

image

 

依赖倒转原则是COM、CORBA、JavaBean以及EJB等构建设计模型背后的基本原则。

复用与可维护性的“倒转”

从复用的角度来看,高层次的模块是设计者应当复用的。但是在传统的过程性的设计中,复用却侧重于具体层次模块的复用,比如算法的复用、数据结构的复用、函数库的复用等,都不可避免是具体层次模块里的复用。较高层次的结构依赖于较低层次的结构,较低层次的结构又进一步依赖于更低层次的结构,如此继续,直到依赖于每一行的代码。较低层次上的修改,会造成较高层次的修改,直到高层次逻辑的修改。

同样,传统的做法也强调具体层次上的可维护性,包括一个函数、数据结构等的可维护性,而不是高层次上的可维护性。

从复用的意义上讲,既然抽象层次含有一个应用系统最重要的宏观商务逻辑,是做战略性判断和决定的地方,那么抽象层次就应当使较为稳定的,应当是复用的重点。由于现有的复用侧重于具体模块和细节的复用,因此,“倒转”一词则是指复用应当将复用的重点放在抽象层次上。如果抽象层次的模块相对独立于具体层次的模块的话,那么抽象层次的模块的复用便是相对较为容易的了。

同样,最重要的宏观商务逻辑也应当使维护的重点,而不是相反。

因此,遵守依赖倒转原则会带来复用和可维护性的“倒转”。

三种耦合关系

在面向对象的系统里,两个类之间可以发生三种不同的耦合关系:

零耦合(Nil Coupling)关系:如果两个类没有耦合关系,就称之为零耦合。

具体耦合(Concrete Coupling)关系:具体性耦合发生在两个具体的(可实例化的)类之间,经由一个类对另一个具体类的直接引用造成。
抽象耦合(Abstract Coupling)关系:抽象耦合关系发生在一个具体类和一个抽象类(或者Java接口)之间,使两个必须发生关系的类之间存有最大的灵活性。

 

依赖倒转原则(Dependence Inversion Principle)要求客户端依赖于抽象耦合。依赖倒转原则的表述是:

抽象不应当依赖于细节:细节应当依赖于抽象。(Abstractions should not depend upon details。Details should depend upon abstractions)

依赖倒转原则的另一种表述是:

要针对接口编程,不要针对实现编程。(Program to an interface,not an implementation)

针对接口编程的意思是说:应当使用Java接口和抽象Java类进行变量的类型声明、参量的类型声明、方法的返还类型声明,以及数据类型的转换等。

一个具体Java类应当只实现Java接口和抽象Java类中声明过的方法,而不应当给出多余的方法。

依赖倒转关系强调一个系统内的实体之间关系的灵活性。

变量的静态类型和真实类型

变量被声明时的类型叫做变量的静态类型(Static Type),有些作者把静态类型叫做明显类型(Apparent Type),变量所引用的对象的真实类型叫做变量的实际类型(ActualType)。

List employees = new Vector();

引用对象的抽象类型

很多情况下,一个Java程序需要引用一个对象。这个时候,如果这个对象有一个抽象类型的话,应当使用这个抽象类型作为变量的静态类型。这就是针对接口编程的含义。

Vector employees = new Vector();

而应当使用下面的声明语句:

List employees = new Vector();

这两者的区别就是前者使用一个具体类作为变量的类型,而后者使用一个抽象类型(List是一个Java接口)作为类型。这样做的好处,就是在决定将Vector类型转换成ArrayList时,需要改动得很少:

List employees = new ArrayList();

这样程序具有更好的灵活性,因为除去调用构造的一行语句外,程序的其余部分根本察觉不到有什么变化(假设程序不需要多线程的同步化)。

只要一个被引用的对象存在抽象类型,就应当在任何引用此对象的地方使用抽象类型,包括参量的类型声明、方法返回类型的声明、属性变量的类型声明等。

对象的创建

对象创建时Java语言需要使用new 关键字以及这个类的本身。对象创建出来后,就可以灵活地使用这个对象的抽象类型来引用它。因此,Java语言中创建一个对象的过程是违背“开-闭”原则以及依赖倒转原则的。

为了解决对象创建过程中的依赖倒转问题,设计模式给出了多个创建模式,特别是几个工厂模式。

 

怎样做到依赖倒转原则

以抽象方式耦合是依赖倒转原则的关键。由于一个抽象耦合关系总要涉及具体类从抽象类继承,并且需要保证在任何引用到基类的地方都可以改换成其子类,这样,里氏代换原则是依赖倒转原则的基础。

在抽象层次上的耦合虽然有灵活性,但也带来了额外的复杂性。在某些情况下,如果一个具体类发生变化的可能性非常小,那么抽象耦合能发挥的好处便十分有限,这时使用具体耦合反而会更好。

依赖倒转原则是OO设计的核心原则,设计模式的研究和应用是以依赖倒转原则为指导原则的。

 

工厂方法模式

应当使消费一个对象的客户端只依赖于对象的抽象类型,而不是它的具体类型。但是,Java语言要求在将一个(具体)类实例化的时候,必须调用这个具体类的构造子,所以Java语言给出的类的实例化方法无法做到只依赖于抽象类型。

设计模式给出了解决这个问题的可行方案,其中最重要的方案是工厂模式。工厂方法模式是几个工厂模式中最为典型的一个,下图是工厂方法模式的简略类图

image

工厂模式将创建一个类的实例的过程封装起来,消费这个实例的客户端仅仅得到实例化的结果,以及这个实例的抽象类型。任何方法都无法回避Java所要求的new关键字和直接调用具体类的构造子的做法。简单工厂模式将这个违反“开-闭”原则以及依赖倒转原则的做法封装到了一个类里面,而工厂方法模式将这个违反原则的做法推迟到了具体工厂角色中,如下图

image

这样通过适当的封装,工厂模式可以净化大部分的结构,而将违反原则的做法孤立到易于控制的地方。

 

模版方法模式

是依赖倒转原则的具体实现,模版方法模式的简略类图如右图所示。

image

在模版方法模式里面,有一个抽象类将重要的宏观逻辑以具体方法以及具体构造子形式实现,然后声明一些抽象来迫使子类实现剩余的具体细节上的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。模版方法模式支持依赖倒转原则,如下图所示。

image

具体子类不能影响抽象类的宏观逻辑,而抽象逻辑的改变则会导致细节逻辑的改变。

迭代子模式

迭代子模式用一个工厂方法向客户端提供一个聚集的内部迭代功能,客户端得到的是一个Iterator抽象类型,并不知道迭代子的具体实现以及聚集对象的内部结构。迭代子模式的简略类图如下所示。

image

聚集的内部结构的改变就不会波及到客户端,从而实现了对抽象接口的依赖,如下图

image

Java对抽象类型的支持

在Java语言中,可以定义一种抽象类型,并且提供这一抽象类型的各种具体实现。

实际上,Java语言提供了两种而不是一种机制做到这一点。他们就是Java接口和Java抽象类。




声明: 除非注明,本文属( 阳光倾城 )原创,转载请保留链接: http://www.tomrrow.com/archives-8145.html