面试官:你是如何理解Java中依赖倒置和依赖注入以及控制反转的?

什么是依赖(Dependency)?

依赖是类与类之间的链接,依赖关系表示一个类依赖于另外一个类的定义,通俗来说java

就是一种须要,例如一我的(Person)能够买车(Car)和房子(House),Person类依赖于Car类和House类面试

public static void main(String ... args){
        //TODO:

    Person person = new Person();
    person.buy(new House());
    person.buy(new Car());

}

static class Person{

    //表示依赖House
    public void buy(House house){}
    //表示依赖Car
    public void buy(Car car){}
}

static class House{

}

static class Car{

}
复制代码

依赖倒置 (Dependency inversion principle)

依赖倒置是面向对象设计领域的一种软件设计原则编程

软件设计有 6 大设计原则,合称 SOLID设计模式

一、单一职责原则(Single Responsibility Principle,简称SRP )

  • 核心思想: 应该有且仅有一个缘由引发类的变动
  • 问题描述: 假若有类Class1完成职责T1,T2,当职责T1或T2有变动须要修改时,有可能影响到该类的另一个职责正常工做。
  • 好处: 类的复杂度下降、可读性提升、可维护性提升、扩展性提升、下降了变动引发的风险。
  • 需注意: 单一职责原则提出了一个编写程序的标准,用“职责”或“变化缘由”来衡量接口或类设计得是否优良,可是“职责”和“变化缘由”都是不能够度量的,因项目和环境而异。

二、里氏替换原则(Liskov Substitution Principle,简称LSP)

  • 核心思想: 在使用基类的的地方能够任意使用其子类,能保证子类完美替换基类。
  • 通俗来说: 只要父类能出现的地方子类就能出现。反之,父类则未必能胜任。
  • 好处: 加强程序的健壮性,即便增长了子类,原有的子类还能够继续运行。
  • 需注意: 若是子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系 采用依赖、聚合、组合等关系代替继承。

三、依赖倒置原则(Dependence Inversion Principle,简称DIP)

  • 核心思想:高层模块不该该依赖底层模块,两者都该依赖其抽象;抽象不该该依赖细节;细节应该依赖抽象;
  • 说明:高层模块就是调用端,低层模块就是具体实现类。抽象就是指接口或抽象类。细节就是实现类。
  • 通俗来说: 依赖倒置原则的本质就是经过抽象(接口或抽象类)使个各种或模块的实现彼此独立,互不影响,实现模块间的松耦合。
  • 问题描述: 类A直接依赖类B,假如要将类A改成依赖类C,则必须经过修改类A的代码来达成。这种场景下,类A通常是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操做;假如修改类A,会给程序带来没必要要的风险。
  • 解决方案: 将类A修改成依赖接口interface,类B和类C各自实现接口interface,类A经过接口interface间接与类B或者类C发生联系,则会大大下降修改类A的概率。
  • 好处:依赖倒置的好处在小型项目中很难体现出来。但在大中型项目中能够减小需求变化引发的工做量。使并行开发更友好。

四、接口隔离原则(Interface Segregation Principle,简称ISP)

  • 核心思想:类间的依赖关系应该创建在最小的接口上
  • 通俗来说: 创建单一接口,不要创建庞大臃肿的接口,尽可能细化接口,接口中的方法尽可能少。也就是说,咱们要为各个类创建专用的接口,而不要试图去创建一个很庞大的接口供全部依赖它的类去调用。
  • 问题描述: 类A经过接口interface依赖类B,类C经过接口interface依赖类D,若是接口interface对于类A和类B来讲不是最小接口,则类B和类D必须去实现他们不须要的方法。
  • 需注意:
  • 接口尽可能小,可是要有限度。对接口进行细化能够提升程序设计灵活性,可是若是太小,则会形成接口数量过多,使设计复杂化。因此必定要适度
  • 提升内聚,减小对外交互。使接口用最少的方法去完成最多的事情
  • 为依赖接口的类定制服务。只暴露给调用的类它须要的方法,它不须要的方法则隐藏起来。只有专一地为一个模块提供定制服务,才能创建最小的依赖关系。

五、迪米特法则(Law of Demeter,简称LoD)

  • 核心思想: 类间解耦。
  • 通俗来说: 一个类对本身依赖的类知道的越少越好。自从咱们接触编程开始,就知道了软件编程的总的原则:低耦合,高内聚。不管是面向过程编程仍是面向对象编程,只有使各个模块之间的耦合尽可能的低,才能提升代码的复用率。低耦合的优势不言而喻,可是怎么样编程才能作到低耦合呢?那正是迪米特法则要去完成的。

