深入理解DIP、IoC、DI以及IoC容器
摘要
面向对象设计(OOD,Object-Oriented Design)有助于我们开发出高性能、易扩展以及易复用的程序.
其中OOD有一个重要的思想那就是依赖倒置原则(DIP,Dependence Inversion Principle),并由此引申出IoC、DI以及IoC容器等概念.
目录
前言
简单概念:
依赖倒置原则(DIP):
一种软件架构设计的原则(抽象概念)
控制翻转(IoC):
一种翻转流、依赖和接口的方式(DIP的具体实现方式)
依赖注入(DI):
IoC的一种实现方式,用来反转依赖(IoC的具体实现方式)
IoC容器:
依赖注入的框架,用来映射依赖,管理对象创建和生存周期(DI框架)
依赖倒置原则(DIP)
依赖倒置原则,它转换了依赖,高层模块不依赖于低层模块的实现,而低层模块依赖于高层模块定义的接口.
通俗的讲,就是高层模块定义接口,低层模块负责实现
Bob Martins对DIP的定义:
高层模块不应依赖于低层模块,两者应该依赖于抽象.
抽象不不应该依赖于实现,实现应该依赖于抽象.
场景一 依赖无倒置(低层模块定义接口,高层模块负责实现)
从上图中,我们发现高层模块的类依赖于低层模块的接口.
因此,低层模块需要考虑到所有的接口.
如果有新的低层模块类出现时,高层模块需要修改代码,来实现新的低层模块的接口.
这样,就破坏了开放封闭原则.
场景二 依赖倒置(高层模块定义接口,低层模块负责实现)
在这个图中,我们发现高层模块定义了接口,将不再直接依赖于低层模块,低层模块负责实现高层模块定义的接口.
这样,当有新的低层模块实现时,不需要修改高层模块的代码.
由此可以总结出使用DIP的优点:
系统更柔韧:
可以修改一部分代码而不影响其他模块.
系统更健壮:
可以修改一部分代码而不会让系统奔溃.
系统更高效:
组件松耦合,且可复用,提高开发效率.
控制反转(IoC)
- DIP是一种软件设计原则,它仅仅告诉你两个模块之间应该如何依赖,但是它并没有告诉你如何做.
- IoC则是一种软件设计模式,它告诉你应该如何做,来解除相互依赖模块的耦合.控制反转(IoC),它为相互依赖的组件提供抽象,将依赖(低层模块)对象的获得交给第三方(系统)来控制,即依赖对象不在被依赖模块的类中直接通过new来获取.
软件设计原则: 原则为我们提供指南,它告诉我们什么是对,什么是错的.它不会告诉我们如何解决问题.它仅仅给出一些准则,以便我们可以设计好的软件,避免不良的设计.一些常见的原则,比如DRY(Don’t Repeat Yourself)、OCP(Open Closed Principle)、DIP(Dependency Inversion Principle) 等.
软件设计模式: 模式是在软件开发过程中总结得出的一些可重用的解决方案,它能解决一些实际的问题.一些常见的模式,比如工厂模式、单例模式 等等.
已手机为例
手机类
1 | public class iPhone6 |
使用者类
1 | public class User |
测试
1 | static void Main(string[] args) |
那么如果我手机坏了需要换成iPhoneX怎么办?
重新定义一个iPhoneX类
1 | public class iPhoneX |
由于User中直接引用的iPhone6这个类,所以还需要修改引用,替换成iPhoneX
使用者类
1 | public class User |
那么如果我再次换手机?又需要修改代码.
显然,这不是一个良好的设计,组件之间高度耦合,可扩展性较差,它违背了DIP原则.
高层模块User类不应该依赖于低层模块iPhone6,iPhoneX,两者应该依赖于抽象.
IoC有2种常见的实现方式:依赖注入和服务定位.
其中依赖注入(DI)使用最为广泛.
依赖注入(DI)
控制反转(IoC)一种重要的方式,就是将依赖对象的创建和绑定转移到被依赖对象类的外部来实现. 在上述的实力中,User类所依赖的对象iPhone6的创建和绑定是在User类内部进行的.
事实证明,这种方法并不可取.
既然,不能再User类内部直接绑定依赖关系,那么如何将iPhone对象的引用传递给User类使用?
依赖注入(DI,Dependency Injection),它提供一种机制,将需要依赖(低层模块)对象的引用传递给被依赖(高层对象)对象 .通过DI,我们可以在User类的外部将iPhone对象的引用传递给Order类对象.
方法一构造函数注入
构造函数函数注入,通过构造函数传递依赖.
因此,构造函数的参数必然用来接收一个依赖对象.
那么参数的类型是什么?
具体依赖对象的类型?
还是一个抽象类型?
根据DIP原则,我们知道高层模块不应该依赖于低层模块,两者应该依赖于抽象.
那么构造函数的参数应该是一个抽象类型.
回到上面的问题,如何将iPhone对象的引用传递给User类使用?
首先,我们要定义一个iPhone的抽象类型IPhone,并声明SendMessage方法.
1 | public interface IPhone |
然后在iPhone6类中,实现IPhone接口.
1 | public class iPhone6 : IPhone |
接下来,我们还需要修改User类
1 | public class User |
测试一下
1 | static void Main(string[] args) |
从上面我们可以看出,我们将依赖对象iPhone6对象的创建和绑定转移到User类外部来实现.这样就解除了iPhone6和User类的耦合关系.
当我们换手机的时候,只需要重新定义一个iPhoneX类,然后外部重新绑定,不需要修改User类内部代码.
定义iPhoneX类
1 | public class iPhoneX : IPhone |
重新绑定依赖关系:
1 | static void Main(string[] args) |
我们不需要修改User类的代码,就完成了换手机这一流程,提现了IoC的精妙之处.
方法二属性注入
属性注入是通过属性来传递依赖的,
因此我们需要在User类中定义一个属性:
1 | public class User |
测试代码
1 | static void Main(string[] args) |
方法三接口注入
相比构造函数注入和属性注入,接口注入显得有些复杂,使用也不常见.
具体思路是先定义一个接口,包含一个设置依赖的方法.
然后依赖类,继承并实现这个接口.
首先定义一个接口:
1 | public interface IDependent |
依赖类实现这个接口:
1 | public class User : IDependent |
通过SetDependence()方法传递依赖:
1 | static void Main(string[] args) |
IoC容器
前面所有的例子中,我们都是通过手动的方式来创建依赖对象,并将引用传递给被依赖模块.
比如:
1 | iPhone6 iphone = new iPhone6(); |
对于大型项目来说,相互依赖的组件比较多.
如果还用手动的方式,自己来创建和注入依赖的话,显然效率很低,而且往往还会出现不可控的场面.
正因如此,IoC容器诞生了.
IoC容器实际上是一个DI框架,它能简化我们的工作量.它包含以下几个功能:
- 动态创建、注入依赖对象.
- 管理对象生命周期.
- 映射依赖关系.
比较流行的IoC容器有以下几种:
Ninject: http://www.ninject.org/
Castle Windsor: http://www.castleproject.org/container/index.html
Autofac: http://code.google.com/p/autofac/
StructureMap: http://docs.structuremap.net/
Unity: http://unity.codeplex.com/
Spring.NET: http://www.springframework.net/
LightInject: http://www.lightinject.net/
以Ninject为例,来实现方法一构造函数注入的功能.
首先在项目中添加Ninject程序集
1 | using Ninject; |
然后,IoC容器注册绑定依赖:
1 | StandardKernel kernel = new StandardKernel(); |
接下来,我们获取User对象(已经注入了依赖对象)
1 | User user = kernel.Get<User>(); |
测试一下
1 | static void Main(string[] args) |
总结
DIP是软件设计的一种思想,IoC则是基于DIP衍生出的一种软件设计模式.
DI是IoC的具体实现方式之一,使用最为广泛.
IoC容器是DI构造函数注入的框架,它管理着依赖项的生命周期及映射关系.