【Engineering】依赖反转原则(The Dependency Inversion Principle)和控制反转(Inversion of Control)

Posted by 西维蜀黍 on 2018-02-24, Last Modified on 2021-09-21

什么是依赖(Dependency)

**依赖(Dependency)**是一种关系,通俗来讲就是一种需要。

程序员需要电脑,因为没有电脑程序员就没有办法编写代码,所以说程序员依赖电脑,电脑被程序员依赖。

在面向对象编程中,代码可以这样编写。

class Coder {
    Computer mComputer;

    public Coder () {
        mComputer = new Computer();
    }
}

在这个例子中,Coder 类的内部依赖 Computer 类,这就是依赖在编程世界中的体现。

依赖也可以称之为耦合(coupling)

依赖反转原则 (The Dependency Inversion Principle)

在传统的应用架构中,低层次的组件设计用于被高层次的组件使用,这一点提供了逐步的构建一个复杂系统的可能。在这种结构下,高层次的组件直接依赖于低层次的组件去实现一些任务,但这种对于低层次组件的依赖限制了高层次组件被重用的可行性

在面向对象编程领域中,依赖反转原则(Dependency inversion principle,DIP)是指一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。

理解

  • 高层次的模块不应依赖低层次的模块的具体实现,他们都应该依赖于抽象。
  • 抽象不应依赖于具体实现,具体实现应依赖抽象。

高层次的和低层次的对象都应该依赖于相同的抽象接口,一般来说,高层次实体引用接口,而低层次类实现该接口,而不是高层次类直接引用低层次类,以此实现解耦。

从本质上来说,依赖反转原则是面向接口编程的体现。

例子

最初的实现

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

public class Person {
    private Bike mBike;

    public Person() {
        mBike = new Bike();
    }

    public void goOut() {
        System.out.println("外出ing...");
        mBike.drive();
    }
}

我们创建了一个 Person 类,它拥有一台自行车,而且他出门的时候就骑自行车。

public class Test {
    public static void main(String[] args) {
        Person person = new Person();
        person.goOut();
    }
}

不过,骑自行车只适合很短的出行。如果,要去郊区游玩呢?自行车可能就不大合适了。于是就要改成汽车。

因此,我们就有了下面的代码。

public class Person {
    //private Bike mBike;
 		private Car mCar;
 		
    public Person() {
        //mBike = new Bike();
        mCar = new Car();
    }

    public void goOut() {
        System.out.println("外出ing...");
        //mBike.drive();
      	mCar.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("外出ing...");
      	//mBike.drive();
        //mCar.drive();
        mTrain.drive();
    }
}

不知道你有没有发现,我们这样的实现并不是很优美。本质上,是因为Person类依赖于一个具体的交通工具,


我们再次回顾下依赖反转原则 (The Dependency Inversion Principle)的定义。

  1. 上层模块不应该依赖底层模块,它们(上层模块和底层模块)都应该依赖于抽象。
  2. 抽象不应该依赖于细节,细节应该依赖于抽象。

优化的实现

而基于依赖反转原则,我们可以对上面的实现进行优化。

由于Person 属于高层模块,而Bike、Car 和 Train 属于底层模块。根据"上层模块不应该依赖底层模块,它们都应该依赖于抽象",我们应该这样修改:

public class Person {
//  private Bike mBike;
//  private Car mCar;
//  private Train mTrain;
    private Vehicle mvehicle;

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

    public void goOut() {
        System.out.println("外出ing...");
      	//mBike.drive();
        //mCar.drive();
      	//mTrain.drive();
        mvehicle.drive();
    }
}

调用

public class Test {
    public static void main(String[] args) {
        Person p1 = new Person(new Bike());
        p1.goOut();

        Person p2 = new Person(new Train());
        p2.goOut();
    }
}

分析

现在,Person 类中仅仅依赖于 Vehicle 接口(Person 需要的是 Vehicle,即交通工具),它没有限定自己出行的可能性,任何 Car、Bike 或者是 Train 都可以的,哪怕之后要去太空旅游而坐宇宙飞船。

