在.NET中实现DI的“正确”方法是什么?


22

我希望在相对较大的应用程序中实现依赖注入,但是对此没有经验。我研究了概念和可用的IoC和依赖注入程序的一些实现,例如Unity和Ninject。但是,有一件事让我难以理解。如何在应用程序中组织实例创建?

我正在考虑的是,我可以创建一些特定的工厂,其中将包含为某些特定的类类型创建对象的逻辑。基本上是一个静态类,带有一个调用此类中静态内核实例的Ninject Get()方法的方法。

它是在我的应用程序中实现依赖注入的正确方法还是我应该根据其他原则实现它?


5
我不认为存在正道,但很多正确的方式,这取决于你的项目。我会坚持其他建议并建议构造函数注入,因为您将能够确保每个依赖项都注入到一个点上。另外,如果构造函数签名过长,您会知道这些类做得太多。
Paul Kertscher

不知道您正在构建哪种.net项目,就很难回答。例如,对于WPF来说,好的答案可能对MVC来说是不好的答案。
JMK

最好将所有依赖项注册组织到解决方案或每个项目的DI模块中,并可能将其用于某些测试,具体取决于要测试的对象。哦,是的,您当然应该使用构造函数注入,其他内容用于更高级/更疯狂的用法。
Mark Rogers

Answers:


30

暂时不要考虑要使用的工具。您可以在没有IoC容器的情况下进行DI。

要点:Mark Seemann在.Net中有一本关于DI的很好的书

第二:组成根。确保整个设置在项目的入口点完成。您的其余代码应了解注入,而不是所使用的任何工具。

第三:构造函数注入是最可能的方法(在某些情况下,您不希望使用它,但数量并不多)。

第四:研究使用lambda工厂和其他类似功能,以避免仅出于注入目的而创建不必要的接口/类。


5
所有极好的建议;特别是第一部分:学习如何做纯DI,然后开始研究可能减少该方法所需样板代码数量的IoC容器。
大卫·阿诺

6
...或一起跳过IoC容器-保留静态验证的所有好处。
书斋

在进入DI之前,我喜欢这个建议是一个很好的起点,实际上,我有Mark Seemann的书。
史努比(Snoop)

我可以第二个回答。通过将部分引导程序逻辑分解为组成该应用程序的模块,我们成功地在一个很大的应用程序中使用了bad-mans-DI(手写引导程序)。
wigy

1
小心。使用lambda注入可以快速达到疯狂状态,尤其是在测试引起的设计损坏类型中。我知道。我已经走了那条路。
jpmc26 2016年

13

您的问题有两个部分-如何正确实现DI,以及如何重构大型应用程序以使用DI。

第一部分由@Miyamoto Akira(尤其是阅读Mark Seemann的“ .net中的依赖注入”书)的建议得到了很好的回答。Marks 博客也是一个很好的免费资源。

第二部分要复杂得多。

第一步是将所有实例都简单地移到类的构造函数中,而不是注入依赖关系,只需确保仅new在构造函数中调用即可。

这将突出显示您一直在犯的所有违反SRP的行为,因此您可以开始将班级分解为较小的协作者。

下一个问题将是依赖于运行时参数进行构造的类。通常,您可以通过创建简单工厂来解决此问题,通常使用Func<param,type>,在构造函数中对其进行初始化,然后在方法中对其进行调用。

下一步将是为您的依赖关系创建接口,并将除这些接口之外的其他构造函数添加到您的类中。您的无参数构造函数将更新具体实例,并将其传递给新的构造函数。这通常称为“ B * stard注射”或“穷人DI”。

这将使您能够进行一些单元测试,如果这是重构的主要目标,则可能是您停止的地方。新代码将使用构造函数注入编写,但是您的旧代码可以继续按编写的方式工作,但仍可以进行测试。

您当然可以走得更远。如果打算使用IOC容器,则下一步可能是将对无new参数构造函数的所有直接调用替换为对IOC容器的静态调用,本质上是将其用作服务定位符。

这将抛出更多情况下的运行时构造函数参数要像以前一样处理。

完成此操作后,您可以开始删除无参数构造函数,并将其重构为纯DI。

最终,这将需要大量工作,因此请确保您决定要这么做的原因,并确定将受益于重构的代码库优先级


