摘要
面向对象设计(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) 等.
软件设计模式: 模式是在软件开发过程中总结得出的一些可重用的解决方案,它能解决一些实际的问题.一些常见的模式,比如工厂模式、单例模式 等等.
已手机为例
手机类
public class iPhone6
{
public void SendMessage()
{
Console.WriteLine("发送一条消息");
}
}
使用者类
public class User
{
private readonly iPhone6 phone = new iPhone6();
public void SendMessage()
{
phone.SendMessage();
}
}
测试
static void Main(string[] args)
{
User user = new user();
user.SendMessage();
Console.Read();
}
那么如果我手机坏了需要换成iPhoneX怎么办?
重新定义一个iPhoneX类
public class iPhoneX
{
public void SendMessage()
{
Console.WriteLine("发送一条消息");
}
}
由于User中直接引用的iPhone6这个类,所以还需要修改引用,替换成iPhoneX
使用者类
public class User
{
private readonly iPhoneX phone = new iPhoneX();
public void SendMessage()
{
phone.SendMessage();
}
}
那么如果我再次换手机?又需要修改代码.
显然,这不是一个良好的设计,组件之间高度耦合,可扩展性较差,它违背了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方法.
public interface IPhone
{
void SendMessage();
}
然后在iPhone6类中,实现IPhone接口.
public class iPhone6 : IPhone
{
public void SendMessage()
{
Console.WriteLine("发送消息!");
}
}
接下来,我们还需要修改User类
public class User
{
private IPhone _iphone;
public User(IPhone iPhone)
{
_iphone = iPhone;
}
public void SendMessage()
{
_iphone.SendMessage();
}
}
测试一下
static void Main(string[] args)
{
iPhone6 _iphone = new iPhone6();
User _user = new User(_iphone);
_user.SendMessage();
Console.Read();
}
从上面我们可以看出,我们将依赖对象iPhone6对象的创建和绑定转移到User类外部来实现.这样就解除了iPhone6和User类的耦合关系.
当我们换手机的时候,只需要重新定义一个iPhoneX类,然后外部重新绑定,不需要修改User类内部代码.
定义iPhoneX类
public class iPhoneX : IPhone
{
public void SendMessage()
{
Console.WriteLine("iPhoneX发送消息!");
}
}
重新绑定依赖关系:
static void Main(string[] args)
{
iPhoneX _iphone = new iPhoneX();
User _user = new User(_iphone);
_user.SendMessage();
Console.Read();
}
我们不需要修改User类的代码,就完成了换手机这一流程,提现了IoC的精妙之处.
方法二属性注入
属性注入是通过属性来传递依赖的,
因此我们需要在User类中定义一个属性:
public class User
{
private IPhone _iphone;
public IPhone IPhone
{
set
{
_iphone = value;
}
get
{
return _iphone;
}
}
public void SendMessage()
{
_iphone.SendMessage();
}
}
测试代码
static void Main(string[] args)
{
iPhone6 _iPhone = new iPhone6();
User _user = new User();
_user.IPhone = _iPhone;
_user.SendMessage();
Console.Read();
}
方法三接口注入
相比构造函数注入和属性注入,接口注入显得有些复杂,使用也不常见.
具体思路是先定义一个接口,包含一个设置依赖的方法.
然后依赖类,继承并实现这个接口.
首先定义一个接口:
public interface IDependent
{
// 设置依赖项
void SetDependence(IPhone _iphone);
}
依赖类实现这个接口:
public class User : IDependent
{
private IPhone _iphone;
public void SetDependence(IPhone _iphone)
{
this._iphone = _iphone;
}
public void SendMessage()
{
_iphone.SendMessage();
}
}
通过SetDependence()方法传递依赖:
static void Main(string[] args)
{
iPhone6 _iphone = new iPhone6();
User _user = new User();
_user.SetDependence(_iphone);
_user.SendMessage();
Console.Read();
}
IoC容器
前面所有的例子中,我们都是通过手动的方式来创建依赖对象,并将引用传递给被依赖模块.
比如:
iPhone6 iphone = new iPhone6();
User user = new User(iphone);
对于大型项目来说,相互依赖的组件比较多.
如果还用手动的方式,自己来创建和注入依赖的话,显然效率很低,而且往往还会出现不可控的场面. 正因如此,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程序集
using Ninject;
然后,IoC容器注册绑定依赖:
StandardKernel kernel = new StandardKernel();
// 注册依赖
kernel.Bind<IPhone>().To<IPhone6>().
接下来,我们获取User对象(已经注入了依赖对象)
User user = kernel.Get<User>();
测试一下
static void Main(string[] args)
{
StandardKernel kernel = new StandardKernel();// 创建Ioc容器
kernel.Bind<IPhone>().To<IPhone6>();// 注册依赖
User user = kernel.Get<User>();// 获取目标对象
user.SendMessage();
Console.Read();
}
总结
DIP是软件设计的一种思想,IoC则是基于DIP衍生出的一种软件设计模式.
DI是IoC的具体实现方式之一,使用最为广泛.
IoC容器是DI构造函数注入的框架,它管理着依赖项的生命周期及映射关系.
💡本文非原创,转载自:https://asuka4every.top/DIP-IoC-DI-and-IoC-containers/