使用Unity的依赖注入:解决依赖关系

恬静的小魔龙 2020-12-10 17:08:30 9851

介绍

控制反转(IOC)和依赖注入(DI)协同工作,使我们的应用程序更松散地耦合并且易于扩展。

本文是为了展示紧密耦合的应用程序存在的问题,以及如何使应用程序松散耦合并通过依赖注入实现控制反转,依赖关系问题的解决方法是可以在项目架构中使用三层架构或者n层架构来解决。

问题

在三层架构上开发时,可能会出现应用程序较高级别的模块/层/类依赖于较低级别的模块的情况。

这是一个简单的基本示例,应用程序在三层体系结构中具有表示层,业务层和数据访问层。表示层与业务层对话,业务层又与数据访问层对话。

要在表示层中调用业务层的方法,需要在表示层中创建业务层类的对象,然后调用所需的方法。

要在数据访问层中调用类的方法,业务层类需要在数据访问层中创建类的对象,然后调用该方法。

业务层与数据访问层进行通信,因为由于架构限制和灵活性,

image-20201210164706979

表示层本身不创建业务层的对象,而是将这种责任委托给任何第三方容器,并且当需要实例时,则该对象是为表示层创建的。

例如,注入依赖和业务层的方法可以通过它进行访问。业务层与数据访问层之间的通信也是如此。Unity充当此对象创建和依赖关系解析的容器。

我们可以解决业务层的依赖性,但是数据访问层呢?我们如何解决业务层中数据访问层的依赖性?我们是否需要在业务层中再次编写容器?在本教程中,我们将使用一种非常简单的方法来执行此操作。让我们写一些代码,首先看问题,然后逐步解决。

创建没有DI的应用程序

由于本文的重点是要以简单的方式说明如何使用统一性来解决依赖关系的依赖关系,因此我们将不会创建大型应用程序,而是创建一个小型的三层控制台应用程序,每一层只有一个类,而一个每个类中的方法用于概念验证。一个人可以创建MVC或任何其他应用程序,也可以在任何大型应用程序中将此解决方案用作POC。

步骤1.打开Visual Studio。创建一个控制台应用程序,并将其命名为Presentation,添加一个类库,并将其命名为Business,添加另一个类库,并将其命名为Data。因此,我们将这些层与这些类库分开,其中业务类库用于业务层,数据类库用于数据访问层。解决方案应如下所示。我已将解决方案命名为“ WithoutDI”。

image-20201210164706979

从业务和数据类库中删除名为Class1.cs的默认类。

image-20201210164706979

第2步。现在添加一个BusinessClass在业务项目和DataClass数据项目中命名的类。在中添加一个名为的方法GetDataDataClass并从该方法返回任何字符串。因此,该类的实现如下所示:

namespace Data
{
  public class DataClass
  {
    public string GetData()
    {
      return "From Data";
    }
  }
}

第三步 现在,我们的要求是将这些数据从GetData方法中获取到我们的表示层中并在控制台上显示。直接的方法是,我们从表示层的Program.cs调用Business类的方法,而该类又将GetData从调用此方法DataClass。为此,我们首先需要在Presentation控制台应用程序中添加Business项目的引用,并在Business类库中添加Data项目的引用。

image-20201210164706979

第四步。即时访问GetData数据项目的方法是DataClassBusinessClass,我们首先需要的实例DataClassBusinessClass。我们在BusinessClass构造函数中创建实例,然后在Business类中应该有一个方法,然后使用该DataClass实例来调用GetData方法以返回字符串。因此,实现如下所示。

using Data;
namespace Business
{
  public class BusinessClass
  {
    DataClass _dataClass;
    public BusinessClass()
    {
       _dataClass = new DataClass();
    }
    public string GetBusinessData()
    {
      return _dataClass.GetData();
    }
  }
}

第五步 现在要从表示层对该类的GetBusinessData方法进行Business分类,我们再次需要BusinessClass在表示层中创建一个类的实例并调用该方法。因此,请BusinessClass在main方法或Program.cs中创建一个实例,然后GetBusinessData按如下所示通过该实例进行调用。

using System;

