笔者通常希望应用程序中的组件尽可能独立,而只有很少几个可控的依赖项。—— 在理想情况下,每个组件都不了解其他组件,而只是通过抽象接口来处理应用程序的其他区域。这称为松耦合 。—— 它能够使应用程序更易于测试和修改。
当我们需要编写或者引用一个组件来实现一系列功能时(比如编写一个名称为“MyEmailSender”的组件,用以发送邮件信息),我们可以定义一个接口,它包含了发送邮件所需的所有 public 函数(这个接口可以命名为 “IEmailSender”)。
(IEmailSender 是一个接口,MyEmailSender 是该接口的一个具体实现类)
应用程序中任何需要发送电子邮件的部分(即“应用场景”,比如名称为 “PasswordResetHelper” 的“密码重置辅助程序”),只要通过引用该接口中的方法便可以发送一份邮件。
通过引入 IEmailSender,保证了 PasswordResetHelper 与 MyEmailSender 之间没有直接的依赖项。
这样的好处是,笔者完全可以用另一个邮件发送程序来替换 MyEmailSender,甚至用一个模仿实现以进行测试,而无须对 PasswordResetHelper 做任何修改。
3.3.1 使用依赖项注入
接口有助于解除组件耦合,但这里仍面临一个问题:
PasswordResetHelper 类要通过 IEmailSender 接口来配置并发送电子邮件,总归需要创建一个实现该接口的对象(必须创建一个 MyEmailSender 的实例)。但 C# 并未提供内置的方法以方便地创建实现接口的对象,除非以 new 关键字创建一个具体组件的实例。于是,代码如下:
public class PasswordResetHelper
{
public void ResetPassword()
{
IEmailSender mySender = new MyEmailSender();
//调用接口方法,以配置 email 细节
mySender.SendEmail();
}
}
这破坏了无须修改 PasswordResetHelper 就能替换 MyEmailSender 的目的,这意味着此刻仅处于组件松耦合的半途。
现在需要有一种办法,它能够获取实现某接口的对象,而不必直接创建该对象。—— 这一问题的解决方法称为依赖项注入(DI),也称为控制反转(IoC)。
DI 是一种实现组件解耦的设计模式。(也是有效从事 MVC 开发的一个重要概念。同时,DI 也可能会造成很多困惑)
DI 模式有两个部分:
1. 打断和声明依赖项
第一个部分是从组件(此例中的 PasswordResetHelper)中去除掉对具体类的依赖项。其做法是创建一个以所需接口的实现作为其参数的类构造器(构造方法)。如下所示:
public class PasswordResetHelper
{
private IEmailSender emailSender ; public PasswordResetHelper (IEmailSender emailSenderParam)
{
emailSender = emailSenderParam ;
} public void ResetPassword ()
{
emailSender.SendEmail();
}
}
现在可以将 PasswordResetHelper 类的构造器(构造方法)称为对 IEmailSender 接口声明了一个依赖项。
(除非接受一个实现了 IEmailSender 接口的对象,否则便不能创建和使用它 —— PasswordResetHelper 类)
在依赖项声明中,PasswordResetHelper 类不再有 MyEmailSender 的任何知识,它仅仅依赖于 IEmailSender 接口。简言之,PasswordResetHelper 不再了解或关心如何实现 IEmailSender 接口。
2. 注射依赖项
DI 模式的第二个部分是在创建 PasswordResetHelper 类的实例时,注入由其声明的依赖项。(即,在调用 PasswordResetHelper 的构造方法创建 PasswordResetHelper 类的实例时,把依赖项 —— MyEmailSender 的实例放进 PasswordResetHelper 的构造方法的参数里面去)。故称为依赖项注入。
(其实这也很好理解:第一步你要声明说这里需要放一个什么,那么第二步当然就是把它放进去啦!)—— 这种依赖项是在运行时处理的。
上述 PasswordResetHelper 类是通过构造器(构造方法/构造函数)来声明其依赖项的,这称为 “构造器注入”。
也可以通过一个 public 属性来声明要注入的依赖项,这称为 “设置器注入”,意即在该属性的 set 代码块中声明依赖项。
3.2.2 使用依赖项注入容器
至此已解决了依赖项问题,但按照现在的情况,在应用程序的某个地方仍然需要以下这些语句。
IEmailSender sender = new MyEmailSender();
helper = new PasswordResetHelper(sender);
如何在无须在应用程序的某个其他地方创建依赖项而对接口的具体实现进行实例化呢? —— 答案是使用 “依赖项注入容器(简称 DI 容器)”。
DI 容器是一种组件,它在类所声明的依赖项和用来解决这些依赖项的类( PasswordResetHelper 和 MyEmailSender )之间充当着中间件的角色。
可以用这种 DI 容器注册一组应用程序要使用的接口或抽象类型,并指明满足依赖项所需实例化的实现类。
因此在上例中,便会用 DI 容器注册 IEmailSender 接口,并指明在需要实例化 IEmailSender 时,应该创建一个 MyEmailSender 的实例。
(这里讲的就是第二步 —— 依赖项注入,所以重点在 IEmailSender 和 MyEmailSender )
当应用程序中需要一个 PasswordResetHelper 对象时,便要求 DI 容器去创建一个。
DI 容器知道 PasswordResetHelper 已经声明了一个关于 IEmailSender 接口的依赖项,而且知道已经将 MyEmailSender 类指定为用于该接口的实现。
DI 容器会将这两项信息结合在一起,从而创建 MyEmailSender 对象,然后用它作为创建 PasswordResetHelper 对象的一个参数,于是在应用程序中便可以使用这个 MyEmailSender 了。
重要的是要注意到:应用程序中已经不需要自己动手使用 new 关键字去创建这种对象了,而是进入 DI 容器请求所需的对象)
(DI 新手可能还需要一段时间去适应,但后面会看到,MVC 框架提供了一些特性,可以使该过程更加简单)
我们不需要自己去编写 DI 容器,有一些很棒的开源代码和免费的许可实现是可用的。
笔者所喜欢的并在自己的项目中使用的叫做 Ninject,可以在 http://www.ninject.org 上获得其细节。(第 6 章将介绍 Ninject 的使用,并演示如何用 NuGet 安装这个包)
微软公司创建了自己的 DI容器,叫做 Unity .(如果需要了解更多关于 Unity 的信息,可参阅 unity.codeplex.com)
DI 容器的作用似乎简单而平常,但事实并非如此。—— 一个好的 DI 容器,如 Ninject,有一些聪明的特性。
1、依赖链解析:
如果 MyEmailSender 类的构造器需要一个 INetworkTransport 接口的实现,DI 容器将实例化这个接口的默认实现,把它传递给 MyEmailSender 的构造器,并将返回结果作为 IEmailSender 的默认实现。)
2、对象生命周期管理:
一个好的 DI 容器能让你配置组件的生命周期,允许你从预定义的选项中进行选择。这些选项包括……(但是这里并没有介绍为什么要进行这个设置)
3、构造器参数值的配置:
如果 INetworkTransport 接口实现的构造器需要一个叫做 serverName 的字符串,你应该能够在 DI 容器的配置中为其设置一个值。
这是一种笨拙但简单的配置系统,它不需要你的代码传递诸如连接字符串、服务器地址等参数。
最后,编写自己的 DI 容器是理解 C# 和 .NET 如何处理类型及反射的一种很好的方式,而且建议将其作为闲暇而无事可做时的一个好项目。
但切不要试图在一个实际的项目中部署你写的这些代码。—— 编写可靠、健壮且高性能的 DI 容器是困难的,你应该找一个已经过验证且测试过的包来使用。