本文大纲:

  • 小引
  • 共享的范例代码
  • 使用 Lazy<T>
  • 使用自动工厂
  • 注入自定义工厂

小引

当我们说「解析某个型别/组件」时,意思通常是呼叫某类别的建构函式,以建立其实例(instance)。但有些场合,我们会希望解析时先不要生成对象,而是等到真正要呼叫对象的方法时才建立对象。这种延后建立对象的解析方式,叫做「延迟解析」(deferred resolution)。

延迟解析通常用在哪里呢?一个典型的场合是欲解析的对象的创建过程需要花较多时间(例如解析时可能因为建构函式需要注入其他对象,而产生多层巢状解析的情形),而我们希望能加快这个过程,以提升应用程序的响应速度。

本文介绍两种实现延迟解析的作法,一种是 Lazy&lt;T&gt;,另一种是「自动工厂」(automatic factories)。

共享的范例代码

为了避免往后重复太多相同的程序代码,这里先列出共享的接口与类别。

假设情境

假设应用程序需要提供讯息通知机制,而此机制需支持多种发送管道,例如:电子邮件、简讯服务(Short Message Service)、行动应用程序的讯息推送(push notification)等等。简单起见,这里仅实作其中两种服务,而且发送讯息的部分都使用简单的 Console.WriteLine() 来输出讯息,方便观察程序的执行结果。

设计

用一个 NotificationManager 类来作为整个讯息通知功能的管理员。各类讯息通知机制则由以下兩個类提供:

  • EmailService:透过电子邮件发送讯息
  • SmsService:透过简讯服务发送讯息

以上三个类均实作同一个接口: IMessageService, 而且 NotificationManager 只知道 IMessageService 接口,而不直接依赖具象类。下图描绘了它们的关系:

代码

讯息通知管理员的相关代码:

public interface INotificationManager
{
void Notify(string to, string msg);
} public class NotificationManager : INotificationManager
{
private readonly IMessageService _msgService = null; // 从建构函式注入讯息服务对象。
public NotificationManager(IMessageService svc)
{
_msgService = svc;
} // 利用讯息服务来发送讯息给指定对象。
public void Notify(string to, string msg)
{
_msgService.SendMessage(to, msg);
}
}

这里采用了 Constructor Injection 的注入方式,由建构函式传入讯息服务。其中的 Notify 方法则利用事先注入的讯息服务来发送讯息给指定的接收对象(自变量 "to")。在真实世界中,你可能会需要用额外的类别来代表讯息接收对象(例如设计一个 MessageRecipient 类 别来封装收件人的各项信息),这里为了示范而对这部分做了相当程度的简化。

底下是各类讯息服务的程序代码:

public interface IMessageService
{
void SendMessage(string to, string msg);
} public class EmailService : IMessageService
{
public void SendMessage(string to, string msg)
{
Console.WriteLine(" 透过 EmailService 发送邮件给 {0}。", to);
}
} public class SmsService : IMessageService
{
public void SendMessage(string to, string msg)
{
Console.WriteLine(" 透过 SmsService 发送简讯给 {0}。", to);
}
}

使用 Lazy<T>

Unity 可以让我们直接使用 .NET Framework 内建的 Lazy&lt;T&gt; 来实现延迟解析。试比较底下两个范例,首先是一般的写法:

// 一般写法
var container = new UnityContainer();
container.RegisterType<IMessageService, EmailService>(); // 注册 var svc = container.Resolve<IMessageService>(); // 解析组件(呼叫实作类别的建构函式)
svc.SendMessage("Michael", "Hello!"); // 使用组件

然后是 Lazy&lt;T&gt; 的延迟解析写法,由于注册型别的程序代码不变,故只列出解析的部分:

var lazyObj = container.Resolve<Lazy<IMessageService>>(); // 延迟解析
var svc = lazyObj.Value; // 此时才真正呼叫类别的建构函式
svc.SendMessage("Michael", "Hello!"); // 使用组件