namespace Presentation
{
  class Program
  {
    static void Main(string[] args)
    {
      Business.BusinessClass businessClass = new Business.BusinessClass();
      string data = businessClass.GetBusinessData();
      Console.WriteLine(data);
      Console.ReadLine();
    }
  }
}

运行应用程序时,将获得所需的输出,如下所示。

image-20201210164706979

这意味着我们的数据来自DataClass的GetData()方法。我们的应用程序运行良好,但问题是它遵循SOLID原则吗?我的应用是否松散耦合?如果我们仔细观察并尝试回答这些问题,答案是否定的。

应用程序违反了单一职责原则,类还做额外的工作来创建相关类的对象。

应用程序违反了开闭原则,因为如果构造函数实现在类中发生更改,则调用类将不得不对其代码进行修改,以使更改可能中断应用程序或始终不可行。

应用违反了接口隔离原则以及应用程序的可测试性非常差,因为我们不具备充当合同的类定义的接口。

应用程序违反了依赖倒置原则,因为类依赖于其他类实例,并直接在类中创建实例。想象一下这样一种情况:DataClass的实例创建失败,由于这种情况,Business类构造函数也将无法初始化并引发错误。这根本是不能接受的。

因此,违反了五分之四的原则。而且,该应用程序无法进行单元测试。该应用程序紧密耦合。让我们尝试在Unity Framework的帮助下一个接一个地解决这些问题,并继续了解如何解决依赖关系的依赖关系。

在应用程序中介绍Unity

是时候在应用程序中引入Unity了。Unity Framework是一个Nuget软件包,因此我们将通过Package Manager Console安装它。在Visual Studio中打开“工具”,导航到Nuget软件包管理器,然后打开“软件包管理器控制台”,如下图所示。

image-20201210164706979

打开软件包管理器控制台后,键入命令Install-Package Unity以获取最新版本的Unity框架。确保在控制台中将Presentation项目选择为默认项目。当我们按Enter键输入命令后,将为演示项目安装Unity软件包。我的Unity最新版本是5.7.3。它可能会有所不同,并且取决于您基于发布的最新版本实施此操作的时间。

image-20201210164706979

将在应用程序中创建一个packages.config文件,在该文件中将引用此Unity软件包,并且该软件包本身将下载到packages文件夹中的文件系统中,并且在Presentation项目中将自动引用该文件。

image-20201210164706979

通过Unity进行依赖注入

让我们在Presentation层中进行一些修改,而不是BusinessClass从Main方法中调用方法,而是添加一个名为Initiator的类,然后从那里调用该方法。我们本可以做得早一些,但是错过了,所以现在就开始做。这只是为了更清楚地了解我们如何解决依赖关系。因此,添加一个名为的类Initiator并添加以下代码。

using Business;
namespace Presentation
{
  public class Initiator
  {
    BusinessClass _businessClass;
    public Initiator()
    {
      _businessClass = new BusinessClass();
    }
    public string FetchData()
    {
      return _businessClass.GetBusinessData();
    }
  }
}

image-20201210164706979

Program.cs的代码如下:

using System;

namespace Presentation
{
  class Program
  {
    static void Main(string[] args)
    {
      Initiator initiator = new Initiator();
      string data = initiator.FetchData();
      Console.WriteLine(data);
      Console.ReadLine();
    }
  }
}

输出和逻辑是一样的,我们刚搬到几件从Program.cs文件Initiator类,并在Main方法现在使用此启动类的实例来获取数据。

因此,现在的问题是,Program.cs Main()方法创建Initiator类的实例,Initiator类创建的实例BusinessClass,而Business类创建的实例DataClass以获取数据。让我们将实例创建的责任交给其他人,例如Unity,然后通过Interfaces将依赖项注入已定义的构造函数中。是的,我们现在需要创建接口并定义相应的方法。让我们一步一步地做。

步骤1。创建IData在Data项目中命名的接口,并在其中定义GetData ()方法,如下所示。

namespace Data
{
  public interface IData
  {
    string GetData();
  }
}

现在,DataClass如下所示实现此接口:

namespace Data
{
  public class DataClass : IData
  {
    public string GetData()
    {
      return "From Data";
    }
  }
}

第2步。现在进入Business项目,定义一个名为的接口,IBusiness并在其中定义GetBusinessData()方法,如下所示。

