摘要

面向对象设计(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
2
3
4
5
6
7
public class iPhone6
{
public void SendMessage()
{
Console.WriteLine("发送一条消息");
}
}

使用者类

1
2
3
4
5
6
7
8
9
public class User
{
private readonly iPhone6 phone = new iPhone6();

public void SendMessage()
{
phone.SendMessage();
}
}

测试

1
2
3
4
5
6
7
8
static void Main(string[] args)
{
User user = new user();
user.SendMessage();

Console.Read();
}


那么如果我手机坏了需要换成iPhoneX怎么办?

重新定义一个iPhoneX类

1
2
3
4
5
6
7
public class iPhoneX
{
public void SendMessage()
{
Console.WriteLine("发送一条消息");
}
}

由于User中直接引用的iPhone6这个类,所以还需要修改引用,替换成iPhoneX

使用者类

1
2
3
4
5
6
7
8
9
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方法.

1
2
3
4
public interface IPhone
{
void SendMessage();
}

然后在iPhone6类中,实现IPhone接口.

1
2
3
4
5
6
7
public class iPhone6 : IPhone
{
public void SendMessage()
{
Console.WriteLine("发送消息!");
}
}

接下来,我们还需要修改User类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class User
{
private IPhone _iphone;

public User(IPhone iPhone)
{
_iphone = iPhone;
}

public void SendMessage()
{
_iphone.SendMessage();
}
}

测试一下

1
2
3
4
5
6
7
8
9
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类

1
2
3
4
5
6
7
public class iPhoneX : IPhone
{
public void SendMessage()
{
Console.WriteLine("iPhoneX发送消息!");
}
}

重新绑定依赖关系:

1
2
3
4
5
6
7
8
9
static void Main(string[] args)
{
iPhoneX _iphone = new iPhoneX();
User _user = new User(_iphone);

_user.SendMessage();

Console.Read();
}

我们不需要修改User类的代码,就完成了换手机这一流程,提现了IoC的精妙之处.

方法二属性注入

属性注入是通过属性来传递依赖的,
因此我们需要在User类中定义一个属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class User
{
private IPhone _iphone;

public IPhone IPhone
{
set
{
_iphone = value;
}

get
{
return _iphone;
}
}

public void SendMessage()
{
_iphone.SendMessage();
}
}

测试代码

1
2
3
4
5
6
7
8
9
10
11
static void Main(string[] args)
{
iPhone6 _iPhone = new iPhone6();

User _user = new User();
_user.IPhone = _iPhone;

_user.SendMessage();

Console.Read();
}

方法三接口注入

相比构造函数注入和属性注入,接口注入显得有些复杂,使用也不常见.
具体思路是先定义一个接口,包含一个设置依赖的方法.
然后依赖类,继承并实现这个接口.

首先定义一个接口:

1
2
3
4
5
public interface IDependent
{
// 设置依赖项
void SetDependence(IPhone _iphone);
}

依赖类实现这个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class User : IDependent
{
private IPhone _iphone;

public void SetDependence(IPhone _iphone)
{
this._iphone = _iphone;
}

public void SendMessage()
{
_iphone.SendMessage();
}
}

通过SetDependence()方法传递依赖:

1
2
3
4
5
6
7
8
9
10
static void Main(string[] args)
{
iPhone6 _iphone = new iPhone6();
User _user = new User();
_user.SetDependence(_iphone);

_user.SendMessage();

Console.Read();
}

IoC容器

前面所有的例子中,我们都是通过手动的方式来创建依赖对象,并将引用传递给被依赖模块.
比如:

1
2
iPhone6 iphone = new iPhone6();
User user = new User(iphone);

对于大型项目来说,相互依赖的组件比较多.
如果还用手动的方式,自己来创建和注入依赖的话,显然效率很低,而且往往还会出现不可控的场面.
正因如此,IoC容器诞生了.
IoC容器实际上是一个DI框架,它能简化我们的工作量.它包含以下几个功能:

  • 动态创建、注入依赖对象.
  • 管理对象生命周期.
  • 映射依赖关系.

比较流行的IoC容器有以下几种:

  1. Ninject: http://www.ninject.org/

  2. Castle Windsor: http://www.castleproject.org/container/index.html

  3. Autofac: http://code.google.com/p/autofac/

  4. StructureMap: http://docs.structuremap.net/

  5. Unity: http://unity.codeplex.com/

  6. Spring.NET: http://www.springframework.net/

  7. LightInject: http://www.lightinject.net/

以Ninject为例,来实现方法一构造函数注入的功能.

首先在项目中添加Ninject程序集

1
using Ninject;

然后,IoC容器注册绑定依赖:

1
2
3
4
StandardKernel kernel = new StandardKernel();

// 注册依赖
kernel.Bind<IPhone>().To<IPhone6>().

接下来,我们获取User对象(已经注入了依赖对象)

1
User user = kernel.Get<User>();

测试一下

1
2
3
4
5
6
7
8
9
10
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构造函数注入的框架,它管理着依赖项的生命周期及映射关系.