上集,接着要说明如何运用 DI 来让刚才的范例程序具备执行时期切换实现类型的能力。

(本文摘自電子書《.NET 依賴注入》)

入门范例—DI 版本

为了让 AuthenticationService 类型能够在执行时期才决定要使用 EmailService 还是 ShortMessageService 来发送验证码,我们必须对这些类型动点小手术,把它们之间原本紧密耦合的关系松开——或者说「解耦合」。有一个很有效的工具可以用来解耦合:接口(interface)。

说得更明白些,原本 AuthenticationService 是相依于特定实现类型来发送验证码(如 EmailService),现在我们要让它相依于某个接口,而此接口会定义发送验证码的工作必须包含那些操作。由于接口只是一份规格,并未包含任何实现,故任何类型只要实现了这份规格,便能够与 AuthenticationService 衔接,完成发送验证码的工作。有了中间这层接口,开发人员便能够「针对接口、而非针对实现来编写程序。」(program to an interface, not an implementation)3,使应用程序中的各部组件保持「有点黏、又不会太黏」的适当距离,从而达成宽松耦合的目标。

提炼接口(Extract Interface)

开始动手修改吧!首先要对 EmailService 和 ShortMessageService 进行抽象化(abstraction),亦即将它们的共通特性抽离出来,放在一个接口中,使这些共通特性成为一份规格,然后再分别由具象类型来实现这份规格。以下代码是重构之后的结果,包含一个接口,两个实现类型。我在个别的 Send 方法中使用 Console.WriteLine 方法来输出不同的讯息字符串,方便观察实验结果(此范例是个 Console 类型的应用程序项目)。

interface IMessageService
{
void Send(User user, string msg);
} class EmailService : IMessageService
{
public void Send(User user, string msg)
{
// 发送电子邮件给指定的 user (略)
Console.WriteLine("发送电子邮件给用户,讯息内容:" + msg);
}
} class ShortMessageService : IMessageService
{
public void Send(User user, string msg)
{
// 发送短信给指定的 user (略)
Console.WriteLine("发送短信给用户,讯息内容:" + msg);
}
}

看图可能会更清楚些:

图 1-2:抽离出共通接口之后的类型图

接口抽离出来之后,如先前提过的,AuthenticationService 就可以依赖此接口,而不用再依赖特定实现类型。为了方便比对差异,我将修改前后的代码都一并列出来:

class AuthenticationService
{
// 原本是这样:
private ShortMessageService msgService; public AuthenticationService()
{
this.msgSevice = new ShortMessageService();
} // 現在改成这样:
private IMessageService msgService; public AuthenticationService(IMessageService service)
{
this.msgService = service;
}
}

修改前后的差异如下:

  • 私有成员 msgService 的类型:修改前是特定类型(EmailService 或 ShortMessageService),修改后是 IMessageService 接口。
  • 构造函数:修改前是直接建立特定类型的实例,并将对象参考指定给私有成员 msgService; 修改后则需要由外界传入一个 IMessageService 接口参考,并将此参考指定给私有成员 msgService。

控制反轉(IoC)

现在 AuthenticationService 已经不依赖特定实现了,而只依赖 IMessageService 接口。然而,接口只是规格,没有实现,亦即我们不能这么写(无法通过编译):

IMessageService msgService = new IMessageService();

那么对象从何而来呢?答案是由外界通过 AuthenticationService 的构造函数传进来。请注意这里有个重要意涵:非 DI 版本的 AuthenticationService 类型使用 new 运算符来建立特定讯息服务的对象,并控制该对象的生命周期;DI 版本的 AuthenticationService 则将此控制权交给外层调用端(主程序)来负责——换言之,依赖性被移出去了,「控制反转了」。

最后要修改的是主程序(MainApp):

class MainApp
{
public void Login(string userId, string pwd, string messageServiceType)
{
IMessageService msgService = null; // 用字符串比对的方式来决定该建立哪一种讯息服务对象。
switch (messageServiceType)
{
case "EmailService":
msgService = new EmailService();
break;
case "ShortMessageService":
msgService = new ShortMessageService();
break;
default:
throw new ArgumentException("无效的讯息服务类型!");
} var authService = new AuthenticationService(msgService); // 注入相依对象
if (authService.TwoFactorLogin(userId, pwd))
{
// 此处没有变动,故省略.
}
}
}

现在主程序会负责建立讯息服务对象,然后在建立 AuthenticationService 对象时将讯息服务对象传入其构造函数。这种由调用端将相依对象通过构造函数注入至另一个对象的作法是 DI 的一种常见写法,而这写法也有个名称,叫做「构造函数注入」(Constructor Injection)。「构造函数注入」是实现 DI 的一种方法,第 2 章会进一步介绍。

现在各类型之间的依赖关系如下图所示。请与上一集的第一张图比较一下两者的差异(为了避免图中箭头过于复杂交错,我把无关紧要的配角 User 类型拿掉了) 。

 
圖 1-3:改成 DI 版本之后的类型依赖关系图
 

