1. 问题的提出

开发中,尤其是大型项目的开发中,为了降低模块间、类间的耦合关系,比较提倡基于接口开发,但在实现中也必须面临最终是“谁”提供实体类的问题。Martin Fowler在《Inversion of Control Containers and the Dependency Injection pattern》中也提到了标准的三种实现方式——Constructor Injection、Setter Injection和Interface Injection,很全面的阐释了这个问题。

对于C#而言,由于语法元素上本身要比Java丰富,如何实施注入还有些技巧和特色之处。这方面微软的ObjectBuilder是个不错的教科书,对三种标准方式的实现也都很到位,但就是有些庞大了。

本文中,笔者借鉴Martin Fowler的撰文,也通过一些精简的代码片断向读者介绍C#实现依赖注入的基本技巧。

我有个习惯,每天晚上要看天气预报,就以这个开始好了,先定义待注入对象的抽象行为描述,然后增加一个假的实体类,相关代码和单元测试如下:

C#

using System;
namespace VisionLogic.Training.DependencyInjection.Scenario
{
/// <summary>
/// 抽象注入对象接口
/// </summary>
public interface IWeatherReader
{
string Current { get; }
}
}

C#


using System;
namespace VisionLogic.Training.DependencyInjection.Scenario.Raw
{
/// <summary>
/// 伪造的一个实现类
/// </summary>
class FakeWeatherReader : IWeatherReader
{
public string Current { get { return string.Empty; } }
} /// <summary>
/// 客户程序
/// </summary>
public class Client
{
protected IWeatherReader reader = new FakeWeatherReader();
public virtual string Weather
{
get
{
string current = reader.Current;
switch (current)
{
case "s": return "sunny";
case "r": return "rainy";
case "c": return "cloudy";
default:
return "unknown";
}
}
}
}
}

Unit Test

using Microsoft.VisualStudio.TestTools.UnitTesting;
using VisionLogic.Training.DependencyInjection.Scenario;
using VisionLogic.Training.DependencyInjection.Scenario.Raw;
namespace VisionLogic.Training.DependencyInjection.Scenario.UnitTest.Raw
{
[TestClass]
public class WeatherReaderTest
{
[TestMethod]
public void Test()
{
Client client = new Client();
Assert.AreEqual<string>("unknown", client.Weather);
}
}
}

问题就出现了,虽然美好的愿望是Client仅仅依赖抽象的IWeatherReader,但之前总要和一个实体类“轧”一道,那么实际的效果就是实体类作了修改、重新编译了,Client也要处理,没有真正达到隔离的目的。依赖注入通过引入第三方责任者的方法,相对好的梳理了这个关系,这位重要的角色就是一个Assembler类,他和实体类型打交道,对Client而言他总是可以根据约定,加工出需要的IWeatherReader。

2.进一步的分析

看上去,Client被解放了,但又套住了Assembler,为了尽量让他与实体类间松散些需要做什么呢?

首先要完成自己的职责:可以找到合适的实现类实例,不管是重新构造一个还是找个现成的。
既要根据需要加工接口IWeatherReader,又要让自己尽量不与大量的实体类纠缠在一起,最好的办法就是从.Net Framework中再找到一个“第三方”,这里选中了System.Activator。
还有就是当客户程序调用Assembler的时候,它需要知道需要通过哪个实现类的实例返回,该项工作一方面可以通过一个字典完成,也可以通过配置解决,两者应用都很普遍,怎么选择呢——抽象,提取一个接口,然后都实现。
由于本文主要介绍依赖注入的实现,为了简单起见,采用一个伪造的内存字典方式,而非基于System.Configuration的配置系统实现一个Assembler的协同类。

C# 新增一个用于管理抽象类型——实体类型映射关系的类型ITypeMap

using System;
using System.Collections.Generic;
namespace VisionLogic.Training.DependencyInjection.Scenario
{
/// <summary>
/// 考虑到某些类型没有无参的构造函数,增加了描述构造信息的专门结构
/// </summary>
public class TypeConstructor
{
private Type type;
private object[] constructorParameters; public TypeConstructor(Type type, params object[] constructorParameters)
{
this.type = type;
this.constructorParameters = constructorParameters;
}
public TypeConstructor(Type type) : this(type, null) { }
public Type Type { get { return type; } }
public object[] ConstructorParameters { get { return constructorParameters; } }
} /// <summary>
/// 管理抽象类型与实体类型的字典类型
/// </summary>
public interface ITypeMap
{
TypeConstructor this[Type target]{get;}
}
}