六、开放封闭原则(Open Close Principle,简称OCP)

  • 核心思想: 尽可能经过扩展软件实体来解决需求变化,而不是经过修改已有的代码来完成变化
  • 通俗来说: 一个软件产品在生命周期内,都会发生变化,既然变化是一个既定的事实,咱们就应该在设计的时候尽可能适应这些变化,以提升项目的稳定性和灵活性。

依赖倒置原则的定义以下:微信

  • 上层模块不该该依赖底层模块,它们都应该依赖于抽象。
  • 抽象不该该依赖于细节,细节应该依赖于抽象。

什么是上层模块和底层模块?

无论你认可不认可,“有人的地方就有江湖”,咱们都说人人平等,可是对于任何一个组织机构而言,它必定有架构的设计有职能的划分。按照职能的重要性,天然而然就有了上下之分。而且,随着模块的粒度划分不一样这种上层与底层模块会进行变更,也许某一模块相对于另一模块它是底层,可是相对于其余模块它又多是上层架构

组织架构
ide


公司管理层就是上层,CEO 是整个事业群的上层,那么 CEO 职能之下就是底层。

而后,咱们再以事业群为整个体系划分模块,各个部门经理以上部分是上层,那么之下的组织均可以称为底层。函数

由此,咱们能够看到,在一个特定体系中,上层模块与底层模块能够按照决策能力高低为准绳进行划分。工具

那么,映射到咱们软件实际开发中,通常咱们也会将软件进行模块划分,好比业务层、逻辑层和数据层。this

业务模块


业务层中是软件真正要进行的操做,也就是 作什么

逻辑层是软件现阶段为了业务层的需求提供的实现细节,也就是怎么作

数据层指业务层和逻辑层所须要的数据模型。

所以,如前面所总结,按照决策能力的高低进行模块划分。业务层天然就处于上层模块,逻辑层和数据层天然就归类为底层。

什么是抽象和细节?

象如其名字同样,是一件很抽象的事物。抽象每每是相对于具体而言的,具体也能够被称为细节,固然也被称为具象。

好比:

  1. 这是一幅画。画是抽象,而油画、素描、国画而言就是具体。
  2. 这是一件艺术品,艺术品是抽象,而画、照片、瓷器等等就是具体了。
  3. 交通工具是抽象,而公交车、单车、火车等就是具体了。
  4. 表演是抽象,而唱歌、跳舞、小品等就是具体。

上面能够知道,抽象能够是物也能够是行为。

具体映射到软件开发中,抽象能够是接口或者抽象类形式。

/** * Driveable 是接口,因此它是抽象 */
public interface Driveable {
    void drive();
}
/** * 而 Bike 实现了接口,它们被称为具体。 */
public class Bike implements Driveable {
    @Override
    public void drive() {
        System.out.println("Bike drive");
    }
}
/** * 而 Car实现了接口,它们被称为具体。 */
public class Car implements Driveable {
    @Override
    public void drive() {
        System.out.println("Car drive.");
    }
}
复制代码

依赖倒置的好处

在日常的开发中,咱们大概都会这样编码。

public class Person {

    private Bike mBike;
    private Car mCar;
    private Train mTrain;

    public Person(){
        mBike = new Bike();
        //mCar = new Car();
// mTrain = new Train();
    }

    public void goOut(){
        System.out.println("出门啦");
        mBike.drive();
        //mCar.drive();
// mTrain.drive();
    }

    public static void main(String ... args){
            //TODO:
        Person person = new Person();
        person.goOut();
    }
}
复制代码

咱们建立了一个 Person 类,它拥有一台自行车,出门的时候就骑自行车。

不过,自行车适应很短的距离。若是,我要出门逛街呢?自行车就不大合适了。因而就要改为汽车。

不过,若是我要到北京去,那么汽车也不合适了。

有没有一种方法能让 Person 的变更少一点呢?由于这是最基础的演示代码,若是工程大了,代码复杂了,Person 面对需求变更时改动的地方会更多。

而依赖倒置原则正好适用于解决这类状况。

下面,咱们尝试运用依赖倒置原则对代码进行改造。

咱们再次回顾下它的定义。

上层模块不该该依赖底层模块,它们都应该依赖于抽象。

抽象不该该依赖于细节,细节应该依赖于抽象。

首先是上层模块和底层模块的拆分。