namespace Business
{
  public interface IBusiness
  {
    string GetBusinessData();
  }
}

BusinessClass如下实现该接口:

using Data;
namespace Business
{
  public class BusinessClass
  {
    DataClass _dataClass;
    public BusinessClass()
    {
       _dataClass = new DataClass();
    }
    public string GetBusinessData()
    {
      return _dataClass.GetData();
    }
  }
}

现在,让我们假设Business类将不再承担创建对象的责任,并且此依赖项将BusinessClass在运行时注入到构造函数中,并且在运行时将有一个对象来调用GetData方法DataClass。因此,我们的Business类如下所示:

using Data;

namespace Business
{
  public class BusinessClass : IBusiness
  {
    IData _dataClass;

    public BusinessClass(IData dataClass)
    {
      _dataClass = dataClass;
    }
   public string GetBusinessData()
    {
      return _dataClass.GetData();
    }
  }
}

这很简单。我们在运行时的构造函数中声明IData实例局部变量和期望的IData实例BusinessClass,然后将其分配给局部变量,并在方法中GetBusinessData(),使用此局部变量(假设将事先对其进行初始化),我们将其称为GetData()方法。请注意,此处没有“ new”关键字用于创建实例,现在BusinessClass也不承担创建对象的责任DataClass。此代码仍然无法正常工作。我们还需要在表示层中做一些作业。

第三步 转到Presentation项目,并与在Business层中针对进行相同的操作DataClass,但现在在InitiatorBusiness层中进行类。因此,我们的启动器类如下所示:

using Business;
namespace Presentation
{
  public class Initiator
  {
    IBusiness _businessClass;
    public Initiator(IBusiness businessClass)
    {
      _businessClass = businessClass;
    }
    public string FetchData()
    {
      return _businessClass.GetBusinessData();
    }
  }
}

这也很容易理解。我们声明IBusiness实例局部变量,然后在构造函数中,假定我们获得了业务类的预初始化实例,并将其分配给局部变量并GetBusinessData()通过该实例调用方法。请注意,我们从此处完全删除了“ new”关键字,并且此类也不承担创建对象的责任。我们的工作仍未完成。Main()在program.cs类中,我们仍有方法通过“ new”关键字创建Initiator类的对象并对该FetchData()方法进行分类。我们的目标是完全摆脱这个“新”关键字,并确保类遵循“单一职责原则”。

第四步。添加一个DependencyInjector在Presentation项目中命名的新类,并向其中添加以下代码:

using Unity;
using Unity.Lifetime;

namespace Presentation
{
  public static class DependencyInjector
  {
    private static readonly UnityContainer UnityContainer = new UnityContainer();
    public static void Register<I, T>() where T : I
    {
      UnityContainer.RegisterType<I, T>(new ContainerControlledLifetimeManager());
    }
    public static void InjectStub<I>(I instance)
    {
      UnityContainer.RegisterInstance(instance, new ContainerControlledLifetimeManager());
    }
    public static T Retrieve<T>()
    {
      return UnityContainer.Resolve<T>();
    }
  }
}

此类DependencyInjector是一个通用类,负责解析类型和注册类型。它利用Unity来注册任何类型的实例。我们本可以将其特定于我们的类和接口,但是最好是通用一些,以便将来将来在我们的应用程序中使用任何新类型。

要定义和注册我们的具体类型,例如,IBusinessBusiness类,添加一个名为引导程序另一个类的演示项目。该类充当其名称,即我们应用程序的Bootstrapper,即第一类,在应用程序加载后立即解决依赖关系。因此,添加Bootstrapper.cs并向其中添加以下代码。

using Business;
namespace Presentation
{
  public static class Bootstrapper
  {
    public static void Init()
    {
      DependencyInjector.Register<IBusiness, BusinessClass>();
    }
  }
}

此类仅调用类的泛型Register方法DependencyInjector并请求解析的依赖关系BusinessClass,以便在需要时Business将提供类的实例。

第五步 在该Main方法中,现在只需调用该类的Init()方法Bootstrapper来注册类型并通过该类的Retrieve方法获取Initiator类的实例DependencyInjector。我们需要这个Initiator实例来调用FetchDataInitiator类的方法。因此,以下是我们的Program.cs类的代码:

using System;
namespace Presentation
{
  public class Program
  {
    static void Main(string[] args)
    {
      Bootstrapper.Init();
      Initiator initiator = DependencyInjector.Retrieve<Initiator>();
      string data = initiator.FetchData();
      Console.WriteLine(data);
      Console.ReadLine();
    }
  }
}

简单,清晰。注意,我们从这里也摆脱了“ new”关键字,该类的主要职责是初始化和应用程序启动。

简而言之,我们执行了以下操作:

  1. 引入了通用DependencyInjector类来处理注册/解析类型。
  2. 引入了在其方法Bootstrapper中调用DependencyInjector类方法Init()以注册具体类型的类。
  3. Program.csMain方法中调用类的Init方法,以便在应用程序启动时注册并解析类型。Bootstrapper
  4. FetchData()从启动器类调用了我们的方法。

对我来说,一切看起来都很好,我们都可以运行该应用程序。

第六步 点击F5运行应用程序。我们假设一切顺利,因为我们处理了依赖项,但是出现了以下运行时错误。

image-20201210164706979

当您检查InnerException时,该错误是明显的并且不言自明。它说:- InnerException {"The current type, Data.IData, is an interface and cannot be constructed. Are you missing a type mapping?"} System.Exception {System.InvalidOperationException}

你是对的。我们把注册的保健BusinessClassIBusiness。我们负责解决Initiator类实例,但是那又怎么样DataClass呢?记住,BusinessClass当我们尝试获取DataClass实例时,我们也从构造函数中删除了“ new” ,但我们尚未注册该类型,也未解析该类型以在需要时使用。

解决方案是我们也需要为其注册类型DataClass,以便在需要时获取其实例。但是怎么做呢?

我们无法在表示层访问或引用数据项目,因为它将违反我们的分层体系结构。

因此,唯一的地方是业务项目。但是同样,如果我们像在Presentation层中一样完全这样做,那将是没有意义的,并且在应用程序中将有代码重复和两个引导程序。让我们探讨如何克服这种情况。

使用Unity扩展解决依赖关系的依赖关系

Unity提供了这种灵活性,可以通过UnityContainerExtensions来解决依赖关系的依赖性,而不会破坏结构 ,如下所示:

namespace Unity.Extension
{
  public abstract class UnityContainerExtension: IUnityContainerExtensionConfigurator
  {
    protected UnityContainerExtension();

    public IUnityContainer Container { get; }
    protected ExtensionContext Context { get; }

    public void InitializeExtension(ExtensionContext context);
    public virtual void Remove();
    protected abstract void Initialize();
  }
}

此类是Unity.Abstractions dll的一部分,包含一个名为的抽象方法Initialize()。抽象方法意味着我们可以重写此方法并编写自定义初始化逻辑。所以,让我们这样做。

创建一个DependencyOfDependencyExtension在业务项目中命名的类。您可以根据自己的选择命名课程。为了便于理解,我以这种方式命名该类。以与演示项目相同的方式在业务项目中安装Unity软件包。不要忘记Business 在软件包管理器控制台中选择默认项目,如下所示。

image-20201210164706979

步骤1。一旦像表示层一样将Unity添加到项目中,则将以下名称空间添加到我们新创建的DependencyOfDependencyExtension类中

using Data;
using Unity;
using Unity.Extension;

现在,从Assembly命名空间中UnityContainerExtension存在的类继承该类,并按如下所示重写其Initialize方法以向Type注册类型。Unity.Extension``Unity.Abstractions``DataClass``IData

using Data;
using Unity;
using Unity.Extension;

namespace Business
{
  public class DependencyOfDependencyExtension : UnityContainerExtension
  {
    protected override void Initialize()
    {
      Container.RegisterType<IData,DataClass>();
    }
  }
}

第2步。现在,我们必须使我们的表示层了解此扩展。因此,转到表示层,并在DependencyInjector类中添加一个名为的新通用方法,以AddExtension()添加新的扩展,如下所示,

public static void AddExtension<T>() where T : UnityContainerExtension
    {
      UnityContainer.AddNewExtension<T>();
    }

不要忘记使用Unity.Extension; namespace添加此类以访问UnityContainerExtension该类。如下所示。