对于Person 类而言,都是没有区别的。因为,每次实例化一个Person 对象时,只需要传入一个对应的实现了Vehicle 接口的交通工具类即可。

到这一步,就符合了"上层模块不依赖底层模块,它们(上层模块和底层模块)都依赖于抽象“的准则了。


那么,”抽象不应该依赖于细节,细节应该依赖于抽象“又怎么理解呢?

Vehicle 是抽象(或者说,Vehicle 是一个接口),而 Bike、Car、Train 等都是这个抽象具体的实现。Vehicle接口不依赖于具体的细节(即各种交通工具,Bike、Car或Train ),而各种交通工具依赖于抽象(即Vehicle接口)。

总结

在上面的例子中,Person 属于高层模块,而Bike、Car 和 Train 属于底层模块。因此:

  • 上层模块(Person类)不依赖于底层模块(Bike、Car 和 Train 类);
  • 上层模块(Person类)依赖于抽象(Vehicle 接口);
  • 底层模块(Bike、Car 和 Train 类)也依赖于抽象(Vehicle 接口)。

如果尝试对应到一个具体的业务场景,Person 类好比一个Voucher Service,Voucher Service需要拉取voucher的信息(类比于Person类需要通过一个交通工具来达到一个指定的地方)。

随着业务系统的发展,或者在不同的情况下,可能需要采用不同的方式来实现拉取(比如直接通过无Cache layer直接访问MySQL,访问MySQL的slave或master - 根据场景所要求的一致性),类比于Person类在不同的情况下需要通过不同的交通工具来达到一个指定的地方,比如根据目的地的远近。

控制反转 (Inversion of Control)

控制反转(Inversion oc Control, IoC)是在面向对象编程中的一种思想,用来减少代码之间的耦合度。

按个人目前的理解,**控制反转 (Inversion of Control)可以理解为依赖反转原则 (The Dependency Inversion Principle)**概念的延伸,或者说在此基础上的进一步讨论。即,在依赖反转原则(“上层模块不应该依赖底层模块,它们都应该依赖于抽象“)的基础之上,控制反转还进一步去讨论了为对象进行实例化的控制问题

之所以这么说,是因为在依赖反转原则的讨论上下文中,我们只关心类与类之间的耦合问题(或者说模块与模块之间的耦合问题);而在控制反转的讨论上下文中,我们在关心在类与类耦合问题的同时,还关心被耦合(被依赖)的类对应的对象在哪进行初始化。

更具体的来说,在控制反转思想中,控制指的是对一个具体的对象内部成员变量的实例化,而反转的是对内部成员变量的实例化过程的控制。

合在一起,就是一个具体的对象内部的成员变量的实例化不再由这个对象来负责(如果由这个对象来负责,则是正常的情况,或者称为正转),而是由一个被称为**控制反转容器(IoC container)**的实体负责。


从进行对象实例化操作的角度而言:

  • 在正常的模式(或者说非控制反转的模式)中,通常由对象自身来完成对其内部成员变量的实例化(控制);
  • 而在控制反转的场景中,对象内部的成员变量的实例化,由控制反转容器来完成。即开发者只需要将定义好的类(implementation)交给控制反转容器,控制反转容器会通过反射直接完成对象内部成员变量的实例化(当然,在此之前,开发者需要在特定位置(比如配置文件中),指定对象与对象之间的依赖关系)。

对象实例化场所 - 控制反转容器(IoC container)

我们注意到,在上面基于依赖反转原则进行讨论的例子中,我们只是简单地在主函数中完成Person类和具体交通工具类的实例化:

public class Test {
    public static void main(String[] args) {
        Person p1 = new Person(new Bike());
        p1.goOut();

        Person p2 = new Person(new Train());
        p2.goOut();
    }
}

而事实上,对象实例化的场所可以不仅限于这种由程序员通过**硬编码(hard code)**来完成实例化的情况。

在控制反转的上下文中,包含一个**控制反转容器(IoC container)**的概念。控制反转容器是指在运行时对对象进行实例化的实体。