C# 实现一个Assembler类型,为了示例方便,同时实现了一个ITypeMap和IWeatherReader

using System;
using System.Collections.Generic;
namespace VisionLogic.Training.DependencyInjection.Scenario
{
/// <summary>
/// 测试用的实体类
/// </summary>
public class WeatherReaderImpl : IWeatherReader
{
private string weather;
public WeatherReaderImpl(string weather)
{
this.weather = weather;
}
public string Current
{
get { return weather; }
}
} /// <summary>
/// 管理抽象类型与实际实体类型映射关系,实际工程中应该从配置系统、参数系统获得。
/// 这里为了示例方便,采用了一个纯内存字典的方式。
/// </summary>
public class MemoryTypeMap : ITypeMap
{
private Dictionary<Type, TypeConstructor> dictionary = new Dictionary<Type, TypeConstructor>();
public static readonly ITypeMap Instance; /// <summary>
/// Singleton
/// </summary>
private MemoryTypeMap(){}
static MemoryTypeMap()
{
MemoryTypeMap singleton = new MemoryTypeMap();
// 注册抽象类型需要使用的实体类型
// 该类型实体具有构造参数,实际的配置信息可以从外层机制获得。
singleton.dictionary.Add(typeof(IWeatherReader), new TypeConstructor(
typeof(WeatherReaderImpl), "s"));
Instance = singleton;
} /// <summary>
/// 根据注册的目标抽象类型,返回一个实体类型及其构造参数数组
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public TypeConstructor this[Type type]
{
get
{
TypeConstructor result;
if (!dictionary.TryGetValue(type, out result))
return null;
else
return result;
}
}
} public class Assembler<T> where T : class
{
/// <summary>
/// 其实TypeMap工程上本身就是个需要注入的类型,可以通过访问配置系统获得,
/// 这里为了示例的方便,手工配置了一些类型映射信息。
/// </summary>
private static ITypeMap map = MemoryTypeMap.Instance;
public T Create()
{
TypeConstructor constructor = map[typeof(T)];
if (constructor != null)
{
if (constructor.ConstructorParameters == null)
return (T)Activator.CreateInstance(constructor.Type);
else
return (T)Activator.CreateInstance(constructor.Type, constructor.ConstructorParameters);
}
else
return null;
}
}
}

Unit Test

using Microsoft.VisualStudio.TestTools.UnitTesting;
using VisionLogic.Training.DependencyInjection.Scenario;
namespace VisionLogic.Training.DependencyInjection.Scenario.UnitTest
{
[TestClass()]
public class AssemblerTest
{
[TestMethod]
public void Test()
{
IWeatherReader reader = new Assembler<IWeatherReader>().Create();
Assert.IsNotNull(reader);
Assert.AreEqual<System.Type>(typeof(WeatherReaderImpl), reader.GetType());
}
}
}

3.经典方式下的注入实现

在完成了Assembler这个基础环境后,就是怎么注入的问题了,下面是对三种方式的经典方法实现:

3.1 Constructor Injection方式 (构造注入)
Unit Test - Constructor

using Microsoft.VisualStudio.TestTools.UnitTesting;
using VisionLogic.Training.DependencyInjection.Scenario;
namespace VisionLogic.Training.DependencyInjection.Scenario.UnitTest
{
[TestClass]
public class ConstructorInjectionTest
{
class Client
{
private IWeatherReader reader;
public Client(IWeatherReader reader)
{
this.reader = reader;
}
} [TestMethod]
public void Test()
{
IWeatherReader reader = new Assembler<IWeatherReader>().Create();
Client client = new Client(reader);
Assert.IsNotNull(client);
}
}
}

3.2 Setter Injection方式 (Setter注入)

Unit Test - Setter

using Microsoft.VisualStudio.TestTools.UnitTesting;
using VisionLogic.Training.DependencyInjection.Scenario;
namespace VisionLogic.Training.DependencyInjection.Scenario.UnitTest
{
[TestClass]
public class SetterInjectionTest
{
class Client
{
private IWeatherReader reader;
public IWeatherReader Reader
{
get { return reader; }
set { reader = value; }
}
} [TestMethod]
public void Test()
{
IWeatherReader reader = new Assembler<IWeatherReader>().Create();
Client client = new Client();
client.Reader = reader;
Assert.IsNotNull(client.Reader);
}
}
}

