`
skambc
  • 浏览: 45029 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

java

阅读更多
转载自:http://www.cnblogs.com/cbf4life/archive/2009/12/15/1624435.html
3.1 依赖倒置原则的定义

     依赖倒置原则(Dependence Inversion Principle,简称DIP)这个名字看着有点别扭,“依赖”还“倒置”,这到底是什么意思?依赖倒置原则的原始定义是:High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions。翻译过来,包含三层含义:

高层模块不应该依赖低层模块,两者都应该依赖其抽象;
抽象不应该依赖细节;
细节应该依赖抽象。
     高层模块和低层模块容易理解,每一个逻辑的实现都是由原子逻辑组成的,不可分割的原子逻辑就是低层模块,原子逻辑的再组装就是高层模块。那什么是抽象,什么又是细节呢?在Java语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点就是可以直接被实例化,也就是可以加上一个关键字new产生一个对象。依赖倒置原则在Java语言中的表现就是:

模块间的依赖是通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;
接口或抽象类不依赖于实现类;
实现类依赖接口或抽象类。
     更加精简的定义就是“面向接口编程”——OOD(Object-Oriented Design,面向对象设计)的精髓之一。

3.2 言而无信,你太需要契约

     采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,减少并行开发引起的风险,提高代码的可读性和可维护性。

     证明一个定理是否正确,有两种常用的方法:一种是根据提出的论题,经过一番论证,推出和定理相同的结论,这是顺推证法;还有一种是首先假设提出的命题是伪命题,然后推导出一个荒谬、与已知条件互斥的结论,这是反证法。我们今天就用反证法来证明依赖倒置原则是多么的优秀和伟大!

     论题:依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,减少并行开发引起的风险,提高代码的可读性和维护性。

     反论题:不使用依赖倒置原则也可以减少类间的耦合性,提高系统的稳定性,减少并行开发引起的风险,提高代码的可读性和维护性。

     我们通过一个例子来说明反论题是不成立的。现在的汽车越来越便宜了,也就顶多一个卫生间的价格就可以买到一辆不错的汽车,有汽车就必然有人来驾驶了,司机驾驶奔驰车的类图如图3-1所示。



图3-1 司机驾驶奔驰车类图

     奔驰车可以提供一个方法run,代表车辆运行,实现过程如代码清代3-1所示。

代码清单3-1 司机源代码


public class Driver {
 
//司机的主要职责就是驾驶汽车
 
public void drive(Benz benz){
 
benz.run();
 
}
 
}
     司机通过调用奔驰车的run方法开动奔驰车,其源代码如代码清单3-2所示。

代码清单3-2 奔驰车源代码

public class Benz {
 
//汽车肯定会跑
 
public void run(){
 
System.out.println("奔驰汽车开始运行...");
 
}
 
}
     有车,有司机,在Client场景类产生相应的对象,其源代码如代码清代3-3所示。

代码清单3-3 场景类源代码

public class Client {
 
public static void main(String[] args) {
 
Driver zhangSan = new Driver();
 
Benz benz = new Benz();
 
//张三开奔驰车
 
zhangSan.drive(benz);
 
}
 
}
     通过以上的代码,完成了司机开动奔驰车的场景,到目前为止,这个司机开奔驰车的项目没有任何问题。我们常说“危难时刻见真情”,我们把这句话移植到技术上就成了“变更才显真功夫”,业务需求变更永无休止,技术前进就永无止境,在发生变更时才能发觉我们的设计或程序是否是松耦合。我们在一段貌似磐石的程序上加上一块小石头:张三司机不仅要开奔驰车,还要开宝马车,又该怎么实现呢?麻烦出来了,那好,我们走一步是一步,我们先把宝马车产生出来,实现过程如代码清单3-4所示。

代码清单3-4 宝马车源代码

public class BMW {
 
//宝马车当然也可以开动了
 
public void run(){
 
System.out.println("宝马汽车开始运行...");
 
}
 
}
     宝马车也产生了,但是我们却没有办法让张三开动起来,为什么?张三没有开动宝马车的方法呀!一个拿有C照的司机竟然只能开奔驰车而不能开宝马车,这也太不合理了!在现实世界都不允许存在这种情况,何况程序还是对现实世界的抽象,我们的设计出现了问题:司机类和奔驰车类之间是一个紧耦合的关系,其导致的结果就是系统的可维护性大大降低,可读性降低,两个相似的类需要阅读两个文件,你乐意吗?还有稳定性,什么是稳定性?固化的、健壮的才是稳定的,这里只是增加了一个车类就需要修改司机类,这不是稳定性,这是易变性。被依赖者的变更竟然让依赖者来承担修改的成本,这样的依赖关系谁肯承担!证明到这里,我们已经知道伪命题已经部分不成立了。

     注意 设计是否具备稳定性,只要适当的“松松土”,观察“设计的蓝图”是否还可以茁壮的成长就可以得出结论,稳定性较高的设计,在周围环境频繁变化的时候,依然可以做到“我自岿然不动”。

     我们继续证明,“减少并行开发引起的风险”,什么是并行开发的风险?并行开发最大的风险就是风险扩散,本来只是一段程序的错误或异常,逐步波及一个功能,一个模块,甚至到最后毁坏了整个项目,为什么并行开发就有这个风险呢?一个团队,20人开发,各人负责不同的功能模块,甲负责汽车类的建造,乙负责司机类的建造,在甲没有完成的情况下,乙是不能完全地编写代码的,缺少汽车类,编译器根本就不会让你通过!在缺少Benz类的情况下,Driver类能编译吗?更不要说是单元测试了!在这种不使用依赖倒置原则的环境中,所有的开发工作都是“单线程”的,甲做完,乙再做,然后是丙继续…,这在90年代“个人英雄主义”编程模式中还是比较适用的,一个人完成所有的代码工作,但在现在的大中型项目中已经是完全不能胜任了,一个项目是一个团队的协作结果,一个“英雄”再牛X也不可能了解所有的业务和所有的技术,要协作就要并行开发,要并行开发就要解决模块之间的项目依赖关系,那然后呢?依赖倒置原则就隆重出场了!

     根据以上证明,如果不使用依赖倒置原则就会加重类间的耦合性,降低系统的稳定性,增加并行开发引起的风险,降低代码的可读性和维护性。承接上面的例子,引入依赖倒置原则后的类图如图3-2所示。



图3-2 引入依赖倒置原则后的类图

     建立两个接口:IDriver和ICar,分别定义了司机和汽车的各个职能,司机就是驾驶汽车,必须实现drive()方法,其实现过程如代码清单3-5所示。

代码清单3-5 司机接口

public interface IDriver {
 
//是司机就应该会驾驶汽车
 
public void drive(ICar car);
 
}
     接口只是一个抽象化的概念,是对一类事物的最抽象描述,具体的实现代码由相应的实现类来完成,Driver实现类如代码清单3-6所示。

代码清单3-6 司机类的实现

public class Driver implements IDriver{
 
//司机的主要职责就是驾驶汽车
 
public void drive(ICar car){
 
car.run();
 
}
 
}
     在IDriver中,通过传入ICar接口实现了抽象之间的依赖关系,Driver实现类也传入了ICar接口,至于到底是哪个型号的Car需要在高层模块中声明。

     ICar及其两个实现类的实现过程如代码清单3-7所示。

代码清单3-7 汽车接口及两个实现类

public interface ICar {
 
//是汽车就应该能跑
 
public void run();
 
}
 
public class Benz implements ICar{
 
//汽车肯定会跑
 
public void run(){
 
System.out.println("奔驰汽车开始运行...");
 
}
 
}
 
public class BMW implements ICar{
 
//宝马车当然也可以开动了
 
public void run(){
 
System.out.println("宝马汽车开始运行...");
 
}
 
}
     在业务场景中,我们贯彻“抽象不应该依赖细节”,也就是我们认为抽象(ICar接口)不依赖BMW和Benz两个实现类(细节),因此我们在高层次的模块中应用都是抽象,Client的实现过程如代码清单3-8所示。

代码清单3-8 业务场景

public class Client {

public static void main(String[] args) {

IDriver zhangSan = new Driver();

ICar benz = new Benz();

//张三开奔驰车

zhangSan.drive(benz);

}

}

     Client属于高层业务逻辑,它对低层模块的依赖都建立在抽象上,zhangSan的显示类型是IDriver,benz的显示类型是ICar,也许你要问,在这个高层模块中也调用到了低层模块,比如new Driver()和new Benz()等,如何解释?确实如此,zhangSan的显示类型是IDriver,是一个接口,是抽象的,非实体化的,在其后的所有操作中,zhangSan都是以IDriver类型进行操作,屏蔽了细节对抽象的影响。当然,张三如果要开宝马车,也很容易,我们只要修改业务场景类就可以,实现过程如代码清单3-9所示。

代码清单3-9 张三驾驶宝马车的实现过程

public class Client {
 
public static void main(String[] args) {
 
IDriver zhangSan = new Driver();
 
ICar bmw = new BMW();
 
//张三开奔驰车
 
zhangSan.drive(bmw);
 
}
 
}
     在新增加低层模块时,只修改了业务场景类,也就是高层模块,对其他低层模块如Driver类不需要做任何修改,业务就可以运行,把“变更”引起的风险扩散降低到最小。

     注意 在Java中,只要定义变量就必然要有类型,一个变量可以有两个类型:显示类型和真实类型,显示类型是在定义的时候赋予的类型,真实类型是对象的类型,如zhangSan的显示类型是IDriver,真实类型是Driver。

     我们再来思考依赖倒转对并行开发的影响。两个类之间有依赖关系,只要制定出两者之间的接口(或抽象类)就可以独立开发了,而且项目之间的单元测试也可以独立的运行,而TDD(Test-Driven Development,测试驱动开发)开发模式就是依赖倒置原则的最高级应用。我们继续回顾上面司机驾驶汽车的例子,甲程序员负责IDriver的开发,乙程序员负责ICar的开发,两个开发人员只要制定好了接口就可以独立地开发了,甲开发进度比较快,完成了IDriver以及相关的实现类Driver的开发工作,而乙程序员滞后开发,那甲是否可以进行单元测试(Unit Test)呢?答案是可以,我们引入一个JMock工具,其最基本的功能是根据抽象虚拟一个对象进行测试,测试类如代码清单3-10所示。

代码清单3-10 测试类


public class DriverTest extends TestCase{
 
Mockery context = new JUnit4Mockery();
 
@Test
 
public void testDriver() {
 
//根据接口虚拟一个对象
 
final ICar car = context.mock(ICar.class);
 
IDriver driver = new Driver();
 
//内部类
 
context.checking(new Expectations(){{
 
oneOf (car).run();
 
}});
 
driver.drive(car);
 
}
 
}
     注意粗体部分,我们只需要一个ICar的接口,就可以对Driver类进行单元测试,从这一点来看,两个相互依赖的对象可以分别进行开发,孤立的单元测试,进而保证并行开发的效率和质量,TDD开发的精髓不就在这里吗?测试驱动开发,先写好单元测试类,然后再写实现类,这对代码的质量有非常大的提高,特别适合研发类项目或在项目成员整体水平比较低的情况下采用。

     抽象是对实现的约束,对依赖者而言,也是一种契约,不仅仅约束自己,还同时约束自己与外部的关系,其目的是保证所有的细节不逃脱契约的范畴,确保约束双方按照既定的契约(抽象)共同发展,只要抽象这根基线在,细节就逃脱不了这个圈圈,始终让你的对象做到“言而有信”“言出必行”

3.3 依赖的三种写法

     依赖是可以传递的,A对象依赖B对象,B又依赖C,C又依赖D…,生生不息,依赖不止,记住一点:只要做到抽象依赖,即使是多层的依赖传递也无所畏惧!

     对象的依赖关系有三种方式来传递,如下所示。

构造函数传递依赖对象
     在类中通过构造函数声明依赖对象,按照依赖注入的说法这种方式叫做构造函数注入,按照这种方式的注入,IDriver和Driver的程序修改后如代码清单3-11所示。

代码清单3-11 构造函数传递依赖对象

public interface IDriver {
 
//是司机就应该会驾驶汽车
 
public void drive();
 
}
 
public class Driver implements IDriver{
 
private ICar car;
 
//构造函数注入
 
public Driver(ICar _car){
 
this.car = _car;
 
}
 
//司机的主要职责就是驾驶汽车
 
public void drive(){
 
this.car.run();
 
}
 
}
Setter方法传递依赖对象
     在抽象中设置setter方法声明依赖关系,依照依赖注入的说法就是setter依赖注入,按照这种方式的注入,IDriver和Driver的程序修改后如代码清单3-12所示。

代码清单3-12 Setter依赖注入


public interface IDriver {
 
//车辆型号
 
public void setCar(ICar car);
 
//是司机就应该会驾驶汽车
 
public void drive();
 
}
 
public class Driver implements IDriver{
 
private ICar car;
 
public void setCar(ICar car){
 
this.car = car;
 
}
 
//司机的主要职责就是驾驶汽车
 
public void drive(){
 
this.car.run();
 
}
 
}
接口声明依赖对象
     在接口的方法中声明依赖对象,3.2章节的例子就是采用了接口声明依赖的方式,,该方法也叫做接口注入。

3.4 最佳实践

     依赖倒转原则的本质就是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合,我们怎么在项目中使用这个规则呢?只要遵循以下的几个规则就可以:

每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备。
     这是依赖倒置的基本要求,接口和抽象类都是属于抽象的,有了抽象才可能依赖倒置。

变量的显示类型尽量是接口或者是抽象类。
     很多书上说变量的类型一定要是接口或者是抽象类,这个有点绝对化了,比如一个工具类,xxxUtils一般是不需要接口或是抽象类的。还有,如果你要使用类的clone方法,就必须使用实现类,这个是JDK提供一个规范。

任何类都不应该从具体类派生。
     如果一个项目处于开发状态,确实不应该有从具体类派生出的子类的情况,但这也不是绝对的,因为人都是会犯错误的,有时设计缺陷是在所难免的,因此只要不超过两层的继承都是可以忍受的。特别是做项目维护的同志,基本上可以不考虑这个规则,为什么?维护工作基本上都是做扩展开发,修复行为,通过一个继承关系,覆写一个方法就可以修正一个很大的Bug,何必再要去继承最高的基类呢?

尽量不要覆写基类的方法。
     如果基类是一个抽象类,而且这个方法已经实现了,子类尽量不要覆写。类间依赖的是抽象,覆写了抽象方法,对依赖的稳定性会产生一定的影响。

结合里氏替换原则使用
     在上一个章节中我们讲解了里氏替换原则,父类出现的地方子类就能出现,再结合本章节的讲解,我们可以得出这样一个通俗的规则: 接口负责定义public属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。

     讲了这么多,估计大家对“倒置”这个词还是有点不理解,那到底什么是“倒置”呢?我们先说“正置”是什么意思,依赖正置就是类间的依赖是实实在在的实现类间的依赖,也就是面向实现编程,这也是正常人的思维方式,我要开奔驰车就依赖奔驰车,我要使用笔记本电脑就直接依赖笔记本电脑,而编写程序需要的是对现实世界的事物进行抽象,抽象的结果就是有了抽象类和接口,然后我们根据系统设计的需要产生了抽象间的依赖,代替了人们传统思维中的事物间的依赖,“倒置”就是从这里产生的。

     依赖倒置原则的优点在小型项目中很难体现出来,例如小于10个人月的项目,使用简单的SSH架构,基本上不费太大力气就可以完成,是否采用依赖倒置原则影响不大。但是,在一个大中型项目中,采用依赖倒置原则可以带来非常多的优点,特别是规避一些非技术因素引起的问题。项目越大,需求变化的概率也越大,通过采用依赖倒置原则设计的接口或抽象类对实现类进行约束,可以减少需求变化引起的工作量剧增的情况。人员的变动在大中型项目中也是时常存在的,如果设计优良、代码结构清晰,人员变化对项目的影响基本为零。大中型项目的维护周期一般都很长,采用依赖倒置原则可以让维护人员轻松地扩展和维护。

     依赖倒置原则是六个设计原则中最难以实现的原则,它是实现开闭原则的重要途径,依赖倒置原则没有实现,就别想实现对扩展开放,对修改关闭。在项目中,大家只要记住是“面向接口编程”就基本上抓住了依赖倒转原则的核心。

     讲了这么多依赖倒置原则的优点,我们也来打击一下大家,在现实世界中确实存在着必须依赖细节的事物,比如法律,就必须依赖细节的定义。 “杀人偿命”在中国的法律中古今有之,那这里的杀人就是一个抽象的含义,怎么杀,杀什么人,为什么杀人,都没有定义,只要是杀人就统统得偿命,那这就是有问题了,好人杀了坏人,还要陪上自己的一条性命,这是不公正的,从这一点看,我们在实际的项目中使用依赖倒置原则时需要审时度势,不要抓住一个原则不放,每一个原则的优点都是有限度的,并不是放之四海而皆准的真理,所以别为了遵循一个原则而放弃了一个项目的终极目标:投产上线和盈利。作为一个项目经理或架构师,应该懂得技术只是实现目的的工具,惹恼了顶头上司,设计做得再漂亮,代码写得再完美,项目做得再符合标准,一旦项目亏本,产品投入大于产出,那整体就是扯淡!你自己也别想混得更好!
分享到:
评论

相关推荐

    JAVA_API1.6文档(中文)

    java.lang.management 提供管理接口,用于监视和管理 Java 虚拟机以及 Java 虚拟机在其上运行的操作系统。 java.lang.ref 提供了引用对象类,支持在某种程度上与垃圾回收器之间的交互。 java.lang.reflect 提供类...

    Java 面经手册·小傅哥.pdf

    这是一本以面试题为入口讲解 Java 核心内容的技术书籍,书中内容极力的向你证实代码是对数学逻辑的具体实现。当你仔细阅读书籍时,会发现Java中有大量的数学知识,包括:扰动函数、负载因子、拉链寻址、开放寻址、...

    java源码包---java 源码 大量 实例

    Applet钢琴模拟程序java源码 2个目标文件,提供基本的音乐编辑功能。编辑音乐软件的朋友,这款实例会对你有所帮助。 Calendar万年历 1个目标文件 EJB 模拟银行ATM流程及操作源代码 6个目标文件,EJB来模拟银行ATM...

    JAVA上百实例源码以及开源项目

     Java局域网通信——飞鸽传书源代码,大家都知道VB版、VC版还有Delphi版的飞鸽传书软件,但是Java版的确实不多,因此这个Java文件传输实例不可错过,Java网络编程技能的提升很有帮助。 Java聊天程序,包括服务端和...

    java源码包2

    Applet钢琴模拟程序java源码 2个目标文件,提供基本的音乐编辑功能。编辑音乐软件的朋友,这款实例会对你有所帮助。 Calendar万年历 1个目标文件 EJB 模拟银行ATM流程及操作源代码 6个目标文件,EJB来模拟银行...

    java源码包4

    Applet钢琴模拟程序java源码 2个目标文件,提供基本的音乐编辑功能。编辑音乐软件的朋友,这款实例会对你有所帮助。 Calendar万年历 1个目标文件 EJB 模拟银行ATM流程及操作源代码 6个目标文件,EJB来模拟银行...

    java源码包3

    Applet钢琴模拟程序java源码 2个目标文件,提供基本的音乐编辑功能。编辑音乐软件的朋友,这款实例会对你有所帮助。 Calendar万年历 1个目标文件 EJB 模拟银行ATM流程及操作源代码 6个目标文件,EJB来模拟银行...

    java api最新7.0

    JAVA开发人员最新版本7.0 api文档!本文档是 Java Platform Standard Edition 7 的 API !Java 1.7 API的中文帮助文档。 深圳电信培训中心 徐海蛟博士教学用api 7.0中文文档。支持全文检索,在线即时查询。 里面列...

    java开源包11

    JoSQL(SQLforJavaObjects)为Java开发者提供运用SQL语句来操作Java对象集的能力.利用JoSQL可以像操作数据库中的数据一样对任何Java对象集进行查询,排序,分组。 搜索自动提示 Autotips AutoTips是为解决应用系统对于...

    java开源包4

    JoSQL(SQLforJavaObjects)为Java开发者提供运用SQL语句来操作Java对象集的能力.利用JoSQL可以像操作数据库中的数据一样对任何Java对象集进行查询,排序,分组。 搜索自动提示 Autotips AutoTips是为解决应用系统对于...

    java开源包6

    JoSQL(SQLforJavaObjects)为Java开发者提供运用SQL语句来操作Java对象集的能力.利用JoSQL可以像操作数据库中的数据一样对任何Java对象集进行查询,排序,分组。 搜索自动提示 Autotips AutoTips是为解决应用系统对于...

    java开源包9

    JoSQL(SQLforJavaObjects)为Java开发者提供运用SQL语句来操作Java对象集的能力.利用JoSQL可以像操作数据库中的数据一样对任何Java对象集进行查询,排序,分组。 搜索自动提示 Autotips AutoTips是为解决应用系统对于...

    java开源包5

    JoSQL(SQLforJavaObjects)为Java开发者提供运用SQL语句来操作Java对象集的能力.利用JoSQL可以像操作数据库中的数据一样对任何Java对象集进行查询,排序,分组。 搜索自动提示 Autotips AutoTips是为解决应用系统对于...

    java开源包8

    JoSQL(SQLforJavaObjects)为Java开发者提供运用SQL语句来操作Java对象集的能力.利用JoSQL可以像操作数据库中的数据一样对任何Java对象集进行查询,排序,分组。 搜索自动提示 Autotips AutoTips是为解决应用系统对于...

    java开源包10

    JoSQL(SQLforJavaObjects)为Java开发者提供运用SQL语句来操作Java对象集的能力.利用JoSQL可以像操作数据库中的数据一样对任何Java对象集进行查询,排序,分组。 搜索自动提示 Autotips AutoTips是为解决应用系统对于...

    Java开发技术大全(500个源代码).

    HelloWorldApp.java 第一个用Java开发的应用程序。 firstApplet.java 第一个用Java开发的Applet小程序。 firstApplet.htm 用来装载Applet的网页文件 第2章 示例描述:本章介绍开发Java的基础语法知识。 ...

    java开源包1

    JoSQL(SQLforJavaObjects)为Java开发者提供运用SQL语句来操作Java对象集的能力.利用JoSQL可以像操作数据库中的数据一样对任何Java对象集进行查询,排序,分组。 搜索自动提示 Autotips AutoTips是为解决应用系统对于...

    java开源包3

    JoSQL(SQLforJavaObjects)为Java开发者提供运用SQL语句来操作Java对象集的能力.利用JoSQL可以像操作数据库中的数据一样对任何Java对象集进行查询,排序,分组。 搜索自动提示 Autotips AutoTips是为解决应用系统对于...

    Java 中文入门学习手册合集[chm版]

    第一章 Java语言的产生及其特点 第二章 Java程序开发与运行环境 第三章 Java程序设计基础 第四章 Java应用程序的基本框架 第五章 Java的类 第六章 Java图形用户接口 第七章 多线程 第八章 Java的"异常" 第九...

Global site tag (gtag.js) - Google Analytics