3
感谢您的详尽回答。您给了我一些有关如何解决我所面临的问题的想法。该应用程序的整个体系结构已经考虑了IoC。我想要使​​用DI的主要原因不是什至是单元测试,它是一种奖励,而是一种功能,它可以为我的应用程序的核心中定义的不同接口交换不同的实现,而无需付出任何努力。有问题的应用程序在不断变化的环境中工作,我经常不得不交换应用程序的一部分以根据环境的变化使用新的实现。
user3223738

1
很高兴我可以提供帮助,是的,我同意DI的最大优点是松散耦合以及由此带来的轻松重新配置的能力,而单元测试是一个很好的副作用。
史蒂夫

1

首先,我想提到的是,通过重构现有项目而不是开始新项目,您正在使自己的工作更加困难。

您说这是一个大型应用程序,因此请选择一个小的组件开始。最好是一个“叶节点”组件,其他任何组件都不使用。我不知道此应用程序上自动化测试的状态是什么,但是您将破坏该组件的所有单元测试。所以为此做好准备。步骤0为您要修改的组件编写集成测试(如果尚不存在)。作为最后的选择(没有测试基础结构;没有用于编写它的插件),找出一系列手动测试,即可验证此组件是否正常运行。

声明DI重构目标的最简单方法是,您要从此组件中删除“ new”运算符的所有实例。这些通常分为两类:

  1. 不变成员变量:这些变量仅设置一次(通常在构造函数中设置),并且在对象的生存期内不会重新分配。对于这些,您可以将对象的实例注入到构造函数中。通常,您不负责处理这些对象(我不想在这里永远不要说,但是您实际上不应该承担那种责任)。

  2. 变量成员变量/方法变量:这些变量将在对象的生存期内的某个时间点收集垃圾。对于这些,您需要将一个工厂注入类中以提供这些实例。您负责处理工厂创建的对象。

您的IoC容器(听起来很像)将负责实例化这些对象并实现您的工厂接口。无论使用什么组件,您都需要了解IoC容器,以便它可以检索您的组件。

完成上述步骤后,您将可以从所选组件的DI中获得任何希望获得的好处。现在是添加/修复那些单元测试的好时机。如果存在现有的单元测试,则必须决定是否要通过注入真实对象或使用模拟编写新的单元测试来将它们修补在一起。

“简单地”对应用程序的每个组件重复上述操作,随即向上移动对IoC容器的引用,直到只有主用户需要知道它。


1
好的建议:从一个小的组件开始,而不是“大重写”
Daniel Hollinrake

0

正确的方法是使用构造函数注入(如果使用)

我正在考虑的是,我可以创建一些特定的工厂,其中将包含为某些特定的类类型创建对象的逻辑。基本上是一个静态类,带有一个调用此类中静态内核实例的Ninject Get()方法的方法。

那么您最终将获得服务定位器,而不是依赖项注入。


当然。构造函数注入。假设我有一个接受接口实现作为参数之一的类。但是我仍然需要创建接口实现的实例,并将其传递给某个地方的构造函数。最好是一些集中的代码。
user3223738

2
不,在初始化DI容器时,必须指定接口的实现,然后DI容器将创建实例并注入到构造函数中。
低飞鹈鹕

就个人而言,我发现过度使用了构造函数注入。我经常看到,注入了10种不同的服务,而实际上只需要一个函数调用,这种方式太过常见了-为什么函数参数的那部分不那么呢?
urbanhusky

2
如果注入了10种不同的服务,那是因为有人违反了SRP,应该将SRP分成较小的组件。
低飞鹈鹕

1
@Fabio问题是什么可以买到你。我还没有看到一个示例,其中有一个很好的设计可以处理一打完全不同的事情。DI唯一要做的就是使所有这些违规行为更加明显。
Voo

0

您说要使用它,但不说明原因。

DI只是提供一种从接口生成凝固物的机制。

这本身来自DIP。如果您的代码已经以这种风格编写,并且您在一个地方生成了结点,那么DI不会给聚会带来任何好处。在这里添加DI框架代码只会使您的代码库膨胀并模糊不清。

假设您确实要使用它,通常会在应用程序的早期设置工厂/制造商/容器(或其他),以便清晰可见。