3.3 Interface Injection方式 (接口注入)

Unit Test - Interface

using Microsoft.VisualStudio.TestTools.UnitTesting;
using VisionLogic.Training.DependencyInjection.Scenario;
namespace VisionLogic.Training.DependencyInjection.Scenario.UnitTest
{
[TestClass]
public class InterfaceInjectionTest
{
interface IClientWithWeatherReader
{
IWeatherReader Reader { get; set;}
} class Client : IClientWithWeatherReader
{
private IWeatherReader reader; #region IClientWithWeatherReader Members
public IWeatherReader Reader
{
get { return reader; }
set { reader = value; }
}
#endregion
} [TestMethod]
public void Test()
{
IWeatherReader reader = new Assembler<IWeatherReader>().Create();
Client client = new Client();
IClientWithWeatherReader clientWithReader = client;
clientWithReader.Reader = reader;
Assert.IsNotNull(clientWithReader.Reader);
}
}
}

3.4 Attribute Injection方式(用属性注入)

C#还可以通过Attribute注入,Enterprise Library中大量使用这种方式将各种第三方机制加入到类系统中。例如:

  • 运行监控需要的Performance Counter。
  • 用于构造过程的指标信息。
  • 用于日志、密码处理。
  • 等等。

注:Java语言虽然发展比较慢,但在Java 5种也提供了类似的Annotation的机制,换了个名字省去被评估为“抄袭”的嫌疑。)
为了演示方便,下面设计一个应用情景:

Scenario
1、 应用需要一个集中的机制了解系统中实际创建过多少个特定类型对象的实例,用于评估系统的Capacity要求。
2、 为了防止系统资源被用尽,需要控制每类对象实例数量。

怎么实现呢?如下:
1、增加一个内存的注册器,登记每个类已经创建过的实例实例数量。
2、然后给每个类贴个标签——Attribute,让Assembler在生成的对象的时候根据标签的内容把把登记到注册器。

4.1定义抽象业务实体

C#

using System;
namespace VisionLogic.Training.DependencyInjection.Scenario.Attributer
{
/// <summary>
/// 抽象的处理对象
/// </summary>
public interface IObjectWithGuid
{
string Guid { get; set;}
}
}

定义需要注入的限制接口,并用一个Attribute管理它
C#

