http://qing.weibo.com/tj/400082fa33001h7x.html

1.5 实现依赖注入1.5.1 背景介绍

设计模式中,尤其是结构型模式很多时候解决的就是对象间的依赖关系,变依赖具体为依赖抽象。平时开发中如果发现客户程序依赖某个(或某类)对象,我们常常会对它们进行一次抽象,形成抽象的抽象类、接口,这样客户程序就可以摆脱所依赖的具体类型。

这个过程中有个环节被忽略了——谁来选择客户程序需要的满足抽象类型的具体类型呢?通过后面的介绍你会发现很多时候创建型模式可以比较优雅地解决这个问题。但另一问题出现了,如果您设计的不是具体业务逻辑,而是公共库或框架程序,这时候您是一个“服务方”,不是您调用那些构造类型,而是它们把抽象类型传给您,怎么松散地把加工好的抽象类型传递给客户程序就是另一回事了。

这个情形也就是常说的“控制反转”,IOC:Inverse of Control;框架程序与抽象类型的调用关系就像常说的好莱坞规则:Don’t call me, I’ll call you.

参考Martin Fowler在《Inversion of Control Containers and the Dependency Injection pattern》一文,我们可以采用“依赖注入”的方式将加工好的抽象类型实例“注入”到客户程序中,本书的示例也将大量采用这种方式将各种依赖项“注入”到模式实现的外部——客户程序。下面我们结合一个具体的示例看看为什么需要依赖注入,以及Martin Fowler文中提到的三种经典方式,然后依据C#语言的特质,再扩展出一个基于Attribter方式注入(参考原有的Setter命名,这里将基于Attribute的方法称为Attributer)。

1.5.2 示例情景

客户程序需要一个提供System.DateTime类型当前系统时间的对象,然后根据需要仅仅把其中的年份部分提取出来,因此最初的实现代码如下:

C#

using System;

using System.Diagnostics;

namespace MarvellousWorks.PracticalPattern.Concept.DependencyInjection.Example1

{

class TimeProvider

{

public DateTime CurrentDate { get { return DateTime.Now; } }

}

public class Client

{

public int GetYear()

{

TimeProvider timeProvier = new TimeProvider();

return timeProvier.CurrentDate.Year;

}

}

}

后来因为某种原因,发现使用.NET Framework自带的日期类型精度不够,需要提供其他来源的TimeProvider,确保在不同精度要求的功能模块中使用不同的TimeProvider。这样问题集中在TimeProvider的变化会影响客户程序,但其实客户程序仅需要抽象地使用其获取当前时间的方法。为此,增加一个抽象接口,确保客户程序仅依赖这个接口ITimeProvider,由于这部分客户程序仅需要精确到年,因此它可以使用一个名为SystemTimeProvider (:ITimeProvider)的类型。新的实现代码如下:

C#

using System;

namespace 
   MarvellousWorks.PracticalPattern.Concept.DependencyInjection.Example2

{

interface ITimeProvider

{

DateTime CurrentDate { get;}

}

class TimeProvider : ITimeProvider

{

public DateTime CurrentDate { get { return DateTime.Now; } }

}

public class Client

{

public int GetYear()

{

ITimeProvider timeProvier = new TimeProvider();

return timeProvier.CurrentDate.Year;

}

}

}

这样看上去客户程序后续处理权都依赖于抽象的ITimeProvider,问题似乎解决了?没有,它还要知道具体的SystemTimeProvider。因此,需要增加一个对象,由它选择某种方式把ITimeProvider实例传递给客户程序,这个对象被称为Assembler。新的结构如图1-19所示。

图1-19 增建装配对象后新的依赖关系

其中,Assembler的职责如下:

l          知道每个具体TimeProviderImpl的类型。

l          可根据客户程序的需要,将抽象ITimeProvider反馈给客户程序。

l          本身还负责对TimeProviderImpl的创建。

下面是一个Assembler的示例实现:

C#

public class Assembler

{

/// <summary>

/// 保存“抽象类型/实体类型”对应关系的字典

/// </summary>

private static Dictionary<Type, Type> dictionary = 
       new Dictionary<Type, Type>();

static Assembler()

{

// 注册抽象类型需要使用的实体类型

// 实际的配置信息可以从外层机制获得,例如通过配置定义

dictionary.Add(typeof(ITimeProvider), typeof(SystemTimeProvider));

}

/// 根据客户程序需要的抽象类型选择相应的实体类型,并返回类型实例

/// <returns>实体类型实例</returns>

public object Create(Type type)     // 主要用于非泛型方式调用

{

if ((type == null) || !dictionary.ContainsKey(type)) throw new 
            NullReferenceException();

Type targetType = dictionary[type];

return Activator.CreateInstance(targetType);

}

/// <typeparam name="T">抽象类型(抽象类/接口/或者某种基类)</typeparam>

public T Create<T>()    // 主要用于泛型方式调用

{

return (T)Create(typeof(T));

}

}

