整理一些有关使用 DI 容器的一些建议事项,主要的参考数据源是 Jimmy Board 的文章:Container Usage Guidelines

1.容器设定

避免对同一个组件(DLL)重复扫描两次或更多次

扫描组件的目的是为了自动注册类型对应关系,故其过程涉及了探索组件内含之类型信息。依应用程序所包含的组件与类型数量而定,扫描组件与探索类型的动作可能在毫秒内完成,亦可能需要花费数十秒。因此,当你在应用程序中使用 DI 容器的自动扫描功能来注册类型时,应注意避免对同一个组件重复扫描两次以上,以免拖慢应用程序的执行性能(通常是影响应用程序激活的时间,因为注册类型的动作大多集中写在 Composition Root)。

为了能够自动找到类型对应关系,类型的命名通常会遵循特定惯例,比如说,在扫描组件过程中,自动把 Foo 注册成为 IFoo 的实现类型。

有关 Unity 自动注册的用法可参考 Unity Configuration - Registration by Convention 或《.NET 依赖注入》的第七章。

使用不同类型来注册不同用途的组件

例如,你可能有一个 WebApiConfig 类型负责注册 ASP.NET Web API 相关类型,以及用一个 DalConfig 类型来注册数据存取层(Data Access Layer)的相关类型。然后,在应用程序的组合根(Composition Root)呼叫这些类型的注册类型方法。一种常见的写法是把组合根放在一个叫做Bootstrapper 的类型里,像这样:

public static class Bootstrapper
{
private static readonly IUnityContainer _container = new UnityContainer(); public static void RegisterDependencies()
{
WebApiConfig.RegisterTypes(_container);
DalConfig.RegisterTypes(_container);
BllConfig.RegisterTypes(_container);
}
}

此范例使用了一个静态(static)类型 Bootstrapper 来总管容器对象的建立与类型注册的工作,而此作法对于某些需要更大弹性的场合来说可能不适用,此时可参考下一个建议。

使用非静态类型来建立与设定 DI 容器

有时候,应用程序可能需要在不同时机建立多个 DI 容器,而这些 DI 容器的生命周期可能不完全相同。若应用程序只有一个全局共享的静态 DI 容器,便无法满足上述需求。提供非静态的 API,例如提供 instance 层级的方法和属性(而不是 class 层级的 static 方法和属性),可提供客户端程序更多弹性,同时也更方便测试时抽换特定组件。

不要另外建立一个 DLL 项目来集中处理相依关系的解析

DI 容器的设定应该写在需要解析那些相依类型的应用程序里面,而不要把它们集中放在一个单独的 DLL 项目(例如 DependencyResolver.csproj)。

为个别组件加入一个初始化类型来设定相依关系

如果你的 DLL 组件会给多个项目共享,而且它依赖某些外部类型,此时最好为该组件提供一个初始化类型(如前面提过的 Bootstrapper)来设定相依关系。若该组件需要扫描其他组件来寻找相依类型,此动作也是写在初始化类型里。

扫描组件时,尽量避免指定组件名称

组件名称可能会变,所以在扫描组件时,最好避免指定特定名称的组件。

2. 生命周期管理

多数 DI 容器都有提供对象生命周期管理的功能。一般而言,应用程序比较常用的是 Transient(每次要求解析时都建立新的对象)、Singleton(全都共享同一个对象)、以及 Per-HTTP request(每一个 HTTP 请求范围内共享同一个对象),而只在某些少数场合才需要用到特殊的自定义生命周期。

优先使用 DI 容器来管理对象的生命周期

若无特殊原因,最好使用 DI 容器来管理对象的生命周期,而不要自行撰写对象工厂来管理相依对象的生命周期。这是因为,目前大家喊得出名号的 DI 容器都经过多年的实务考验与多方测试,不仅在设计上考虑得更全面,发生 bug 的机率也比自己写的组件来得低。有时候,对象生灭的时机比较特殊,或者需要更细致地管理对象的生命周期,这些情况则不妨自行撰写对象工厂来管理。

考虑使用子容器来管理 Per-Request 类型的对象