using System;
namespace VisionLogic.Training.DependencyInjection.Scenario.Attributer
{
/// <summary>
/// 需要注入的用以限制最大数量的接口
/// </summary>
public interface ICapacityConstraint
{
int Max { get;}
} public class CapacityConstraint : ICapacityConstraint
{
private int max;
public CapacityConstraint(){this.max = 0;} // 默认情况下不限制
public CapacityConstraint(int max) { this.max = max; }
public int Max { get { return max; } }
} [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class ConstraintAttribute : Attribute
{
private ICapacityConstraint capacity;
public ConstraintAttribute(int max) { this.capacity = new CapacityConstraint(max); }
public ConstraintAttribute() { this.capacity = null; }
public ICapacityConstraint Capacity { get { return capacity; } }
}
}

Assembler上增加通过Attribute注入限制的响应。
C#

using System;
using System.Collections.Generic;
namespace VisionLogic.Training.DependencyInjection.Scenario.Attributer
{
public class Assembler
{
/// <summary>
/// 登记相关类型对“最大容量”属性的使用情况
/// </summary>
private IDictionary<Type, ConstraintAttribute> attributeRegistry = new Dictionary<Type, ConstraintAttribute>(); /// <summary>
/// 登记每个类型(如须受到“最大容量”属性限制的话),实际已经创建的对象数量
/// </summary>
private IDictionary<Type, int> usageRegistry = new Dictionary<Type, int>();
public T Create<T>() where T : IObjectWithGuid, new()
{
ICapacityConstraint constraint = GetAttributeDefinedMax(typeof(T));
if ((constraint == null) || (constraint.Max <= 0)) // max <= 0 代表是不需要限制数量的。
return InternalCreate<T>();
else
{
if (usageRegistry[typeof(T)] < constraint.Max) // 检查是否超出容量限制
{
usageRegistry[typeof(T)]++; // 更新使用情况注册信息
return InternalCreate<T>();
}
else
return default(T);
}
} // helper method
// 直接生成特定实例,并setter 方式注入其guid。
private T InternalCreate<T>()
where T : IObjectWithGuid, new()
{
T result = new T();
result.Guid = Guid.NewGuid().ToString();
return result;
} /// helper method.
// 获取特定类型所定义的最大数量, 同时视情况维护attributeRegistry 和usageRegistry 的注册信息。
private ICapacityConstraint GetAttributeDefinedMax(Type type)
{
ConstraintAttribute attribute = null;
if (!attributeRegistry.TryGetValue(type, out attribute)) //新的待创建的类型
{
// 填充相关类型的“最大容量”属性注册信息
object[] attributes = type.GetCustomAttributes(typeof(ConstraintAttribute), false);
if ((attributes == null) || (attributes.Length <= 0))
attributeRegistry.Add(type, null);
else
{
attribute = (ConstraintAttribute)attributes[0];
attributeRegistry.Add(type, attribute);
usageRegistry.Add(type, 0); // 同时补充该类型的使用情况注册信息
}
}
if (attribute == null)
return null;
else
return attribute.Capacity;
}
}
}

4.2对方案的测试
C#

using Microsoft.VisualStudio.TestTools.UnitTesting;
using VisionLogic.Training.DependencyInjection.Scenario.Attributer;
namespace VisionLogic.Training.DependencyInjection.Scenario.UnitTest.Attributer
{
[TestClass()]
public class AssemblerTest
{
public abstract class ObjectWithGuidBase : IObjectWithGuid
{
protected string guid;
public virtual string Guid
{
get { return guid; }
set { guid = value; }
}
} [Constraint(2)] // 通过属性注入限制
public class ObjectWithGuidImplA : ObjectWithGuidBase { }
[Constraint(0)] // 通过属性注入限制
public class ObjectWithGuidImplB : ObjectWithGuidBase { }
[Constraint(-5)] // 通过属性注入限制
public class ObjectWithGuidImplC : ObjectWithGuidBase { }
public class ObjectWithGuidImplD : ObjectWithGuidBase { } [TestMethod]
public void Test()
{
Assembler assembler = new Assembler();
for (int i = 0; i < 2; i++)
Assert.IsNotNull(assembler.Create<ObjectWithGuidImplA>());
Assert.IsNull(assembler.Create<ObjectWithGuidImplA>()); // 最多两个
for (int i = 0; i < 100; i++)
Assert.IsNotNull(assembler.Create<ObjectWithGuidImplB>()); // 不限制
for (int i = 0; i < 100; i++)
Assert.IsNotNull(assembler.Create<ObjectWithGuidImplC>()); // 不限制
for (int i = 0; i < 100; i++)
Assert.IsNotNull(assembler.Create<ObjectWithGuidImplD>()); // 不限制
}
}
}

【转】【C#】实现依赖注入的更多相关文章

  1. webapi - 使用依赖注入

    本篇将要和大家分享的是webapi中如何使用依赖注入,依赖注入这个东西在接口中常用,实际工作中也用的比较频繁,因此这里分享两种在api中依赖注入的方式Ninject和Unity:由于快过年这段时间打算 ...

  2. ASP.NET Core 中文文档 第四章 MVC(3.8)视图中的依赖注入

    原文:Dependency injection into views 作者:Steve Smith 翻译:姚阿勇(Dr.Yao) 校对:孟帅洋(书缘) ASP.NET Core 支持在视图中使用 依赖 ...

  3. 在WPF中使用依赖注入的方式创建视图

    在WPF中使用依赖注入的方式创建视图 0x00 问题的产生 互联网时代桌面开发真是越来越少了,很多应用都转到了浏览器端和移动智能终端,相应的软件开发上的新技术应用到桌面开发的文章也很少.我之前主要做W ...

  4. MVVM模式解析和在WPF中的实现(六) 用依赖注入的方式配置ViewModel并注册消息

