详解C#中的依赖注入和IoC容器

在本文中,我们将通过用C#重构一个非常简单的代码示例来解释依赖注入和IoC容器。

简介:

依赖注入和IoC乍一看可能相当复杂,但它们非常容易学习和理解。

在本文中,我们将通过在C#中重构一个非常简单的代码示例来解释依赖注入和IoC容器。

要求:

构建一个允许用户查看可用产品并按名称搜索产品的应用程序。

第一次尝试:

我们将从创建分层架构开始。使用分层架构有多个好处,但我们不会在本文中列出它们,因为我们关注的是依赖注入。

下面是应用程序的类图:

首先,我们将从创建一个Product类开始:

public class Product
{
  public Guid Id { get; set; }
  public string Name { get; set; }
  public string Description { get; set; }
}

然后,我们将创建数据访问层:

public class ProductDAL
{
  private readonly List<Product> _products;

  public ProductDAL()
  {
    _products = new List<Product>
    {
      new Product { Id = Guid.NewGuid(), Name= "iPhone 9",
             Description = "iPhone 9 mobile phone" },
      new Product { Id = Guid.NewGuid(), Name= "iPhone X",
             Description = "iPhone X mobile phone" }
    };
  }

  public IEnumerable<Product> GetProducts()
  {
    return _products;
  }

  public IEnumerable<Product> GetProducts(string name)
  {
    return _products
      .Where(p => p.Name.Contains(name))
      .ToList();
  }
}

然后,我们将创建业务层:

public class ProductBL
{
  private readonly ProductDAL _productDAL;

  public ProductBL()
  {
    _productDAL = new ProductDAL();
  }

  public IEnumerable<Product> GetProducts()
  {
    return _productDAL.GetProducts();
  }

  public IEnumerable<Product> GetProducts(string name)
  {
    return _productDAL.GetProducts(name);
  }
}

最后,我们将创建UI:

class Program
{
  static void Main(string[] args)
  {
    ProductBL productBL = new ProductBL();

    var products = productBL.GetProducts();

    foreach (var product in products)
    {
      Console.WriteLine(product.Name);
    }

    Console.ReadKey();
  }
}

我们已经写在第一次尝试的代码是良好的工作成果,但有几个问题:

1.我们不能让三个不同的团队在每个层上工作。

2.业务层很难扩展,因为它依赖于数据访问层的实现。

3.业务层很难维护,因为它依赖于数据访问层的实现。

4.源代码很难测试。

第二次尝试:

高级别对象不应该依赖于低级别对象。两者都必须依赖于抽象。那么抽象概念是什么呢?

抽象是功能的定义。在我们的例子中,业务层依赖于数据访问层来检索图书。在C#中,我们使用接口实现抽象。接口表示功能的抽象。

让我们来创建抽象。

下面是数据访问层的抽象:

public interface IProductDAL
{
  IEnumerable<Product> GetProducts();
  IEnumerable<Product> GetProducts(string name);
}

我们还需要更新数据访问层:

public class ProductDAL : IProductDAL

我们还需要更新业务层。实际上,我们将更新业务层,使其依赖于数据访问层的抽象,而不是依赖于数据访问层的实现:

public class ProductBL
{
  private readonly IProductDAL _productDAL;

  public ProductBL()
  {
    _productDAL = new ProductDAL();
  }

  public IEnumerable<Product> GetProducts()
  {
    return _productDAL.GetProducts();
  }

  public IEnumerable<Product> GetProducts(string name)
  {
    return _productDAL.GetProducts(name);
  }
}

我们还必须创建业务层的抽象:

public interface IProductBL
{
  IEnumerable<Product> GetProducts();
  IEnumerable<Product> GetProducts(string name);
}

我们也需要更新业务层:

public class ProductBL : IProductBL

最终我们需要更新UI:

class Program
{
  static void Main(string[] args)
  {
    IProductBL productBL = new ProductBL();

    var products = productBL.GetProducts();

    foreach (var product in products)
    {
      Console.WriteLine(product.Name);
    }

    Console.ReadKey();
  }
}

我们在第二次尝试中所做的代码是有效的,但我们仍然依赖于数据访问层的具体实现:

public ProductBL()
{
  _productDAL = new ProductDAL();
}

那么,如何解决呢?

这就是依赖注入模式发挥作用的地方。

最终尝试

到目前为止,我们所做的工作都与依赖注入无关。

为了使处在较高级别的的业务层依赖于较低级别对象的功能,而没有具体的实现,必须由其他人创建类。其他人必须提供底层对象的具体实现,这就是我们所说的依赖注入。它的字面意思是我们将依赖对象注入到更高级别的对象中。实现依赖项注入的方法之一是使用构造函数进行依赖项注入。

让我们更新业务层:

public class ProductBL : IProductBL
{
  private readonly IProductDAL _productDAL;

  public ProductBL(IProductDAL productDAL)
  {
    _productDAL = productDAL;
  }

  public IEnumerable<Product> GetProducts()
  {
    return _productDAL.GetProducts();
  }

  public IEnumerable<Product> GetProducts(string name)
  {
    return _productDAL.GetProducts(name);
  }
}

基础设施必须提供对实现的依赖:

class Program
{
  static void Main(string[] args)
  {
    IProductBL productBL = new ProductBL(new ProductDAL());

    var products = productBL.GetProducts();

    foreach (var product in products)
    {
      Console.WriteLine(product.Name);
    }

    Console.ReadKey();
  }
}

创建数据访问层的控制与基础设施结合在一起。这也称为控制反转。我们不是在业务层中创建数据访问层的实例,而是在基础设施的中创建它。 Main方法将把实例注入到业务逻辑层。因此,我们将低层对象的实例注入到高层对象的实例中。

这叫做依赖注入。

现在,如果我们看一下代码,我们只依赖于业务访问层中数据访问层的抽象,而业务访问层是使用的是数据访问层实现的接口。因此,我们遵循了更高层次对象和更低层次对象都依赖于抽象的原则,抽象是更高层次对象和更低层次对象之间的契约。

现在,我们可以让不同的团队在不同的层上工作。我们可以让一个团队处理数据访问层,一个团队处理业务层,一个团队处理UI。

接下来就显示了可维护性和可扩展性的好处。例如,如果我们想为SQL Server创建一个新的数据访问层,我们只需实现数据访问层的抽象并将实例注入基础设施中。

最后,源代码现在是可测试的了。因为我们在任何地方都使用接口,所以我们可以很容易地在较低的单元测试中提供另一个实现。这意味着较低的测试将更容易设置。

现在,让我们测试业务层。

我们将使用xUnit进行单元测试,使用Moq模拟数据访问层。

下面是业务层的单元测试:

public class ProductBLTest
{
  private readonly List<Product> _products = new List<Product>
  {
    new Product { Id = Guid.NewGuid(), Name= "iPhone 9",
           Description = "iPhone 9 mobile phone" },
    new Product { Id = Guid.NewGuid(), Name= "iPhone X",
           Description = "iPhone X mobile phone" }
  };
  private readonly ProductBL _productBL;

  public ProductBLTest()
  {
    var mockProductDAL = new Mock<IProductDAL>();
    mockProductDAL
      .Setup(dal => dal.GetProducts())
      .Returns(_products);
    mockProductDAL
      .Setup(dal => dal.GetProducts(It.IsAny<string>()))
      .Returns<string>(name => _products.Where(p => p.Name.Contains(name)).ToList());

    _productBL = new ProductBL(mockProductDAL.Object);
  }

  [Fact]
  public void GetProductsTest()
  {
    var products = _productBL.GetProducts();
    Assert.Equal(2, products.Count());
  }

  [Fact]
  public void SearchProductsTest()
  {
    var products = _productBL.GetProducts("X");
    Assert.Single(products);
  }
}

你可以看到,使用依赖项注入很容易设置单元测试。

IoC容器

容器只是帮助实现依赖注入的东西。容器,通常实现三种不同的功能:

1.注册接口和具体实现之间的映射

2.创建对象并解析依赖关系

3.释放

让我们实现一个简单的容器来注册映射并创建对象。

首先,我们需要一个存储映射的数据结构。我们将选择Hashtable。该数据结构将存储映射。