1.5.3 Constructor注入

构造函数注入,顾名思义,就是在构造函数的时候,通过Assembler或其他机制把抽象类型作为参数传递给客户类型。这种方式虽然相对其他方式有些粗糙,而且仅在构造过程中通过“一锤子”方式设置好,但很多时候我们设计上正好就需要这种Read Only的注入方式。其实现方式如下:

C#

/// 在构造函数中注入

class Client

{

private ITimeProvider timeProvider;

public Client(ITimeProvider timeProvider)

{

this.timeProvider = timeProvider;

}

}

Unit Test

[TestClass]

public class TestClient

{

[TestMethod]

public void Test()

{

ITimeProvider timeProvider = 
           (new Assembler()).Create<ITimeProvider>();

Assert.IsNotNull(timeProvider);     // 确认可以正常获得抽象类型实例

Client client = new Client(timeProvider);   // 在构造函数中注入

}

}

1.5.4 Setter注入

Setter注入是通过属性赋值的办法解决的,由于Java等很多语言中没有真正的属性,所以Setter注入一般通过一个set()方法实现,C#语言由于本身就有可写属性,所以实现起来更简洁,更像Setter。相比较Constructor方式而言,Setter给了客户类型后续修改的机会,它比较适应于客户类型实例存活时间较长,但Assembler修改抽象类型指定的具体实体类型相对较快的情景;不过也可以由客户程序根据需要动态设置所需的类型。其实现方式如下:

C#

/// 通过Setter实现注入

class Client

{

private ITimeProvider timeProvider;

public ITimeProvider TimeProvider

{

get { return this.timeProvider; }   // getter本身和以Setter方式实现
                                                       注入没有关系

set { this.timeProvider = value; } // Setter

}

}

Unit Test

[TestClass]

public class TestClient

{

[TestMethod]

public void Test()

{

ITimeProvider timeProvider = 
           (new Assembler()).Create<ITimeProvider>();

Assert.IsNotNull(timeProvider);     // 确认可以正常获得抽象类型实例

Client client = new Client();

client.TimeProvider = timeProvider; // 通过Setter实现注入

}

}

1.5.5 接口注入

接口注入是将包括抽象类型注入的入口以方法的形式定义在一个接口里,如果客户类型需要实现这个注入过程,则实现这个接口,客户类型自己考虑如何把抽象类型“引入”内部。实际上接口注入有很强的侵入性,除了要求客户类型增加需要的Setter或Constructor注入的代码外,还显式地定义了一个新的接口并要求客户类型实现它。除非还有更外层容器使用的要求,或者有完善的配置系统,可以通过反射动态实现接口方式注入,否则笔者并不建议采用接口注入方式。

既然Martin Fowler文中提到了这个实现方式,就给出如下示例:

C#

/// 定义需要注入ITimeProvider的类型

interface IObjectWithTimeProvider

{

ITimeProvider TimeProvider { get;set;}

}

/// 通过接口方式注入

class Client : IObjectWithTimeProvider

{

private ITimeProvider timeProvider;

/// IObjectWithTimeProvider Members

public ITimeProvider TimeProvider

{

get { return this.timeProvider; }

set { this.timeProvider = value; }

}

}

Unit Test

[TestClass]

public class TestClient

{

[TestMethod]

public void Test()

{

ITimeProvider timeProvider = (new 
              Assembler()).Create<ITimeProvider>();

Assert.IsNotNull(timeProvider);     // 确认可以正常获得抽象类型实例

IObjectWithTimeProvider objectWithTimeProvider = new Client();

objectWithTimeProvider.TimeProvider = timeProvider; // 通过接口方式注入

}

}

1.5.6 基于Attribute实现注入——Attributer

如果做个归纳,Martin Fowler之前所介绍的三种模式都是在对象部分进行扩展的,随着语言的发展(.NET从1.0开始,Java从5开始),很多在类元数据层次扩展的机制相继出现,比如C#可以通过Attribute将附加的内容注入到对象上。直观上的客户对象有可能在使用上做出让步以适应这种变化,但这违背了依赖注入的初衷,三个角色(客户对象、Assembler、抽象类型)之中两个不能变,那只好在Assembler上下功夫,谁叫它的职责就是负责组装呢?