按照决策能力高低或者重要性划分,Person 属于上层模块,Bike、Car 和 Train 属于底层模块。

上层模块不该该依赖于底层模块。

person架构


public class Person {

// private Bike mBike;
    private Car mCar;
    private Train mTrain;
    private Driveable mDriveable;

    public Person(){
// mBike = new Bike();
        //mCar = new Car();
       mDriveable = new Train();
    }

    public void goOut(){
        System.out.println("出门啦");
        mDriveable.drive();
        //mCar.drive();
// mTrain.drive();
    }

    public static void main(String ... args){
            //TODO:
        Person person = new Person();
        person.goOut();
    }
}
复制代码

能够看到,依赖倒置实质上是面向接口编程的体现

控制反转 (IoC)

控制反转 IoC 是 Inversion of Control的缩写,意思就是对于控制权的反转,对么控制权是什么控制权呢?

Person本身掌控着内部 mDriveable 的实例化。

如今,咱们能够更改一种方式。将 mDriveable 的实例化移到 Person 外面。

public class Person2 {

    private Driveable mDriveable;

    public Person2(Driveable driveable){
        this.mDriveable = driveable;
    }

    public void goOut(){
        System.out.println("出门啦");
        mDriveable.drive();
        //mCar.drive();
// mTrain.drive();
    }

    public static void main(String ... args){
            //TODO:
        Person2 person = new Person2(new Car());
        person.goOut();
    }
}
复制代码

就这样不管出行方式怎么变化,Person 这个类都不须要更改代码了。

在上面代码中,Person 把内部依赖的建立权力移交给了 Person2这个类中的 main() 方法。也就是说 Person 只关心依赖提供的功能,但并不关心依赖的建立。

这种思想其实就是 IoC,IoC 是一种新的设计模式,它对上层模块与底层模块进行了更进一步的解耦。控制反转的意思是反转了上层模块对于底层模块的依赖控制。

好比上面代码,Person 再也不亲自建立 Driveable 对象,它将依赖的实例化的权力交接给了 Person2。而 Person2在 IoC 中又指代了 IoC 容器 这个概念。

依赖注入(Dependency injection)

依赖注入,也常常被简称为 DI,其实在上一节中,咱们已经见到了它的身影。它是一种实现 IoC 的手段。什么意思呢?

为了避免由于依赖实现的变更而去修改 Person,也就是说以可能在 Driveable 实现类的改变下不改动 Person 这个类的代码,尽量减小二者之间的耦合。咱们须要采用上一节介绍的 IoC 模式来进行改写代码。

这个须要咱们移交出对于依赖实例化的控制权,那么依赖怎么办?Person 没法实例化依赖了,它就须要在外部(IoC 容器)赋值给它,这个赋值的动做有个专门的术语叫作注入(injection),须要注意的是在 IoC 概念中,这个注入依赖的地方被称为 IoC 容器,但在依赖注入概念中,通常被称为注射器 (injector)。

表达通俗一点就是:我不想本身实例化依赖,你(injector)建立它们,而后在合适的时候注入给我

实现依赖注入有 3 种方式:

  1. 构造函数中注入
  2. setter 方式注入
  3. 接口注入
/** * 接口方式注入 * 接口的存在,代表了一种依赖配置的能力。 */
public interface DepedencySetter {
    void set(Driveable driveable);
}
public class Person2 implements DepedencySetter {

    //接口方式注入
    @Override
    public void set(Driveable driveable) {
        this.mDriveable = mDriveable;
    }

    private Driveable mDriveable;

    //构造函数注入
    public Person2(Driveable driveable){
        this.mDriveable = driveable;
    }

    //setter 方式注入
    public void setDriveable(Driveable mDriveable) {
        this.mDriveable = mDriveable;
    }

    public void goOut(){
        System.out.println("出门啦");
        mDriveable.drive();
        //mCar.drive();
// mTrain.drive();
    }

    public static void main(String ... args){
            //TODO:
        Person2 person = new Person2(new Car());
        person.goOut();
    }
}
复制代码

最后

读到这的朋友以为不错能够点赞关注下,感谢您的支持,之后会不停更新更多精选干货及资讯分享,欢迎你们在评论区留言讨论!

欢迎关注享学课堂online微信公众号,天天会持续更新技术干货,热点,吐槽等文章,还有免费的Java架构视频资料和面试专题资料免费领取分享,后台回复关键字【Java资料】,免费获取Java架构面试专题文档资料、电子书及更多架构进阶视频资料(视频+笔记)

相关文章
相关标签/搜索