首先,我们将在容器的构造函数中初始化Hashtable。然后,我们将创建一个RegisterTransient方法来注册映射。最后,我们会创建一个创建对象的方法 Create :

public class Container
{
  private readonly Hashtable _registrations;

  public Container()
  {
    _registrations = new Hashtable();
  }

  public void RegisterTransient<TInterface, TImplementation>()
  {
    _registrations.Add(typeof(TInterface), typeof(TImplementation));
  }

  public TInterface Create<TInterface>()
  {
    var typeOfImpl = (Type)_registrations[typeof(TInterface)];
    if (typeOfImpl == null)
    {
      throw new ApplicationException($"Failed to resolve {typeof(TInterface).Name}");
    }
    return (TInterface)Activator.CreateInstance(typeOfImpl);
  }
}

最终,我们会更新UI:

class Program
{
  static void Main(string[] args)
  {
    var container = new Container();
    container.RegisterTransient<IProductDAL, ProductDAL>();

    IProductBL productBL = new ProductBL(container.Create<IProductDAL>());
    var products = productBL.GetProducts();

    foreach (var product in products)
    {
      Console.WriteLine(product.Name);
    }

    Console.ReadKey();
  }
}

现在,让我们在容器中实现Resolve方法。此方法将解决依赖关系。

Resolve方法如下:

public T Resolve<T>()
{
    var ctor = ((Type)_registrations[typeof(T)]).GetConstructors()[0];
    var dep = ctor.GetParameters()[0].ParameterType;
    var mi = typeof(Container).GetMethod("Create");
    var gm = mi.MakeGenericMethod(dep);
    return (T)ctor.Invoke(new object[] { gm.Invoke(this, null) });
}

然后我们可以在UI中使用如下Resolve方法:

class Program
{
    static void Main(string[] args)
    {
        var container = new Container();
        container.RegisterTransient<IProductDAL, ProductDAL>();
        container.RegisterTransient<IProductBL, ProductBL>();

        var productBL = container.Resolve<IProductBL>();
        var products = productBL.GetProducts();

        foreach (var product in products)
        {
            Console.WriteLine(product.Name);
        }

        Console.ReadKey();
    }
}

在上面的源代码中,容器使用container.Resolve<IProductBL>()方法创建ProductBL类的一个对象。ProductBL类是IProductDAL的一个依赖项。因此,container.Resolve<IProductBL>() 通过自动创建并在其中注入一个ProductDAL对象返回ProductBL类的一个对象。这一切都在幕后进行。创建和注入ProductDAL对象是因为我们用IProductDAL注册了ProductDAL类型。

这是一个非常简单和基本的IoC容器,它向你展示了IoC容器背后的内容。就是这样。我希望你喜欢阅读这篇文章。

以上就是详解C#中的依赖注入和IoC容器的详细内容,更多关于C# 依赖注入和IoC容器的资料请关注我们其它相关文章!

(0)