因此,控制反转容器专门负责对象的实例化工作。更具体地说,开发者只需要将定义好的类(implementation)交给控制反转容器,并且在特定位置(比如配置文件中)指定对象与对象之间的依赖关系。对应在上面的例子中,就是指定一个具体的Person对象具体使用哪种交通工具(比如Bike)。此后,控制反转容器会通过反射完成被依赖对象的对象实例化。

总结

总结来说:

  • 控制是指某个对象对其"内部的成员变量的实例化"的控制;
  • 被反转的是对对象实例化的控制;
  • 反转是相对于正转而言的。在正转(正常的情况)时,在传统应用程序中,是由开发者自己在对象中显式地控制其内部对象(依赖对象)的实例化创建。

Inversion of Control To Dependency Injection

控制反转的概念并不是特别好理解,因为从字面上既没有表达出"是对什么的控制”,也没有表达出"相对于什么的反转"(或者说,什么情况是正转)。

Martin Fowler 在一篇经典文章《Inversion of Control Containers and the Dependency Injection pattern》中,为控制反转起了一个更准确表达其含义的名字,叫做依赖注入 (Dependency Injection)

相对控制反转而言,“依赖注入”的确更加准确地描述了这种古老而又时兴的设计理念。从名字上理解,所谓依赖注入,即对象实例之间的具体依赖关系在特定位置(比如配置文件或以Java注解的形式)被指定,而控制反转容器负责在运行时,对对象进行实例化,并将被依赖的对象实例注入到对应的对象内部


对应于上面的例子,假设传统的非依赖注入的实现是这样的(即这个Person对象依赖于Bike出现):

Person p1 = new Person(new Bike());
p1.goOut();

采用依赖注入后,开发者仅需在特定位置指定这个Person对象需要Bike对象即可(而不需要显式地像上面代码这样对被依赖的具体的Vehicle进行实例化)。

控制反转实现方式

我们有不同的方法来实现控制反转:

  • 依赖注入(dependency injection)
  • 服务定位器(service locator pattern)
  • contextualized lookup
  • template method design pattern
  • strategy design pattern

注意,依赖注入(dependency injection)只是实现控制反转思想的其中一种方式。

依赖注入(Dependency Injection)

Refer to https://swsmile.info/post/dependency-injection/.

Dependency injection implements inversion of control through composition, and is often similar to the strategy pattern.

While the strategy pattern is intended for dependencies that are interchangeable throughout an object’s lifetime, in dependency injection it may be that only a single instance of a dependency is used. This still achieves polymorphism, but through delegation and composition.

服务定位器模式(Service Locator Pattern)

Service Locator模式背后的基本思想是:有一个对象(即服务定位器)知道如何获得一个应用程序所需的所有服务。

You create a class known as the service locator that creates and stores dependencies and then provides those dependencies on demand.

class ServiceLocator {

    private static ServiceLocator instance = null;

    private ServiceLocator() {}

    public static ServiceLocator getInstance() {
        if (instance == null) {
            synchronized(ServiceLocator.class) {
                instance = new ServiceLocator();
            }
        }
        return instance;
    }

    public Engine getEngine() {
        return new Engine();
    }
}

class Car {

    private Engine engine = ServiceLocator.getInstance().getEngine();

    public void start() {
        engine.start();
    }
}

class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}

The service locator pattern is different from dependency injection in the way the elements are consumed. With the service locator pattern, classes have control and ask for objects to be injected; with dependency injection, the app has control and proactively injects the required objects.

Compared to dependency injection:

  • The collection of dependencies required by a service locator makes code harder to test because all the tests have to interact with the same global service locator.
  • Dependencies are encoded in the class implementation, not in the API surface. As a result, it’s harder to know what a class needs from the outside. As a result, changes to Car or the dependencies available in the service locator might result in runtime or test failures by causing references to fail.
  • Managing lifetimes of objects is more difficult if you want to scope to anything other than the lifetime of the entire app.

依赖查找(Dependency Lookup)

依赖查找和依赖注入一样属于控制反转原则的具体实现,不同于依赖注入的被动接受,依赖查找这是主动请求,在需要的时候通过调用框架提供的方法来获取对象,获取时需要提供相关的配置文件路径、key等信息来确定获取对象的状态。

Template Method Design Pattern

Reference