注:有关 Lazy&T& 的用法,请参阅 MSDN 在线文件,或搜寻关键词「Lazy of T」。

使用自动工厂

「自动工厂」(automatic factories)指的是 DI 容器能够自动生成一个轻量级的工厂类别,这样我们就不用花工夫自己写了。那么,什么情况会用到自动工厂呢?通常是用来实现「延迟解析」,或者应付更复杂、更动态的晚期绑定(late binding)的需求。

仍旧以先前提过的 NotificationManager 和 IMessageService 为例。假设 EmailService 这个组件在建立实例时需要花费较长的时间(约五秒),参考以下程序片段:

public class EmailService : IMessageService
{
public EmailService()
{
// 以暂停线程的方式来仿真对象生成的过程需要花费较长时间。
System.Threading.Thread.Sleep();
} // 其余程序代码在这里并不重要,予以省略。
}

如果照一般的组件解析方式,会这么写:

// (1) 注册
var container = new UnityContainer();
container.RegisterType<IMessageService, EmailService>(); // (2) 解析
var notySvc = container.Resolve<NotificationManager>(); // (3) 呼叫
notySvc.Notify("Michael", "自动工厂范例");

这种写法,在其中的「(2) 解析」这个步骤会发生下列动作:

  1. DI 容器欲解析 NotificationManager,发现其建构函式需要传入 IMessageService 对象,于是先解析 IMessageService。
  2. 由于先前向容器注册组件时已经指定由 EmailService 来作为 IMessageService 的实作类别,故容器会先建立一个 EmailService 对象,然后将此对象传入 NotificationManager 的建构函式,以便建立一个 NotificationManager 对象。

也就是说,在解析 NotificationManager 时便一并建立了 EmailService 对象,故此步骤至少要花五秒的时间才能完成。然而,现在我们想要延后相依对象的创建时机,亦即等到真正呼叫组件的方法时,才真正建立其相依对象的实例。像这种场合,我们可以利用 Func&lt;T&gt; 与 Unity 的「自动工厂」来达到延迟解析相依对象的效果。作法很简单,只要修改 NotificationManager 类别就行了。如下所示:

class NotificationManager
{
private IMessageService _msgService;
private Func<IMessageService> _msgServiceFactory public NotificationManager(Func<IMessageService> svcFactory)
{
// 把工厂方法保存在委派对象里
_msgServiceFactory = svcFactory;
} public void Notify(string to, string msg)
{
// 由于每次呼叫 _msgServiceFactory() 时都会建立一个新的 IMessageService 对象,
// 这里用一个私有成员变量来保存先前建立的对象,以免不断建立新的实例。
// 当然这并非必要;有些场合,你可能会想要每次都建立新的相依对象。
if (_msgService == null)
{
_msgService = _msgServiceFactory();
} _msgService.SendMessage(to, msg);
}
}

另一方面,原先的「注册、解析、呼叫」三步骤的程序代码都不用任何改变。方便阅读起见,这里再将注册组件的程序代码贴上来:

// (1) 注册
var container = new UnityContainer();
container.RegisterType<IMessageService, EmailService>();

请注意,NotificationManager 的建构函式要求注入的明明是 Func&lt;IMessageService&gt;,可是向容器注册组件时,却依旧写 IMessageService,而不用改成 Func&lt;IMessageService&gt;(要五毛,给一块)。如此一来,当你想要为既有程序代码加入延迟解析(延迟建立相依对象)的能力时,就可以少改一些程序代码。这是 Unity 容器的「自动工厂」提供的好处。

进一步解释,当 Unity 容器欲解析 NotificationManager 时,发现其建构函式需要一个 Func&lt;IMessageService&gt; 委派(delegate),于是便自动帮你生成这个对象,并将它注入至 NotificationManager 类别的建构函式。由于注入的是委派对象(你可以把它当作是个工厂方法),故此时并没有真正建立 IMessageService 对象,而是等到上层模块呼叫此组件的 Notify 方法时,才透过呼叫委派方法来建立 IMessageService 对象 。

