依赖倒置原则(Dependecy-Inversion Principle)

依赖倒置原则(Dependence Inversion Principle,DIP)的原始定义:

  • 高层模块不应该依赖底层模块,两者都应该依赖其抽象;
  • 抽象不应该依赖细节;
  • 细节应该依赖抽象。

抽象:即抽象类或接口,两者是不能够实例化的。

细节:即具体的实现类,实现接口或者继承抽象类所产生的类,两者可以通过关键字new直接被实例化。

依赖倒置原则在Java语言中的表现是:

  • 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或者抽象类产生的;
  • 接口或抽象类不依赖于实现类;
  • 实现类依赖接口或抽象类。

一、什么是依赖倒置原则
一种表述:
抽象不应当依赖于细节;细节应当依赖于抽象。

另一种表述:
要针对接口编程,不要针对实现编程。

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

不要针对实现编程的意思就是说,不应当使用具体Java类进行变量的类型声明、参量的类型声明、方法的返回类型声明,以及数据类型的转换等。

其核心思想是:依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。

高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。

(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中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。

    依赖倒置原则的核心思想是面向接口编程。

该原则规定:

  1. 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口
  2. 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。

该原则颠倒了一部分人对于面向对象设计的认识方式。如高层次和低层次对象都应该依赖于相同的抽象接口。

应用依赖反转原则同样被认为是应用了[适配器模式],例如:高层的类定义了它自己的适配器接口(高层类所依赖的抽象接口)。被适配的对象同样依赖于适配器接口的抽象(这是当然的,因为它实现了这个接口),同时它的实现则可以使用它自身所在低层模块的代码。通过这种方式,高层组件则不依赖于低层组件,因为它(高层组件)仅间接的通过调用适配器接口多态方法使用了低层组件,而这些多态方法则是由被适配对象以及它的低层模块所实现的。

依赖倒置与依赖正置

依赖正置就是类间的依赖是实实在在的实现类间的依赖,也就是面向实现编程,这也是正常人的思维方式,我要开奔驰车就依赖奔驰车,我要使用笔记本电脑就直接依赖笔记本电脑,而编写程序需要的是对现实世界的事物进行抽象,抽象的结构就是有了抽象类和接口,然后我们根据系统设计的需要产生了抽象间的依赖,代替了人们传统思维中的事物间的依赖,“倒置”就是从这里产生的。

依赖注入
依赖注入就是将实例变量传入到一个对象中去(Dependency injection means giving an object its instance variables)。
什么是依赖
如果在 Class A 中,有 Class B 的实例,则称 Class A 对 Class B 有一个依赖。例如下面类 Human 中用到一个 Father 对象,我们就说类 Human 对类 Father 有一个依赖。
依赖注入
依赖注入是这样的一种行为,在类Car中不主动创建GasEnergy的对象,而是通过外部传入GasEnergy对象形式来设置依赖。 常用的依赖注入有如下三种方式

构造器注入
将需要的依赖作为构造方法的参数传递完成依赖注入。

Setter方法注入
增加setter方法,参数为需要注入的依赖亦可完成依赖注入。

接口注入
接口注入,闻其名不言而喻,就是为依赖注入创建一套接口,依赖作为参数传入,通过调用统一的接口完成对具体实现的依赖注入。

接口注入和setter方法注入类似,不同的是接口注入使用了统一的方法来完成注入,而setter方法注入的方法名称相对比较随意。

在实现依赖倒转原则时,我们需要针对抽象层编程,而将具体类的对象通过依赖注入(DependencyInjection, DI)的方式注入到其他对象中,依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。常用的注入方式有三种,分别是:构造注入,设值注入(Setter注入)和接口注入。构造注入是指通过构造函数来传入具体类的对象,设值注入是指通过Setter方法来传入具体类的对象,而接口注入是指通过在接口中声明的业务方法来传入具体类的对象。这些方法在定义时使用的是抽象类型,在运行时再传入具体类型的对象,由子类对象来覆盖父类对象。

依赖注入

  1. 依赖 如果在 Class A 中,有 Class B 的实例,则称 Class A 对 Class B 有一个依赖。例如下面类 Human 中用到一个 Father 对象,我们就说类 Human 对类 Father 有一个依赖。

public class Human {
    ...
    Father father;
    ...
    public Human() {
        father = new Father();
    }
}

仔细看这段代码我们会发现存在一些问题:
(1). 如果现在要改变 father 生成方式,如需要用new Father(String name)初始化 father,需要修改 Human 代码;
(2). 如果想测试不同 Father 对象对 Human 的影响很困难,因为 father 的初始化被写死在了 Human 的构造函数中;
(3). 如果new Father()过程非常缓慢,单测时我们希望用已经初始化好的 father 对象 Mock 掉这个过程也很困难。

  1. 依赖注入 上面将依赖在构造函数中直接初始化是一种 Hard init 方式,弊端在于两个类不够独立,不方便测试。我们还有另外一种 Init 方式,如下:

public class Human {
    ...
    Father father;
    ...
    public Human(Father father) {
        this.father = father;
    }
}

上面代码中,我们将 father 对象作为构造函数的一个参数传入。在调用 Human 的构造方法之前外部就已经初始化好了 Father 对象。像这种非自己主动初始化依赖,而通过外部来传入依赖的方式,我们就称为依赖注入
现在我们发现上面 1 中存在的两个问题都很好解决了,简单的说依赖注入主要有两个好处:
(1). 解耦,将依赖之间解耦。
(2). 因为已经解耦,所以方便做单元测试,尤其是 Mock 测试。

3、什么是倒置

到了这里,我们对依赖倒置原则的“依赖”就很好理解了,但是什么是“倒置”呢。是这样子的,刚开始按照正常人的一般思维方式,我想吃香蕉就是吃香蕉,想吃苹果就吃苹果,编程也是这样,都是按照面向实现的思维方式来设计。而现在要倒置思维,提取公共的抽象,面向接口(抽象类)编程。不再依赖于具体实现了,而是依赖于接口或抽象类,这就是依赖的思维方式“倒置”了。

4、依赖的三种实现方式

对象的依赖关系有三种方式来传递:

//人接口
public interface People {
    public void eat(Fruit fruit);//人都有吃的方法,不然都饿死了
}
//水果接口
public interface Fruit {
    public String getName();//水果都是有名字的
}
//具体Jim人类
public class Jim implements People{
    public void eat(Fruit fruit){
        System.out.println("Jim eat " + fruit.getName());
    }
}
//具体苹果类
public class Apple implements Fruit{
    public String getName(){
        return "apple";
    }
}
//具体香蕉类
public class Banana implements Fruit{
    public String getName(){
        return "banana";
    }
}
public class Client {
    public static void main(String[] args) {
        People jim = new Jim();
        Fruit apple = new Apple();
        Fruit Banana = new Banana(); //这里符合了里氏替换原则
        jim.eat(apple);
        jim.eat(Banana);
    }
}

Client类是复杂的业务逻辑,属于高层模块,而People和Fruit是原子模块,属于低层模块。Client依赖于抽象的People和Fruit接口,这就做到了:高层模块不应该依赖低层模块,两者都应该依赖于抽象(抽象类或接口)。
Client不仅依赖于接口Fruit,还依赖于具体的实现Apple了。

People和Fruit接口与各自的实现类没有关系,增加实现类不会影响接口,这就做到了:抽象(抽象类或接口)不应该依赖于细节(具体实现类)。

Jim、Apple、Banana实现类都要去实现各自的接口所定义的抽象方法,所以是依赖于接口的。这就做到了:细节(具体实现类)应该依赖抽象。

“依赖于抽象(或者叫接口)”这个说法是对的,只是这段代码Fruit apple = new Apple();不能去new具体的实现,应该用工厂方法来生产出具体的实现,这样就可以做到只依赖于接口了。

接口方法中声明依赖对象。就是我们上面代码所展示的那样。
而接口注入是指通过在接口中声明的业务方法来传入具体类的对象。这些方法在定义时使用的是抽象类型,在运行时再传入具体类型的对象,由子类对象来覆盖父类对象。

构造方法传递依赖对象。在构造函数中的需要传递的参数是抽象类或接口的方式实现。代码如下:

//具体Jim人类
public class Jim implements People{
     
    private Fruit fruit;
     
    public Jim(Fruit fruit){//构造方法传递依赖对象
        this.fruit = fruit;
    }
     
    public void eat(Fruit fruit){
        System.out.println("Jim eat " + this.fruit.getName());
    }
}

Setter方法传递依赖对象。在我们设置的setXXX方法中的参数为抽象类或接口,来实现传递依赖对象。代码如下:

//具体Jim人类
public class Jim implements People{
     
    private Fruit fruit;
     
    public void setFruit(Fruit fruit){//setter方式传递依赖对象
        this.fruit = fruit;
    }
     
    public void eat(){
        System.out.println("Jim eat " + this.fruit.getName());
    }
}

5、优点

从上面的代码修改过程中,我们可以看到由于类之间松耦合的设计,面向接口编程依赖抽象而不依赖细节,所以在修改某个类的代码时,不会牵涉到其他类的修改,显著降低系统风险,提高系统健壮性。

还有一个优点是,在我们实际项目开发中,都是多人团队协作,每人负责某一模块。比如一个人负责开发People模块,一人负责开发Fruit模块,如果未采用依赖倒置原则,没有提取抽象,那么开发People模块的人必须等Fruit模块开发完成后自己才能开发,否则编译都无法通过,这就是单线程的开发。为了能够两人并行开发,设计时遵循依赖倒置原则,提取抽象,就可以大大提高开发进度。

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

6、总结

依赖倒置原则实际上就是要求“面向接口编程”。

说到底,依赖倒置原则的核心就是面向接口编程的思想,尽量对每个实现类都提取抽象和公共接口形成接口或抽象类,依赖于抽象而不要依赖于具体实现。依赖倒置原则的本质其实就是通过抽象(抽象类或接口)使各个类或模块的实现彼此独立,不相互影响,实现模块间的松耦合。但是这个原则也是6个设计原则中最难以实现的了,如果没有实现这个原则,那么也就意味着开闭原则(对扩展开放,对修改关闭)也无法实现。

首先要明白所有原则都是为了达到面向对象设计的可扩展可复用可维护性而出现的…… 开闭原则是目的:一个已有的代码模块,需要很容易增加新的扩展功能(可扩展性),这个已有模块需要是对外开放的;为了使已有模块可以复用(可复用性),已有模块需要是独立的(单一职责,高内聚,不与其他模块耦合在一起),同时为了方便维护(可维护性),已有模块最好不要对原有代码进行修改,也就是需要是对内封闭的;实现了开闭原则的设计,就达到面向对象设计可扩展可复用可维护性的目的,所以说开闭原则是目的! 里氏代换原则是基础:通过针对抽象基类编程(业务逻辑关系的建立),具体运行时代换具体子类对象执行,可以达到开闭原则的目的,该实现过程就是里氏代换原则定义本身,所以说里氏代换原则是理论基础! 依赖倒转原则是手段:牛人们总结了实现里氏代换原则的方法,抽象不依赖于细节,细节应该依赖于抽象的依赖倒转原则。具体就是变量、参数、方法返回、数据类型转换等都要用抽象定义声明,再通过依赖注入(构造注入、设值注入和接口注入)的方式将具体对象注入到有依赖关系的对象中。所以说依赖倒转原则是实现目的手段!

开闭原则是目标,里氏代换原则是基础,依赖倒转原则是手段; 第一要明确,所有的原则都是为了实现面向对象设计的可扩展性,可复用性,可维护性(对原有代码不要进行修改)而定义的;一个已有的代码模块,需要实现可扩展性,需要对外保持开放;为了实现可复用性,需要保持独立(单一职责,高内聚,低耦合);为了实现可维护性,需要对内封闭(对已有代码模块不要进行修改)。 开闭原则(Open-Closed Principle, OCP):一个软件实体应当对扩展开放(可扩展性),对修改关闭(可维护性)。即软件实体应尽量在不修改原有代码的情况下进行扩展。所以说开闭原则是目的。 第二,为了实现开闭原则(可扩展性,可维护性和复用性)在最初就需要对代码模块进行抽象化设计(抽象化是实现开闭原则的关键);面向对象思想中的抽象化是指把现实中一类具有相同属性,行为的事物归类的方法。 抽象 ---对同一类对象的共同属性和行为进行概括,形成类。 有:数据抽象(属性或状态)、代码抽象(某类对象的共有的行为特征或功能)。抽象的实现是:类 大牛们就想在Java、C#等编程语言中,可以为系统定义一个相对稳定的抽象层,而将不同的实现行为移至具体的实现层中完成。在很多面向对象编程语言中都提供了接口、抽象类等机制,可以通过它们定义系统的抽象层,再通过具体类来进行扩展。如果需要修改系统的行为,无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可,实现在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。具体操作作:通过针对抽象基类编程(业务逻辑关系的建立),具体运行时代换具体子类对象执行,可以达到开闭原则的目的,该实现过程就是里氏代换原则定义本身,所以说里氏代换原则是理论基础! 通过里氏代换原则的操操作过程,大牛们总结出抽象不依赖于细节,细节应该依赖于抽象的依赖倒转原则。具体就是变量、参数、方法返回、数据类型转换等都要用抽象定义声明,再通过依赖注入(构造注入、设值注入和接口注入)或依赖获取的方式将具体对象注入到有依赖关系的对象中。所以说依赖倒转原则是实现目的手段! 在实现针对抽象编程的实践中总结出接口隔离原则(Interface Segregation Principle, ISP):使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。

使用依赖倒转原则的编程方式+里氏转换的约束=开闭原则

本质:

依赖倒置原则的本质就是通过抽象(接口或者抽象类)使各个类或模型的实现彼此独立,不互相影响,实现模块间的松耦合。

规则:

每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备;
变量的表面类型尽量是接口或者抽象类;
任何类都不应该从具体类派生;
尽量不要覆写基类的非抽象方法;
结合里氏替换原则使用。

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值