什么是依赖(Dependency)
依赖(Dependency)是一种关系,通俗来讲就是一种需要。
程序员需要电脑,因为没有电脑程序员就没有办法编写代码,所以说程序员依赖电脑,电脑被程序员依赖。
在面向对象编程中,代码可以这样编写。
1 | class Coder { |
在这个例子中,Coder 类的内部依赖 Computer 类,这就是依赖在编程世界中的体现。
依赖也可以称之为耦合(coupling)。
依赖反转原则 (The Dependency Inversion Principle)
在传统的应用架构中,低层次的组件设计用于被高层次的组件使用,这一点提供了逐步的构建一个复杂系统的可能。在这种结构下,高层次的组件直接依赖于低层次的组件去实现一些任务,但这种对于低层次组件的依赖限制了高层次组件被重用的可行性。
在面向对象编程领域中,依赖反转原则(Dependency inversion principle,DIP)是指一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。
理解
- 高层次的模块不应依赖低层次的模块,他们都应该依赖于抽象。
- 抽象不应依赖于具体实现,具体实现应依赖抽象。
高层次的和低层次的对象都应该依赖于相同的抽象接口,一般来说,高层次实体引用接口,而低层次类实现该接口,而不是高层次类直接引用低层次类,以此实现解耦。
从本质上来说,依赖反转原则是面向接口编程的体现。
例子
最初的实现
在平常的开发中,我们大概都会这样编码。
1 | public class Person { |
我们创建了一个 Person 类,它拥有一台自行车,而且他出门的时候就骑自行车。
1 | public class Test { |
不过,骑自行车只适合很短的出行。如果,要去郊区游玩呢?自行车可能就不大合适了。于是就要改成汽车。
因此,我们就有了下面的代码。
1 | public class Person { |
不过,如果要去北京,那么汽车又不合适了。
1 | public class Person { |
不知道你有没有发现,我们这样的实现并不是很优美。本质上,是因为Person类依赖于一个具体的交通工具,
我们再次回顾下依赖反转原则 (The Dependency Inversion Principle)的定义。
- 上层模块不应该依赖底层模块,它们(上层模块和底层模块)都应该依赖于抽象。
- 抽象不应该依赖于细节,细节应该依赖于抽象。
优化的实现
而基于依赖反转原则,我们可以对上面的实现进行优化。
由于Person 属于高层模块,而Bike、Car 和 Train 属于底层模块。根据”上层模块不应该依赖底层模块,它们都应该依赖于抽象”,我们应该这样修改:
1 | public class Person { |
调用
1 | public class Test { |
分析
现在,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 接口)。
控制反转 (Inversion of Control)
控制反转(Inversion oc Control, IoC)是在面向对象编程中的一种思想,用来减少代码之间的耦合度。
按个人目前的理解,控制反转 (Inversion of Control)可以理解为依赖反转原则 (The Dependency Inversion Principle)概念的延伸,或者说在此基础上的进一步讨论。即,在依赖反转原则(”上层模块不应该依赖底层模块,它们都应该依赖于抽象“)的基础之上,控制反转还进一步去讨论了为对象进行实例化的控制问题。
之所以这么说,是因为在依赖反转原则的讨论上下文中,我们只关心类与类之间的耦合问题(或者说模块与模块之间的耦合问题);而在控制反转的讨论上下文中,我们在关心在类与类耦合问题的同时,还关心被耦合(被依赖)的类对应的对象在哪进行初始化。
更具体的来说,在控制反转思想中,控制指的是对一个具体的对象内部成员变量的实例化,而反转的是对内部成员变量的实例化过程的控制。
合在一起,就是一个具体的对象内部的成员变量的实例化不再由这个对象来负责(如果由这个对象来负责,则是正常的情况,或者称为正转),而是由一个被称为控制反转容器(IoC container)的实体负责。
从进行对象实例化操作的角度而言:
- 在正常的模式(或者说非控制反转的模式)中,通常由对象自身来完成对其内部成员变量的实例化(控制);
- 而在控制反转的场景中,对象内部的成员变量的实例化,由控制反转容器来完成。即开发者只需要将定义好的类(implementation)交给控制反转容器,控制反转容器会通过反射直接完成对象内部成员变量的实例化(当然,在此之前,开发者需要在特定位置(比如配置文件中),指定对象与对象之间的依赖关系)。
对象实例化场所 - 控制反转容器(IoC container)
我们注意到,在上面基于依赖反转原则进行讨论的例子中,我们只是简单地在主函数中完成Person类和具体交通工具类的实例化:
1 | public class Test { |
而事实上,对象实例化的场所可以不仅限于这种由程序员通过硬编码(hard code)来完成实例化的情况。
在控制反转的上下文中,包含一个控制反转容器(IoC container)的概念。控制反转容器是指在运行时对对象进行实例化的实体。
因此,控制反转容器专门负责对象的实例化工作。更具体地说,开发者只需要将定义好的类(implementation)交给控制反转容器,并且在特定位置(比如配置文件中)指定对象与对象之间的依赖关系。对应在上面的例子中,就是指定一个具体的Person对象具体使用哪种交通工具(比如Bike)。此后,控制反转容器会通过反射完成被依赖对象的对象实例化。
总结
总结来说:
- 控制是指某个对象对其”内部的成员变量的实例化”的控制;
- 被反转的是对对象实例化的控制;
- 反转是相对于正转而言的。在正转(正常的情况)时,在传统应用程序中,是由开发者自己在对象中显式地控制其内部对象(依赖对象)的实例化创建。
从控制反转 (Inversion of Control)到依赖注入 (Dependency Injection)
控制反转的概念并不是特别好理解,因为从字面上既没有表达出”是对什么的控制”,也没有表达出”相对于什么的反转”(或者说,什么情况是正转)。
Martin Fowler 在一篇经典文章《Inversion of Control Containers and the Dependency Injection pattern》中,为控制反转起了一个更准确表达其含义的名字,叫做依赖注入 (Dependency Injection)。
相对控制反转而言,“依赖注入”的确更加准确地描述了这种古老而又时兴的设计理念。从名字上理解,所谓依赖注入,即对象实例之间的具体依赖关系在特定位置(比如配置文件或以Java注解的形式)被指定,而控制反转容器负责在运行时,对对象进行实例化,并将被依赖的对象实例注入到对应的对象内部。
对应于上面的例子,假设传统的非依赖注入的实现是这样的(即这个Person对象依赖于Bike出现):
1 | Person p1 = new Person(new Bike()); |
采用依赖注入后,开发者仅需在特定位置指定这个Person对象需要Bike对象即可(而不需要显式地像上面代码这样对被依赖的具体的Vehicle进行实例化)。
实现方式
我们有不同的方法来实现控制反转:
- 服务定位器(service locator pattern)
- 依赖注入(dependency injection)
- 构造器注入(Constructor injection)
- Setter方法注入(Setter injection)
- 接口注入(Interface injection)
- contextualized lookup
- template method design pattern
- strategy design pattern
注意,依赖注入(dependency injection)只是实现控制反转思想的其中一种方式。
依赖注入模式 (Dependency Injection Pattern)
控制反转不等于依赖注入
对于广义上的控制反转而言,依赖注入模式只是实现控制反转思想的其中一种实现方式。因此,这自然也意味着我们还可以采用其他方法来实现控制反转,比如服务定位器模式(service locator pattern)。
然而,对于狭义上的控制反转,即控制反转等同于依赖注入。因为,我们通常都使用依赖注入模式来实现控制反转思想。
依赖注入的解释
假设类 A 对象中依赖一个类 B 对象,一般情况下,需要在类A的实现代码中显式的new一个类B的对象。
采用依赖注入技术之后,类A的实现代码只需要定义一个私有的B对象的引用,而不需要直接new来获得这个对象。控制反转容器会将类B对象实例化后,注入到一个类A对象的类B对象引用中。
对于开发者而言,唯一要做的,就是在特定位置(通常在配置文件或者通过注解的形式),指定类A对象依赖一个类 B对象。
Spring中的依赖注入
下面我们结合Spring的控制反转容器,简单描述一下这个过程。
1 | class MovieLister{ |
我们先定义两个类,可以看到都使用了依赖注入的方式,通过外部传入依赖,而不是自己创建依赖。那么问题来了,谁把依赖传给他们,也就是说谁负责创建finder引用指向的对象,并且把这个具体的MovieFinder对象传给MovieLister对象。
答案是Spring的控制反转容器。
要使用控制反转容器,首先要进行配置。这里我们使用XML的配置,也可以通过代码注解(Anotation)方式配置。
下面是spring.xml的内容:
1 | <beans> |
在Spring中,每个bean代表一个对象的实例,默认是单例模式,即在程序的生命周期内,所有的对象都只有一个实例,进行重复使用。通过配置bean,控制反转容器在启动的时候会根据配置生成bean实例。
这里,我们只需要知道,控制反转容器会根据XML中的配置,在运行时(runtime)创建一个特定的MovieFinder对象,并把这个特定的MovieFinder对象赋值给一个MovieLister对象的finder字段,最终完成依赖注入的过程。
测试代码
下面给出测试代码:
1 | public void testWithSpring() throws Exception { |
分析
- 根据配置生成ApplicationContext,即IoC容器;
- 从容器中获取MovieLister的实例。
实现依赖注入的方式
实现依赖注入有 4 种方式:
- 构造器中注入(Constructor injection)
- setter 方式注入(Setter injection)
- 接口注入(Interface injection)
- 注解注入
构造器注入(Constructor injection)
构造器注入(Constructor injection)是将需要的依赖作为构造函数的参数传递,已完成依赖注入。
方案1
1 | interface Energy { |
在上面的代码中,Car
不承担将Energy对象实例化的职能,而是将 energy
对象作为构造函数的一个参数传入。在调用Car
的构造函数前就已经初始化好了一个Energy 对象。
方案2
除此之外,还可以通过Java中注解的方式(且依赖一个依赖注入框架,如 Spring)以实现基于构造器的依赖注入。即,通过在字段声明前添加@Inject
以实现依赖对象的自动注入。
1 | interface Energy { |
好处
- 强依赖解耦:将强依赖之间解耦;
- 可移植性:在减轻了组件之间依赖关系的同时,也大大提高了组件的可移植性(同时,组件得到重用的机会更多);
- 便于测试:便于做Mock测试;
- 在一个 Person 对象实例化时的一开始,就创建好了依赖。
缺点
- 后期无法更改依赖。
setter方法注入(setter injection)
setter方法注入(setter injection)是指增加setter
方法,其参数为需要被注入的依赖对象。
1 | interface Energy { |
优点
Person 对象在运行过程中可以灵活地更改依赖。
缺点
Person 对象运行时,可能会存在依赖项为 null 的情况,所以需要检测依赖项的状态。
接口注入(Interface injection)
接口注入(Interface injection)是指为完成依赖注入创建一个接口,且接口中包含实现依赖注入的方法声明,依赖作为该方法的参数传入。
1 | interface EnergyConsumerInterface { |
总结
控制反转(IoC)是一种在软件工程中实现解耦合的思想,而依赖注入(DI)是一种具体的设计模式。
依赖注入是一种实现控制反转的具体方法,但除此之外还有其他方法可实现控制反转,。
服务定位器模式(Service Locator Pattern)
前面,我们曾经提到过,依赖注入模式只是实现控制反转思想的其中一种实现方式。因此,这自然也意味着我们还可以采用其他方法来实现控制反转,比如服务定位器模式(service locator pattern)。
Service Locator模式背后的基本思想是:有一个对象(即服务定位器)知道如何获得一个应用程序所需的所有服务。
也就是说,在我们的例子中,服务定位器应该有一个方法,用于获得一个MovieFinder实例。当然,这不过是把麻烦换了一个样子,我们仍然必须在MovieLister中获得服务定位器,最终得到的依赖关系如下图所示:
在这里,我把ServiceLocator类实现为一个Singleton的注册表,于是MovieLister就可以在实例化时通过ServiceLocator获得一个MovieFinder实例。
1 | class MovieLister... |
和注入的方式一样,我们也必须对服务定位器加以配置。在这里,我直接在代码中进行配置,但设计一种通过配置文件获得数据的机制也并非难事。
1 | class Tester... |
下面是测试代码:
1 | class Tester... |
依赖查找(Dependency Lookup)
依赖查找和依赖注入一样属于控制反转原则的具体实现,不同于依赖注入的被动接受,依赖查找这是主动请求,在需要的时候通过调用框架提供的方法来获取对象,获取时需要提供相关的配置文件路径、key等信息来确定获取对象的状态。
Reference
- Inversion of Control Containers and the Dependency Injection pattern - https://www.martinfowler.com/articles/injection.html
- IoC容器和Dependency Injection模式 - http://insights.thoughtworkers.org/injection/
- IoC Types - https://web.archive.org/web/20090615045650/http://docs.codehaus.org/display/PICO/IoC+Types
- 深度理解依赖注入(Dependence Injection)- http://www.cnblogs.com/xingyukun/archive/2007/10/20/931331.html
- 控制反转(IoC)与依赖注入(DI)- http://blog.xiaohansong.com/2015/10/21/IoC-and-DI/
- 说说依赖注入 - https://droidyue.com/blog/2015/06/13/talk-show-about-dependency-injection/
- IOC/DIP其实是一种管理思想 - https://coolshell.cn/articles/9949.html
- Dependency Inversion Principle - https://www.oodesign.com/dependency-inversion-principle.html
- 轻松学,浅析依赖倒置(DIP)、控制反转(IOC)和依赖注入(DI) - https://blog.csdn.net/briblue/article/details/75093382
- 控制反转(IoC)/依赖注入(DI)- http://wiki.jikexueyuan.com/project/spring-ioc/iocordi.html
- 控制反转(IoC)与依赖注入(DI) - https://www.jianshu.com/p/07af9dbbbc4b