相关推荐

  • C# 一个WCF简单实例

    WCF实例(带步骤) 复制代码 代码如下: <xmlnamespace prefix ="o" ns ="urn:schemas-microsoft-com:office:office" /> 本篇转自百度文档,自己试过,确实可以用. 以订票为例简单应用wcf 新建一个wcf服务应用程序 在IService1.cs定义服务契约 复制代码 代码如下: namespace WcfDemo { // 注意: 如果更改此处的接口名称 "IService

  • C#利用正则判断输入是否为纯数字、容器类

    容器类.正则表达式在几乎所有编程语言都存在的东西.很常用也很使用.下面用如下的一个控制台小程序说明C#的正则表达式与容器类的应用. 开始直接输出在C#定义好的数据字典Dictionary,这就是Java与Python的HashMap,之后定义一个存int的List,让用户无限输入这个List的元素,输入到#则停止输入,在输入的过程中遇到不是纯输入,则拒绝这个输入.  遍历这个List输出,之后利用C#的另一个容器HashSet为这个List去重. 这个程序的代码如下,其实以上所有的东西都在以前的

  • C#实现根据指定容器和控件名字获得控件的方法

    本文所述为C#实现根据指定容器和控件名字获得控件的方法,在进行C#应用程序设计时有一定的借鉴价值.分享给大家供大家参考借鉴.具体实现方法如下: 功能代码如下: /// <summary> /// 根据指定容器和控件名字,获得控件 /// </summary> /// <param name="obj">容器</param> /// <param name="strControlName">控件名字</

  • C#中HttpWebRequest的用法详解

    本文实例讲述了C#中HttpWebRequest的用法.分享给大家供大家参考.具体如下: HttpWebRequest类主要利用HTTP 协议和服务器交互,通常是通过 GET 和 POST 两种方式来对数据进行获取和提交.下面对这两种方式进行一下说明: GET 方式: GET 方式通过在网络地址附加参数来完成数据的提交,比如在地址 http://www.jb51.net/?hl=zh-CN 中,前面部分 http://www.jb51.net表示数据提交的网址,后面部分 hl=zh-CN 表示附

  • C#中List〈string〉和string[]数组之间的相互转换

    1,从System.String[]转到List<System.String> System.String[] str={"str","string","abc"}; List<System.String> listS=new List<System.String>(str); 2, 从List<System.String>转到System.String[] List<System.Strin

  • C#几种截取字符串的方法小结

    1.根据单个分隔字符用split截取 例如 复制代码 代码如下: string st="GT123_1"; string[] sArray=st.split("_"); 即可得到sArray[0]="GT123",sArray[1]="1"; 2.利用多个字符来分隔字符串 例如 复制代码 代码如下: string str = "GTAZB_JiangjBen_123";string[] sArray = s

  • C#中使用split分割字符串的几种方法小结

    第一种方法: 复制代码 代码如下: string s=abcdeabcdeabcde;string[] sArray=s.Split(c) ;foreach(string i in sArray)Console.WriteLine(i.ToString()); 输出下面的结果:abdeabdeabde 第二种方法: 我们看到了结果是以一个指定的字符进行的分割.使用另一种构造方法对多个字符进行分割: 复制代码 代码如下: string s=abcdeabcdeabcdestring[] sArra

  • C#控制台程序中使用官方依赖注入的实现

    asp.net core 中已经自带了一个官方的依赖注入框架,现在想把它应用到控制台程序中,控制台程序是最简洁的代码结构,摒除了其他一堆嵌入的框架代码,只包含最简洁的入口函数,是学习基础类库框架的最佳选择,为什么最佳,原因很简单,没有其他项的干扰,Demo效果清晰明了,方便写测试代码,调试也顺畅. 1. 业务接口类设计编写 先要写一个测试用的接口和类,我写了一个很简单的计算求和的接口类和方法,方便待会注入演示效果. 我设计的演示接口很简单,IBaseService 基础接口负责生成一个随机的数字

  • asp.net(c#)网页跳转七种方法小结

    ①response.redirect 这个跳转页面的方法跳转的速度不快,因为它要走2个来回(2次postback),但他可以跳 转到任何页面,没有站点页面限制(即可以由雅虎跳到新浪),同时不能跳过登录保护.但速度慢是其最大缺陷!redirect跳转机制:首先是发送一个http请求到客户端,通知需要跳转到新页面,然后客户端在发送跳转请求到服务器端.需要注意的是跳转后内部空间保存的所有数据信息将会丢失,所以需要用到session. 实例 Example that uses Redirect [C#;

  • C#键值对容器的介绍

    StringDictionary:默认key不区分大小写 NameValueCollection:默认key区分大小写 KeyedCollection:不是键值对容器,但是比键值对容器更好用,强烈推荐 命名空间using System.Collections.Specialized System.Collections 命名空间包含接口和类,这些接口和类定义各种对象(如列表.队列.位数组.哈希表和字典)的集合. System.Collections.Generic 命名空间包含定义泛型集合的接口

  • c#的dllimport使用方法详解

    DllImport是System.Runtime.InteropServices命名空间下的一个属性类,其功能是提供从非托管DLL导出的函数的必要调用信息 DllImport属性应用于方法,要求最少要提供包含入口点的dll的名称.DllImport的定义如下: 复制代码 代码如下: [AttributeUsage(AttributeTargets.Method)]public class DllImportAttribute: System.Attribute{public DllImportA

随机推荐