    MVVM模式解析和在WPF中的实现(六) 用依赖注入的方式配置ViewModel并注册消息 系列目录: MVVM模式解析和在WPF中的实现(一)MVVM模式简介 MVVM模式解析和在WPF中的实现(二 ...

  5. .Net Core MVC 网站开发(Ninesky) 2.3、项目架构调整-控制反转和依赖注入的使用

    再次调整项目架构是因为和群友dezhou的一次聊天,我原来的想法是项目尽量做简单点别搞太复杂了,仅使用了DbContext的注入,其他的也没有写接口耦合度很高.和dezhou聊过之后我仔细考虑了一下, ...

  6. ASP.NET Core中如影随形的”依赖注入”[下]: 历数依赖注入的N种玩法

    在对ASP.NET Core管道中关于依赖注入的两个核心对象(ServiceCollection和ServiceProvider)有了足够的认识之后,我们将关注的目光转移到编程层面.在ASP.NET ...

  7. ASP.NET Core中如影随形的”依赖注入”[上]: 从两个不同的ServiceProvider说起

    我们一致在说 ASP.NET Core广泛地使用到了依赖注入,通过前面两个系列的介绍,相信读者朋友已经体会到了这一点.由于前面两章已经涵盖了依赖注入在管道构建过程中以及管道在处理请求过程的应用,但是内 ...

  8. 模拟AngularJS之依赖注入

    一.概述 AngularJS有一经典之处就是依赖注入,对于什么是依赖注入,熟悉spring的同学应该都非常了解了,但,对于前端而言,还是比较新颖的. 依赖注入,简而言之,就是解除硬编码,达到解偶的目的 ...

  9. angular2系列教程(八)In-memory web api、HTTP服务、依赖注入、Observable

    大家好,今天我们要讲是angular2的http功能模块,这个功能模块的代码不在angular2里面,需要我们另外引入: index.html <script src="lib/htt ...

  10. angularjs 依赖注入--自己学着实现

    在用angular依赖注入时,感觉很好用,他的出现是 为了"削减计算机程序的耦合问题" ,我怀着敬畏与好奇的心情,轻轻的走进了angular源码,看看他到底是怎么实现的,我也想写个 ...

随机推荐

  1. 查看Windows端口及端口关闭方法(转)

    摘自:http://www.hackbase.com/tech/2011-05-17/63766.html 查看Windows端口及端口关闭方法 一.查看已开放的端口: 1.借助系统自带MS-DOS命 ...

  2. npm下载缓慢解决方法

    npm的服务器在国外,拉取npm包的列表.下载包这个过程会比较缓慢.凡是包管理工具基本都有这个问题,例如maven.pip等,这些问题都可以通过配置镜像来解决.阿里巴巴提供了maven库,清华大学有p ...

  3. jQuery动态表格插件 AppendGrid

    AppendGrid是一个jQuery动态表格插件,提供像填写电子表格数据一样在页面去输入结构化数据. 它允许用户在表格里增加/删除/插入/删除行,控制input/select/textarea 提交 ...

  4. 将form表单转化为json数据

    参考地址:https://github.com/hongymagic/jQuery.serializeObject

  5. Web前端开发必备工具推荐

    http://gaohaixian.blog.163.com/blog/static/12326010520114265223489/不管你做前端开发还是网页重构,前端工具都起着非常重要的作用,这里向 ...

  6. 【jQuery】清空表单内容

    function resertForm(){ $(':input','#formId') .not(':button, :submit, :reset, :hidden') .val('') .rem ...

  7. C#中?和:?和??总结

    ?代表可空类型修饰符    引用类型可以使用空引用表示一个不存在的值,而值类型通常不能表示为空.为了使值类型也可为空,就可以使用可空类型?:三元表达式    int a=b>c?b:c 如果b大 ...

  8. System V 消息队列 - 复用消息

    消息队列中的消息结构可以由我们自由定义,具备较强的灵活性.通过消息结构可以共享一个队列,进行消息复用.通常定义一个类似如下的消息结构: #define MSGMAXDAT 1024 struct my ...

  9. HR面 - 十大经典提问

    1.HR:你希望通过这份工作获得什么? 1).自杀式回答:我希望自己为之工作的企业能够重视质量,而且会给做得好的员工予以奖励.我希望通过这份工作锻炼自己,提升自己的能力,能让公司更加重视我. a.“我 ...

  10. Ibatis基础知识:#与$的差别

    背景 Ibatis是一个轻量级.非侵入式的持久层框架,适用于范围较广.较轻便--当然,不管J2EE中哪一个持久层框架,都会基于JDBC(不细究JNDI方式).我们在SqlMap中编写SQL,利用各种S ...