当然,Unity 容器的「自动工厂」可能无法满足某些需求。比如说,有些相依对象的创建逻辑比较复杂,需要你撰写自定义的对象工厂。这个时候,你可能会想要知道如何注入自定义工厂。

注入自定义工厂

当你想要让 Unity 容器在解析特定组件时使用你的自定义工厂来建立所需之相依对象,Unity 框架的 InjectionFactory 类别可以派上用场。
延续上一个小节的 NotificationManager 范例。现在假设你写了一个对象工厂来封装 IMessageService 的创建逻辑,像这样:

class MessageServiceFactory
{
public IMessageService GetService()
{
bool isEmail = CheckIfEmailIsUsed();
if (isEmail)
{
return new EmailService();
}
else
{
return new SmsService();
}
}
}

此对象工厂的 GetService 方法会根据执行时期的某些变量来决定要返回 EmailService 还是 SmsService 的实例。EmailService 与 SmsService 这两个类别都实作了 IMessageService 接口,它们的程序代码在这里并不重要,故未列出。如需查看这些类别的程序代码,可参阅稍早的〈共享的范例程序〉一节的内容。

NotificationManager 的建构函式与上一节的范例相同,仍旧是注入 Func&lt;IMessageService&gt;。如下所示:

class NotificationManager
{
private IMessageService _msgService;
private Func<IMessageService> _msgServiceFactory public NotificationManager(Func<IMessageService> svcFactory)
{
// 把工厂方法保存在委派对象里
_msgServiceFactory = svcFactory;
} // (已省略其他不重要的程序代码)
}

剩下的工作,就是告诉 Unity 容器:「在需要解析 IMessageService 的时候,请使用我的 MessageServiceFactory 来建立对象。」参考以下程序片段:

var container = new UnityContainer();

// 注册
Func&lt;IMessageService&gt; factoryMethod = new MessageServiceFactory().GetService;
container.RegisterType&lt;IMessageService&gt;(new InjectionFactory(c =&gt; factoryMethod())); // 解析
container.Resolve&lt;NotificationManager&gt;();

注册组件的部分需要加以说明,如下:

  • 先建立一个 Func&lt;IMessageService&gt; 的委派对象,让它指向 MessageServiceFactory 对象的 GetService 方法。
  • 接着呼叫 Unity 容器的 RegisterType 方法,告诉容器:解析 IMessageService 时,请用我提供的自定义工厂的 GetService 方法,而这个工厂方法已经包在刚才建立的委派对象(变量 factoryMethod),并透过 Unity 的 InjectionFactory 将此工厂方法再包一层,以便保存于 Unity 容器。

此范例所使用的 RegisterType 是个扩充方法,其原型宣告如下:

public static IUnityContainer RegisterType<T>(this IUnityContainer container,
params InjectionMember[] injectionMembers);

InjectionFactory 类别继承自 InjectionMember,而此范例所使用的建构函式之原型宣告为:

public InjectionFactory(Func<IUnityContainer, object> factoryFunc);

注:如需 InjectionFactory 类别的详细说明,可参考在线文件

本文摘自:《 .NET 依赖注入 》第 7 章。