为了实现上的简洁,上面三个经典实现方式实际将抽象对象注入到客户类型都是在客户程序中(也就是那三个Unit Test部分)完成的,其实同样可以把这个工作交给Assembler完成;而对于Attribute方式注入,最简单的方式则是直接把实现了抽象类型的Attribute定义在客户类型上,例如:

C#

(错误的实现情况)

class SystemTimeAttribute : Attribute, ITimeProvider { … }

[SystemTime]

class Client { … }

相信您也发现了,这样虽然把客户类型需要的ITimeProvider通过“贴标签”的方式告诉它了,但事实上又把客户程序与SystemTimeAttribute“绑”上了,它们紧密地耦合在一起。参考上面的三个实现,当抽象类型与客户对象耦合的时候我们引入了Assembler,当Attribute方式出现类似的情况时,我们写个AttributeAssembler不就行了么?还不行。设计上要把Attribute设计成一个“通道”,考虑到扩展和通用性,它本身要协助AttributeAssembler完成ITimeProvider的装配,最好还可以同时装载其他抽象类型来修饰客户类型。示例代码如下:

C#

[AttributeUsage(AttributeTargets.Class, AllowMultiple=true)]

sealed class DecoratorAttribute : Attribute

{

/// 实现客户类型实际需要的抽象类型的实体类型实例,即得注入客户类型的内容

public readonly object Injector;

private Type type;

public DecoratorAttribute(Type type)

{

if (type == null) throw new ArgumentNullException("type");

this.type = type;

Injector = (new Assembler()).Create(this.type);

}

/// 客户类型需要的抽象对象类型

public Type Type { get { return this.type; } }

}

/// 帮助客户类型和客户程序获取其Attribute定义中需要的抽象类型实例的工具类

static class AttributeHelper

{

public static T Injector<T>(object target)

where T : class

{

if (target == null) throw new ArgumentNullException("target");

Type targetType = target.GetType();

object[] attributes = targetType.GetCustomAttributes(
              typeof(DecoratorAttribute), false);

if ((attributes == null) || (attributes.Length <= 0)) return null ;

foreach (DecoratorAttribute attribute in 
           (DecoratorAttribute[])attributes)

if (attribute.Type == typeof(T))

return (T)attribute.Injector;

return null;

}

}

[Decorator(typeof(ITimeProvider))]

// 应用Attribute,定义需要将ITimeProvider通过它注入

class Client

{

public int GetYear()

{

// 与其他方式注入不同的是,这里使用的ITimeProvider来自自己的Attribute

ITimeProvider provider = 
             AttributeHelper.Injector<ITimeProvider>(this);

return provider.CurrentDate.Year;

}

}

Unit Test

[TestMethod]

public void Test()

{

Client client = new Client();

Assert.IsTrue(client.GetYear() > 0);

}

1.5.7 小结

依赖注入虽然被Martin Fowler称为一个模式,但平时使用中,它更多地作为一项实现技巧出现,开发中很多时候需要借助这项技巧把各个设计模式所加工的成果传递给客户程序。各种实现方式虽然最终目标一致,但在使用特性上有很多区别。

l          Constructor方式:它的注入是一次性的,当客户类型构造的时候就确定了。它很适合那种生命期不长的对象,比如在其存续期间不需要重新适配的对象。此外,相对Setter方式而言,在实现上Constructor可以节省很多代码;

l          Setter方式:一个很灵活的实现方式,对于生命期较长的客户对象而言,可以在运行过程中随时适配;

l          接口方式:作为注入方式具有侵入性,很大程度上它适于需要同时约束一批客户类型的情况;

l          属性方式:随着开发语言的发展引入的新方式,它本身具有范围较小的固定内容侵入性(一个DecoratorAttribute),它也很适合需要同时约束一批客户类型情景。它本身实现相对复杂一些,但客户类型使用的时候非常方便——“打标签”即可。