using Unity;
using Unity.Extension;
using Unity.Lifetime;

namespace Presentation
{
  public static class DependencyInjector
  {
    private static readonly UnityContainer UnityContainer = new UnityContainer();
    public static void Register<I, T>() where T : I
    {
      UnityContainer.RegisterType<I, T>(new ContainerControlledLifetimeManager());
    }
    public static void InjectStub<I>(I instance)
    {
      UnityContainer.RegisterInstance(instance, new ContainerControlledLifetimeManager());
    }
    public static T Retrieve<T>()
    {
      return UnityContainer.Resolve<T>();
    }
    public static void AddExtension<T>() where T : UnityContainerExtension
    {
      UnityContainer.AddNewExtension<T>();
    }
  }
}

第三步

我们差不多完成了,现在在Bootstrapper的类Init方法中为此扩展名添加具体类型,如下所示:

using Business;
namespace Presentation
{
  public static class Bootstrapper
  {
    public static void Init()
    {
      DependencyInjector.Register<IBusiness, BusinessClass>();
      DependencyInjector.AddExtension<DependencyOfDependencyExtension>();
    }
  }
}

完成工作后,只需运行应用程序,我们将看到所需的输出,如下所示:

image-20201210164706979

这样,我们将依赖项的依赖项(即依赖项(BusinessClass)的依赖项(DataClass))解析为表示层。

概要

让我们快速总结一下我们在应用程序中所做的工作以及完整的教程。

  1. 我们创建了一个具有表示层,业务层和数据访问层的三层基本体系结构。该架构紧密耦合并违反了SOLID原则。
  2. 我们引入了Unity来解决表示层和业务层之间的依赖性。此后,我们使用构造函数注入通过接口注入依赖项。
  3. 我们使用Unity扩展解决了依赖关系的依赖关系,这样就不会破坏我们的架构。
  4. 我们摆脱了“new”关键字,并将对象创建的责任委托给Unity容器。
  5. 我们通过依赖注入实现了控制反转。
  6. 每个类都有单一的责任,可以扩展而不是修改。
  7. 我们在接口的帮助下实现了抽象。

结论

在本教程中,我们尝试在简单示例的帮助下学习依赖项注入。所举的示例非常基础,但是该概念可以应用于MVC,Web API或任何企业级应用程序中,以解决依赖关系并通过依赖关系注入实现控制反转。我们还通过统一扩展解决了依赖性的依赖性。可能会有更多方法来解决依赖关系,例如属性注入或服务定位器。我们在应用程序中使用了构造函数注入。可以使用其他容器代替Unity。

声明:本文内容由易百纳平台入驻作者撰写,文章观点仅代表作者本人,不代表易百纳立场。如有内容侵权或者其他问题,请联系本站进行删除。
红包 58 9 评论 打赏
评论
0个
内容存在敏感词
手气红包
    易百纳技术社区暂无数据
相关专栏
置顶时间设置
结束时间
删除原因
  • 广告/SPAM
  • 恶意灌水
  • 违规内容
  • 文不对题
  • 重复发帖
打赏作者
易百纳技术社区
恬静的小魔龙
您的支持将鼓励我继续创作!
打赏金额:
¥1易百纳技术社区
¥5易百纳技术社区
¥10易百纳技术社区
¥50易百纳技术社区
¥100易百纳技术社区
支付方式:
微信支付
支付宝支付
易百纳技术社区微信支付
易百纳技术社区
打赏成功!

感谢您的打赏,如若您也想被打赏,可前往 发表专栏 哦~

举报反馈

举报类型

  • 内容涉黄/赌/毒
  • 内容侵权/抄袭
  • 政治相关
  • 涉嫌广告
  • 侮辱谩骂
  • 其他

详细说明

审核成功

发布时间设置
发布时间:
是否关联周任务-专栏模块

审核失败

失败原因
备注
拼手气红包 红包规则
祝福语
恭喜发财,大吉大利!
红包金额
红包最小金额不能低于5元
红包数量
红包数量范围10~50个
余额支付
当前余额:
可前往问答、专栏板块获取收益 去获取
取 消 确 定

小包子的红包

恭喜发财,大吉大利

已领取20/40,共1.6元 红包规则

    易百纳技术社区