你会发现,上一集的图中的依赖关系,是上层依赖下层的方式;或者说,高阶模块依赖低阶模块。这只符合了先前提过的 S.O.L.I.D. 五项原则中的「依赖倒置原则」(Dependency Inversion Principle;DIP)的其中一小部分的要求。DIP 指的是:

  • 高阶模块不应依赖低阶模块;他们都应该依赖抽象层(abstractions)。
  • 抽象层不应依赖实现细节;实现细节应该依赖抽象层。

而从图 1-3 可以发现,DI 版本的范例程序已经符合「依赖倒置原则」。其中的 IMessageService 接口即属于抽象层,而高阶模块 AuthenticationService 和低阶模块皆依赖中间这个抽象层。

此 DI 版本的范例程序有个好处,即万一将来使用者又提出新的需求,希望传送验证码的方式除了 e-mail 和简讯之外,还要增加移动设备平台的信息推送服务(push notification),以便将验证码推送至行动 app。此时只要加入一个新的类型(可能命名为 PushMessageService),并让此类型实现 IMessageService,然后稍微改一下 MainApp 便大致完工,AuthenticationService 完全不需要修改。简单地说,应用程序更容易维护了。

当然,这个范例的程序写法还是有个缺点:它是用字符串比对的方式来决定该建立哪一种讯息服务对象。想象一下,如果欲支持的讯息服务类型有十几种,那个 switch...case 区块不显得太冗长累赘吗?如果有一个专属的对象能够帮我们简化这些类型对应以及建立对象的工作,那就更理想了。这个部分会在第 3 章〈DI 容器〉中进一步说明。

何时该用 DI?

一旦你开始感受到宽松耦合的好处,在设计应用程序时,可能会倾向于让所有类型之间的耦合都保持宽松。换言之,碰到任何需求都想要先定义接口,然后通过依赖注入的方式(例如先前范例中使用的「构造函数注入」)来建立对象之间的依赖关系。然而,天下没有白吃的午餐,宽松耦合也不例外。每当你将类型之间的依赖关系抽离出来,放到另一个抽象层,再于特定时机注入相依对象,这样的动作其实多少都会产生一些额外成本。不管三七二十一,将所有对象都设计成可任意替换、随时插拔,并不是个好主意。

以 .NET 基础类库(Base Class Library;简称 BCL)为例,此类库包含许多组件,各组件又包含许多现成的类型,方便我们直接取用。每当你在程序中使用 BCL 的类型,例如 String、DateTime、Hashtable 等等,就等于在程序中加入了对这些类型的依赖。此时,你会担心有一天自己的程序可能需要把这些 BCL 类型替换成别的类型吗?如果是从网络上找到的开放源代码呢?答案往往取决于你对于特定类型/组件是否会经常变动的信心程度;而所谓的「经常变动」,也会依应用程序的类型、大小而有不同的定义。

相较于其他在网络上找到或购买的第三方组件,我想多数人都会觉得 .NET BCL 里面的类型应该会相对稳定得多,亦即不会随便改来改去,导致现有程序无法编译或正常执行。这样的认知,有一部分可能来自于我们对该类型的提供者(微软)有相当程度的信心,另一部分则是来自以往的经验。无论如何,在为应用程序加入第三方组件时,最好还是审慎评估之后再做决定。

以下是几个可能需要使用或了解 DI 技术的场合:

  • 如果你正在设计一套框架(framework)或可重复使用的类库,DI 会是很好用的技术。
  • 如果你正在开发应用程序,需要在执行时其动态加载、动态切换某些组件,DI 也能派上用场。
  • 希望自已的代码在将来需求变动时,能够更容易替换掉其中一部份不稳定的组件(例如第三方组件,此时可能搭配 Adapter 模式使用)。
  • 你正在接手维护一个应用程序,想要在重构(refactor)代码的时候降低对某些组件的依赖,方便测试以及让代码更好维护。

以下是一些可能不适合、或应该更谨慎使用 DI 的场合:

  • 在小型的、需求非常单纯的应用程序中使用 DI,恐有杀鸡用牛刀之嫌。
  • 在大型且复杂的应用程序中,如果到处都是宽松耦合的接口、到处都用 DI 注入相依对象,对于后续接手维护的新进成员来说可能会有点辛苦。在阅读代码的过程中,他可能会因为无法确定某处使用的对象究竟是哪个类型而感到挫折。比如说,看到代码中调用 IMessageService 接口的 Send 方法,却没法追进去该方法的实现来了解接着会发生什么事,因为接口并没有提供任何实现。若无人指点、也没有文件,每次碰到疑问时就只能以单步调试的方式来了解程序实际运行的逻辑,这确实需要一些耐心。
  • 对老旧程序进行重构(refactoring)时,可能会因为现有设计完全没考虑宽松耦合,使得引入 DI 困难重重。