注意,如果您愿意,可以自己动手,而不是承诺使用Ninject / StructureMap或其他工具。但是,如果您有合理的人员流动,则可能会打滑齿轮以使用公认的框架,或者至少以这种风格来编写框架,这样才不会使学习曲线变得太多。


0

实际上,“正确”的方法是根本不使用工厂,除非绝对没有其他选择(例如在单元测试和某些模拟中-对于生产代码,您不使用工厂)!这样做实际上是一种反模式,应不惜一切代价避免这样做。DI容器背后的全部目的是允许小工具您完成工作。

如前文所述,您希望IoC小工具承担在应用程序中创建各种依赖对象的责任。这意味着让您的DI小工具自己创建和管理各种实例。这就是DI背后的全部要点-您的对象永远都不应知道如何创建和/或管理它们所依赖的对象。否则会破坏松散的耦合。

将现有的应用程序转换为所有DI是一个巨大的步骤,但是要排除这样做的明显困难,您还希望(只是为了让您的生活更轻松)探索一个DI工具,该工具将自动执行大部分绑定(诸如Ninject之类的核心是"kernel.Bind<someInterface>().To<someConcreteClass>()"使接口声明与要用于实现这些接口的具体类相匹配的调用。这些“绑定”调用使DI小工具能够拦截构造函数调用并提供必要的依赖对象实例。一些类的典型构造函数(此处显示伪代码)可能是:

public class SomeClass
{
  private ISomeClassA _ClassA;
  private ISomeOtherClassB _ClassB;

  public SomeClass(ISomeClassA aInstanceOfA, ISomeOtherClassB aInstanceOfB)
  {
    if (aInstanceOfA == null)
      throw new NullArgumentException();
    if (aInstanceOfB == null)
      throw new NullArgumentException();
    _ClassA = aInstanceOfA;
    _ClassB = aInstanceOfB;
  }

  public void DoSomething()
  {
    _ClassA.PerformSomeAction();
    _ClassB.PerformSomeOtherActionUsingTheInstanceOfClassA(_ClassA);
  }
}

请注意,该代码中没有任何地方创建/管理/发布了SomeConcreteClassA实例或SomeOtherConcreteClassB实例。实际上,甚至没有引用任何具体的类。那么...魔术发生在哪里?

在应用程序的启动部分中,发生了以下事情(再次,这是伪代码,但是非常接近真实的(Ninject)东西...):

public void StartUp()
{
  kernel.Bind<ISomeClassA>().To<SomeConcreteClassA>();
  kernel.Bind<ISomeOtherClassB>().To<SomeOtherConcreteClassB>();
}

那里的少量代码告诉Ninject小工具寻找构造函数,对其进行扫描,寻找已经配置为处理的接口实例(即“绑定”调用),然后在任何地方创建并替换具体类的实例实例被引用。

Ninject有一个很好的工具,可以很好地补充Ninject.Extensions.Conventions(还有另一个NuGet包),它将为您完成大部分工作。建立自己的经验并不是要摆脱其将获得的出色学习经验,而是要开始自己,这可能是一种调查的工具。

如果有内存,Unity(以前正式来自Microsoft,现在是一个开源项目)具有执行相同功能的一两个方法调用,其他工具具有类似的帮助器。

无论您选择哪种方式,请务必阅读Mark Seemann的书来进行大量的DI培训,但是,应该指出的是,即使是软件工程界的“大人物”(如Mark)也可能犯下明显的错误-Mark忘了一切Ninject在他的书中,所以这里是专门为Ninject编写的另一个资源。我有它,并且读得很好:掌握依赖注入的Ninject


0

没有“正确的方法”,但是要遵循一些简单的原则:

  • 在应用程序启动时创建合成根
  • 创建合成根之后,将对DI容器/内核的引用扔掉(或至少封装它,以便不能从您的应用程序直接访问它)
  • 不要通过“新建”创建实例
  • 将所有必需的依赖项作为抽象传递给构造函数

就这样。可以肯定的是,这些原则不是法律,但是如果您遵循这些原则,则可以确保您执行DI(如果我做错了,请纠正我)。


那么,如何在运行时创建对象而不使用“ new”并且不知道DI容器?

在使用NInject的情况下,有一个工厂扩展可以创建工厂。当然,创建的工厂仍然具有对内核的内部引用,但是无法从您的应用程序访问。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.