对于 Per HTTP Request 或类似的场合,一种常见的作法是把对象保存在当前的 HttpContext 对象里,例如在 HTTP Request 建立之初便一并建立相依对象并将它们保存于 HttpContext.Current.Items 集合里,然后在 Request 结束前一并释放这些相依对象。DI 容器提供了另一个选择来处理这类需求:子容器(child container)。

所有透过子容器解析(建立)的对象,其寿命不会超过子容器。也就是说,子容器一旦消失,它所管理的对象亦随之消失。基于此特性,我们可以利用子容器来管理如 Per HTTP Request 或其他类似性质的对象生命周期。

在适当时机呼叫容器的 Dispose 方法

DI 容器通常有实现 IDisposable 接口,亦即提供了 Dispose 方法。当你呼叫某容器对象的 Dispose 方法来释放该容器时,它会找出内部管理的所有对象,只要是支持 IDisposable 的对象,就呼叫它的 Dispose 方法,以确保相依对象的资源得以释放。

3. 组件设计相关建议

避免建立深层的巢状对象

虽然我们偏好对象组合而不是类型继承,但如果相依对象的巢状关系太多且深,这样的程序架构仍然不好维护。DI 容器的一个方便却也危险之处,是它具备自动解析巢状相依对象的能力。于是,我们甚至只要写一行代码就能解析所有深层的相依对象。这的确减轻了开发人员的负担,但同时也隐藏了背后的复杂性,因而导致开发人员更容易忽略设计的缺陷:太多零碎的小接口,组合出非常复杂的对象图。对此设计面的问题,DI 容器没法帮忙,唯有靠开发人员自己多加注意。

考虑使用泛型来封装抽象概念

当你发觉整个程序架构太过笨重,可试着从现有的组件中找出同性质的功能,并为它们定义基础的泛型接口,例如ICommand<T>IValidator<T>IRequestHandler<TRequest, TResponse> 等等。

考虑使用 Adapter 或 Facade 来封装 3rd-party 组件

开发应用程序时,难免会使用一些现成的外来(third-party)组件。有时候,外来组件可能会因为版本更新而造成既有应用程序无法运行或产生错误的结果。为了避免这种状况,我们可以利用 Adapter、Facade、若 Proxy 等模式来为自己的应用程序建立一个反腐败层(anti-corruption layer)。

不要一律为每个组件定义一个接口

对 S.O.L.I.D. 设计原则的一个常见误解是:每个组件都应该要有一个相应的接口。当组件本身确实具有某个抽象概念的意涵,那么为它定义一个抽象接口是合理的;但如果它不具备抽象概念,就不要硬为它生出一个接口。此外,你也不应该因为在使用 DI 容器时想要一致透过接口来解析,而为特定组件定义接口。DI 容器可以直接解析具象类型(concrete type),故从技术上而言,直接使用具象类型并不是问题。

看一下应用程序中的组件,如果许多组件的类名正好就是它所实现的接口名称去掉 'I'(例如 Foo 与 IFoo),这可能是一个讯号:有些接口可能只是单纯因为「每个组件都要有个接口」的想法而产生的。

对于同一层(layer)的组件,可依赖其具象类型

如果某组件与其所依赖的其他组件都位在应用程序的同一层(layer),而且没有抽换组件实现的需要(例如单元测试),这种情况,可直接依赖具象类型无妨。这就好像在同一间办公室里面工作的同事,通常是直接当面沟通;除非有特殊原因,否则没必要透过中间人传话,或透过即时消息软件的方式沟通。

4. 动态解析

虽然组件或服务的类型大多能够在应用程序初始化的时候决定,但有时候还是需要依执行时期的特定条件来动态决定使用哪个类型。比如说,在 ASP.NET MVC 应用程序中,可能会需要根据每一次前端网页发出请求的动作参数来决定该绑定哪一个 model 类型,而无法在建立 controller 时预先得知欲绑定的 model 类型。

尽量避免把 DI 容器直接当成 Service Locator 来使用

如需动态解析组件类型,比较偷懒的办法是把 DI 容器当成全局共享的 Service Locator 来使用。像这样:

MyApp.Container.Resolve<IValidationProvider>();