总之,不加思索地使用任何技术总是不好的;没有银弹。

-------- 
 
 

Dependency Injection 筆記 (2)的更多相关文章

  1. Dependency Injection 筆記 (3)

    续上集.接着要来进一步了解的是 DI 的实现技术,也就是注入相依对象的方式.这里介绍的依赖注入方式,又称为「穷人的 DI」(poor man’s DI),因为这些用法都与特定 DI 工具无关,亦即不使 ...

  2. Dependency Injection 筆記 (1)

    <.NET 依賴注入>連載 (1) 本文从一个基本的问题开始,点出软件需求变动的常态,以说明为什么我们需要学习「依赖注入」(dependency injection:简称 DI)来改善设计 ...

  3. Dependency Injection 筆記 (4)

    续上集未完的相关设计模式... (本文摘自電子書:<.NET 依賴注入> Composite 模式 延续先前的电器比喻.现在,如果希望 UPS 不只接计算机,还要接电风扇.除湿机,可是 U ...

  4. Dependency Injection

    Inversion of Control - Dependency Injection - Dependency Lookup loose coupling/maintainability/ late ...

  5. Ninject学习(一) - Dependency Injection By Hand

    大体上是把官网上的翻译下而已. http://www.ninject.90iogjkdcrorg/wiki.html Dependency Injection By Hand So what's Ni ...

  6. MVC Controller Dependency Injection for Beginners【翻译】

    在codeproject看到一篇文章,群里的一个朋友要帮忙我翻译一下顺便贴出来,这篇文章适合新手,也算是对MEF的一个简单用法的介绍. Introduction In a simple stateme ...

  7. 控制反转Inversion of Control (IoC) 与 依赖注入Dependency Injection (DI)

    控制反转和依赖注入 控制反转和依赖注入是两个密不可分的方法用来分离你应用程序中的依赖性.控制反转Inversion of Control (IoC) 意味着一个对象不会新创建一个对象并依赖着它来完成工 ...

  8. [转载][翻译] IoC 容器和 Dependency Injection 模式

    原文地址:Inversion of Control Containers and the Dependency Injection pattern 中文翻译版本是网上的PDF文档,发布在这里仅为方便查 ...

  9. Inversion of Control Containers and the Dependency Injection pattern(转)

    In the Java community there's been a rush of lightweight containers that help to assemble components ...

随机推荐

  1. MVVM、MVVMLight、MVVMLight Toolkit之我见

    原文:MVVM.MVVMLight.MVVMLight Toolkit之我见 我想,现在已经有不少朋友在项目中使用了MVVMLight了吧,如果你正在做WPF,Silverlight,Windows ...

  2. java获取本机IP地址,非127.0.0.1

    综合了网上找的代码,整理的,Windows和Linux都可以用. private static String getHostIp(){ try{ Enumeration<NetworkInter ...

  3. Linux虚拟文件系统(VFS)学习

    虚拟文件系统(Virtual Filesystem)也可称之为虚拟文件系统转换(Virtual Filesystem Switch),是一个内核软件层,用来处理与Unix标准文件系统相关的全部系统调用 ...

  4. .net程序运行流程

    程序员用.net开发的程序要在计算机上运行,首先程序经过编译后,会生成机器指令,一般以一个文件的形式保存,这个文件在外存储器上(存储器分外存与内存.外存:硬盘,U盘等:) 然后cpu会把硬盘上的文件读 ...

  5. 学习 NLP(一)—— TF-IDF

    TF-IDF(Term Frequency & Inverse Document Frequency),是一种用于信息检索与数据挖掘的常用加权技术.它的主要思想是:如果某个词或短语在一篇文章中 ...

  6. Win7 32bit下一个hadoop2.5.1源代码编译平台的搭建各种错误遇到

    从小白在安装hadoop困难和错误时遇到说起,同时,我们也希望能得到上帝的指示. 首先hadoop更新速度非常快,最新的是hadoop2.5.1,因此就介绍下在安装2.5.1时遇到的各种困难. 假设直 ...

  7. ocjp(scjp) 的官网样题收录-20130723

    官网上给的样题很少,带(*)的为正确答案. OBJECTIVE: 1.5: Given a code example, determine if a method is correctly overr ...

  8. 简明Python3教程 18.下一步是什么

    如果你有认真通读本书之前的内容并且实践其中包含的大量例程,那么你现在一定可以熟练使用python了. 同时你可能也编写了一些程序用于验证python特性并提高你的python技能.如果还没有这样做的话 ...

  9. 数学公式的规约(reduce)和简化(simplify)

    to simplify notation, 1. 增广(augment) xi=[xi;1],减少一个常数项: 2. 多个求和号 ∥x∥2=xTx 向量 ⇒ 矩阵: 求和号本身也可化为向量矩阵运算: ...

  10. html5 命运之轮生产

    码,如以下: <%@ page language="java" contentType="text/html; charset=UTF-8" pageEn ...