C#依赖注入实例的更多相关文章

  1. 在net Core3.1上基于winform实现依赖注入实例

    目录 在net Core3.1上基于winform实现依赖注入实例 1.背景 2.依赖注入 2.1依赖注入是什么? 2.1依赖注入的目的 2.2依赖注入带来的好处 2.2.1生命周期的控制 2.2.2 ...

  2. Asp.Net Mvc3.0(MEF依赖注入实例)

    前言 在http://www.cnblogs.com/aehyok/p/3386650.html前面一节主要是对MEF进行简单的介绍.本节主要来介绍如何在Asp.Net Mvc3.0中使用MEF. 准 ...

  3. .NET MVC5+ Dapper+扩展+微软Unity依赖注入实例

    1.dapper和dapper扩展需要在线安装或者引用DLL即可 使用nuget为项目增加Unity相关的包 2.model类 public class UserInfo { public int I ...

  4. spring依赖注入实例

    依赖注入: BL public class T01BL implements Serializable { private static final Log logger = LogFactory.g ...

  5. 深度理解依赖注入(Dependence Injection)

    前面的话:提到依赖注入,大家都会想到老马那篇经典的文章.其实,本文就是相当于对那篇文章的解读.所以,如果您对原文已经有了非常深刻的理解,完全不需要再看此文:但是,如果您和笔者一样,以前曾经看过,似乎看 ...

  6. [转]深度理解依赖注入(Dependence Injection)

    http://www.cnblogs.com/xingyukun/archive/2007/10/20/931331.html 前面的话:提到依赖注入,大家都会想到老马那篇经典的文章.其实,本文就是相 ...

  7. [学习笔记]Spring依赖注入

    依赖: 典型的企业应用程序不可能由单个对象(在spring中,也可称之bean)组成,再简单的应用也是由几个对象相互配合工作的,这一章主要介绍bean的定义以及bean之间的相互协作. 依赖注入: s ...

  8. 依赖注入与Service Locator

    为什么需要依赖注入? ServiceUser是组件,在编写者之外的环境内被使用,且使用者不能改变其源代码. ServiceProvider是服务,其类似于ServiceUser,都要被其他应用使用,不 ...

  9. Console app 里的依赖注入及其实例生命周期

    依赖注入是 ASP.NET Core 里的核心概念之一,我们平常总是愉快地在Startup类的ConfigureServices方法里往IServiceCollection里注册各种类型,以致有一些同 ...

随机推荐

  1. 基于numpy实现矩阵计算器

    要求 制作一个Python的矩阵计算器: ① 程序提供任意两矩阵的加.乘法运算:方阵的行列式计算.逆矩阵计算.特征分解:任意矩阵的转置等计算功能,可自行添加功能 ② 从控制台通过键盘获取数据并完成以上 ...

  2. 版本问题---Bazel与tensorflow的对应关系

    源码安装tf的时候,会用到Bazel,版本不对应,后面会引起好多麻烦. echo "deb [arch=amd64] http://storage.googleapis.com/bazel- ...

  3. 神奇搜索算法A*

    A* A*是一种启发式搜索算法,又叫最佳图搜索算法. 何谓启发式搜索? 众所周知,计算机在执行搜索算法时是没开上帝视角的.因此,在搜索时,往往显得盲目,把所有可能的状态全部遍历,这种搜索我们统称盲目搜 ...

  4. HTTP 状态码(常见及分析)

    首先得明白状态码的几个大类: 状态码 响应类别 出现原因 1XX 信息性状态码(Informational) 服务器正在处理请求 2XX 成功状态码(Success) 请求已正常处理完毕 3XX 重定 ...

  5. 男上加男团队对 修!咻咻! 团队,云打印 团队的Beta产品测试报告

    男上加男团队对 修!咻咻! 团队的Beta产品测试报告 男上加男团队对云打印 团队的Beta产品测试报告 6.2 1.57分补充 睡觉前看终于看到发布的在线版本 重新测试了一下 卡在注册这关 无法收到 ...

  6. 16 关于webpack和npm中几个问题的说明

    1.json里面不能写注释 2.'webpack-dev-server'不是内部或外部命令,也不是可运行的程序或批处理文件. 注意:webpack-dev-server包只需要本地安装就行,不用全局安 ...

  7. 在使用rem布局的时候,遇到的样式排版混乱问题,

    在使用rem布局的时候,遇到的样式排版混乱问题, 问题1:设置字体为rem单位,但是没有设置line-height为100%, 即    * {             line-height: 1; ...

  8. mali tbr Forward Pixel Kill

    https://community.arm.com/cfs-file/__key/communityserver-blogs-components-weblogfiles/00-00-00-20-66 ...

  9. [bzoj 1471] 不相交路径 (容斥原理)

    题目描述 给出一个N(n<=150)N(n<=150)N(n<=150)个结点的有向无环简单图.给出444个不同的点aaa,bbb,ccc,ddd,定义不相交路径为两条路径,两条路径 ...

  10. BZOJ 3162 / Luogu P4895: 独钓寒江雪 树hash+DP

    题意 给出一棵无根树,求本质不同的独立集数模100000000710000000071000000007的值. n≤500000n\le 500000n≤500000 题解 如果是有根树就好做多了.然 ...