这种用法很方便,只要在程序的某处向 DI 容器预先注册组件类型,之后在程序中的任何地方都可以透过 DI 容器来解析组件。正因为它方便,所以容易滥用,使得组件之间的相依关系变得复杂紊乱,增加代码阅读与维护的困难。

因此,若有其他选项,例如 Constructor InjectionFactory 模式 等,应优先考虑。若都没有合适的办法,最后才使用 Service Locator

此外,如果应用程序框架本身已经有提供组件解析的机制,也应该优先采用。例如 ASP.NET MVC 和 Web API 提供的 IDependencyResolver 及其相关机制。

考虑使用对象工厂或 Func<T> 来处理晚期绑定

有时候,必须等到程序执行时,依当下的某些变量值来动态决定该建立何种对象。例如:

class NotificationService
{
private IMessageService _emailService; // 邮件服务
private IMessageService _smsService; // 简讯服务 public NotificationService(IMessageService emailService, IMessage smsService)
{
_emailService = emailService;
_smsService = smsService;
} public void Notify(string to, string msg)
{
if (当前某些变量值符合特定条件)
{
_emailService.SendMessage(to, msg);
}
else
{
_smsService.SendMessage(to, msg);
}
}
}

其中「当前某些变量值符合特定条件」所涉及的相关信息如果不存在 NotificationService 类型里面,则可以考虑将「建立对象」的工作委外,亦即由呼叫端或其他特定类型来负责建立所需之对象。一种常见的委外方式是撰写特定用途的对象工厂,把建立对象的逻辑包在工厂类型里,例如:

class MessageServiceFactory : IMessageServiceFactory
{
public IMessageService GetService()
{
if (当前某些变量值符合特定条件)
{
return new EmailService();
}
else
{
return new SmsService();
}
}
} class NotificationService
{
public NotificationService(IMessageServiceFactory msgServiceFactory)
{
_msgServiceFactory = msgServiceFactory;
} public void Notify(string to, msg)
{
using (var msgService = _msgServiceFactory.GetService())
{
msgService.SendMessage(to, msg);
}
}
}

于是客户端可以这么写:

    var notySvc = new NotificationService(new MessageServiceFactory());
notySvc.Notify("Michael", "DI Example");

另一种可行的作法,是利用 Func<T> 来让外界提供建立对象的函式。像这样:

class NotificationService
{
private Func<IMessageService> _msgServiceFactory; public NotificationService(Func<IMessageService> svcFactory)
{
_msgServiceFactory = svcFactory;
} public void SendMessage(string to, string msg)
{
using (var msgService = _msgServiceFactory())
{
msgService.Send(to, msg);
}
}
}

如果已经了解上述两种做法,还可以试着再进一步,使用 DI 框架提供的「自动工厂」(automatic factory)来达到相同目的。