Unity 入門 - 延遲解析的更多相关文章

  1. 依賴注入入門——Unity(二)

    參考博客文章http://www.cnblogs.com/kebixisimba/category/130432.html http://www.cnblogs.com/qqlin/tag/Unity ...

  2. Delphi APP 開發入門(七)通知與雲端推播

    Delphi APP 開發入門(七)通知與雲端推播 分享: Share on facebookShare on twitterShare on google_plusone_share   閲讀次數: ...

  3. GOOGLE搜索從入門到精通V4.0

    1,前言2,摘要3,如何使用本文4,Google簡介5,搜索入門6,初階搜索 6.1,搜索結果要求包含兩個及兩個以上關鍵字 6.2,搜索結果要求不包含某些特定資訊 6.3,搜索結果至少包含多個關鍵字中 ...

  4. Flask從入門到入土(三)——模板

    模板是一個包含響應文本的文件,其中包含佔位變量表示的動態部分,其具體值只是請求上下文中才能知道.使用真實值替換變量,再返回最終得到的響應字符串,這一過程稱爲渲染.爲了渲染模板,Flask使用了一個名爲 ...

  5. Windows PowerShell 入門(7)-関数編2

    この連載では.Microsoftが提供している新しいシェル.Windows Power Shellの使い方を解説します.前回に引き続きPowerShellにおける関数の取り扱いとして.変数と関数のスコ ...

  6. Windows PowerShell 入門(3)-スクリプト編

    これまでの記事 Windows PowerShell 入門(1)-基本操作編 Windows PowerShell 入門(2)-基本操作編 2 対象読者 Windows PowerShellでコマンド ...

  7. Windows PowerShell 入門(2)-基本操作編 2

    前回に引き続きMicrosoftが提供している新しいシェル.Windows Power Shellの基本操作方法を学びます.基本操作編第2弾の今回は.パイプの使用方法を中心としたコマンドレットの操作方 ...

  8. Delphi APP 開發入門(四)簡易手電筒

    Delphi APP 開發入門(四)簡易手電筒 分享: Share on facebookShare on twitterShare on google_plusone_share   閲讀次數:32 ...

  9. Delphi APP 開發入門(六)Object Pascal 語法初探

    Delphi APP 開發入門(六)Object Pascal 語法初探 分享: Share on facebookShare on twitterShare on google_plusone_sh ...

随机推荐

  1. 【非常高%】【codeforces 733B】Parade

    time limit per test1 second memory limit per test256 megabytes inputstandard input outputstandard ou ...

  2. How to configure spring boot through annotations in order to have something similar to <jsp-config> in web.xml?

    JSP file not rendering in Spring Boot web application You will need not one but two dependencies (ja ...

  3. vue axios拦截器加全局loading

    import axios from 'axios' import util from './util' import {showFullScreenLoading, tryHideFullScreen ...

  4. .net 程序员 两年工作总结

    2013 年7月毕业,算一算从开始在现任的公司实习到现在已经有小两年的时间了.公司的工作虽然不忙,但也一直没有时间思考一下. 现在决定辞职了,忽然一下轻松的让人想思考. 普通程序员.普通本科生.普通的 ...

  5. 在IIS上部署.net core的webapi项目 以及502.5错误的两种解决方法

    首先要在服务器上面安装.net core https://github.com/dotnet/core/tree/master/release-notes/download-archives 这里面有 ...

  6. C#中的DataGridView

    关键字:C# DataGridView作者:peterzb来源:http://www.cnblogs.com/peterzb 1.DataGridView实现课程表 testcontrol.rar 2 ...

  7. HTML5 课程

    http://www.w3school.com.cn/html5/html_5_geolocation.asp HTML5 教程 HTML5 教程 HTML5 简单介绍 HTML5 视频 HTML5 ...

  8. JAVA面试题解惑系列(七)——使用日期和时间

    日期和时间,不仅在考试面试题.处理的问题.似乎没有哪个项目能够避开它们.我们经常在处理用户的出生年月日.注冊日期,订单的创建时间等属性时用到,由此可见其重要性.  java.util.Date类  提 ...

  9. Java之"Mozilla Rhino"引擎(二)

    在Java中使用Rhino, 能让你使用类似Groovy, ECMAScript...等等之类的不同动态脚本语言, 其中值得推荐的是ECMAScript, 它是Rhino的默认实现, 同时也在JDK1 ...

  10. 解压压缩文件报错gzip: stdin: not in gzip format tar: Child returned status 1 tar: Error is not recoverable: exiting now

    压缩包是直接weget 后面加官网上的tar包地址获取的  [root@xuegod43 ~]# tar -zxvf /home/hadoop/hadoop-2.6.5-src.tar.gz gzip ...