DI 容器实务建议的更多相关文章

  1. StructureMap经典的IoC/DI容器

    StructureMap是一款很老的IoC/DI容器,从2004年.NET 1.1支持至今. 一个使用例子 //创建业务接口 public interface IDispatchService { } ...

  2. net core体系-web应用程序-4net core2.0大白话带你入门-8asp.net core 内置DI容器(DependencyInjection,控制翻转)的一点小理解

    asp.net core 内置DI容器的一点小理解   DI容器本质上是一个工厂,负责提供向它请求的类型的实例. .net core内置了一个轻量级的DI容器,方便开发人员面向接口编程和依赖倒置(IO ...

  3. asp.net core 内置DI容器的一点小理解

    DI容器本质上是一个工厂,负责提供向它请求的类型的实例. .net core内置了一个轻量级的DI容器,方便开发人员面向接口编程和依赖倒置(IOC). 具体体现为Micorosoft.Extensio ...

  4. 解读超轻量级DI容器-Guice与Spring框架的区别【转载】

    依赖注入,DI(Dependency Injection),它的作用自然不必多说,提及DI容器,例如spring,picoContainer,EJB容器等等,近日,google诞生了更轻巧的DI容器… ...

  5. DI容器Ninject在管理接口和实现、基类和派生类并实现依赖注入方面的实例

    当一个类依赖于另一个具体类的时候,这样很容易形成两者间的"强耦合"关系.我们通常根据具体类抽象出一个接口,然后让类来依赖这个接口,这样就形成了"松耦合"关系,有 ...

  6. 从yii2框架中的di容器源码中了解反射的作用

    反射简介 参考官方简介的话,PHP 5 具有完整的反射 API,添加了对类.接口.函数.方法和扩展进行反向工程的能力. 此外,反射 API 提供了方法来取出函数.类和方法中的文档注释. YII2框架中 ...

  7. ASP.NET 5 牛刀小試(二):加入第三方 DI 容器

    上回介绍了 ASP.NET vNext 自带容器的基本用法,这次要试试把 ASP.NET vNext 的自带容器换成 Autofac. 这一次,在编写范例程序的过程中,光是解决 KRE 与相关套件的版 ...

  8. ASP.NET 5 (vNext) 牛刀小試:自帶 DI 容器

    小引 在 ASP.NET 5(vNext)之前,亦即 MVC 4/5.Web API 2 的时代,MVC 与 Web API 框架彼此有非常相似的设计,却是以不同的代码来实现.现在,ASP.NET 5 ...

  9. 手动实现一个 IOC/DI 容器

    第一章为源码解析. 第二章为实现一个简单的 IOC 容器. 第三章进阶 Spring 插件开发. 手动实现一个 IOC/DI 容器 上一篇文章里我们已经对 Spring 的源码有了一个大概的认识,对于 ...

随机推荐

  1. Spring 组cxf宣布webservice

    通过spring宣布webservice接口 spring jar包+cxf jar Java包 下列文件丢失jar包需要下载自己 项目结构 watermark/2/text/aHR0cDovL2Js ...

  2. [Android]TextView点击获取部分内容

    TextView控件本身有很多属性可以进行控制,如果要获取内容只需要getText()方法就可以实现,同时也可以为TextView设置各种监听器.但是,如果想要实现点击获取TextView内部的部分内 ...

  3. mingw qt(可以去掉mingwm10.dll、libgcc_s_dw2-1.dll、libstdc++-6.dll的依赖,mingw默认都是动态链接gcc的库而TDM是静态链接gcc库,tdm版本更好用。用aspack压缩没有问题。qt本身不使用异常处理)good

    原文地址:mingw qt作者:孙1东 不使用Qt SDK,使用mingw编译qt源代码所遇问题及解决方法: configure -fast -release -no-exceptions -no-r ...

  4. matlab 格式化文本文件的解析

    比如这样一种格式化的文本文件,文件说明及下载地址:/pub/machine-learning-databases/statlog/german/ 的索引 fid = fopen('german.dat ...

  5. 漫步Unity3D(一)

    前言 采用Unity它已经将近半年的时间,虽然项目仅仅是一个半成品.但Unity熟几乎相同的游戏.在这里,在使用过程中遇到的问题,再梳.不涉及过于详细的功能和代码.但是,假设他们将参与开发一个在线知识 ...

  6. 为什么台湾人工智能可能抢输大陆?(XPU时代来临)

    到了 2020 年,每 3 支手机,就会有一支内建有 AI 芯片. 但目前浮出水面的 AI 芯片新创,几乎都是大陆公司. 为什么台湾这回选择缺席? 「我听说 CPU.GPU,没有听过 NPU? 」11 ...

  7. WPF依赖属性值源(BaseValueSource)

    原文:WPF依赖属性值源(BaseValueSource)   WPF依赖属性提供一个机制,可以获取依赖属性提供值的来源 其以BaseValueSource枚举表示 1.Default public ...

  8. APP和服务端-架构设计(二)

    1. App架构设计经验谈:接口的设计 App与服务器的通信接口如何设计得好,需要考虑的地方挺多的,在此根据我的一些经验做一些总结分享,旨在抛砖引玉. 1.1 安全机制的设计 现在,大部分App的接口 ...

  9. Unity3d报告奇怪的错误CompareBaseObjectsInternal can only be called from the main thread.

    其中使用了该项目.NET的Async Socket代码.后来不知道什么时候这个奇怪的错误的出现: CompareBaseObjectsInternal can only be called from ...

  10. σ 代数与测度(measures)

    1. definition Let A be a collection of subsets(集合的集合体,collection of subsets) of a sample space Ω,A i ...