我希望在相对较大的应用程序中实现依赖注入,但是对此没有经验。我研究了概念和可用的IoC和依赖注入程序的一些实现,例如Unity和Ninject。但是,有一件事让我难以理解。如何在应用程序中组织实例创建?
我正在考虑的是,我可以创建一些特定的工厂,其中将包含为某些特定的类类型创建对象的逻辑。基本上是一个静态类,带有一个调用此类中静态内核实例的Ninject Get()方法的方法。
它是在我的应用程序中实现依赖注入的正确方法还是我应该根据其他原则实现它?
我希望在相对较大的应用程序中实现依赖注入,但是对此没有经验。我研究了概念和可用的IoC和依赖注入程序的一些实现,例如Unity和Ninject。但是,有一件事让我难以理解。如何在应用程序中组织实例创建?
我正在考虑的是,我可以创建一些特定的工厂,其中将包含为某些特定的类类型创建对象的逻辑。基本上是一个静态类,带有一个调用此类中静态内核实例的Ninject Get()方法的方法。
它是在我的应用程序中实现依赖注入的正确方法还是我应该根据其他原则实现它?
Answers:
暂时不要考虑要使用的工具。您可以在没有IoC容器的情况下进行DI。
要点:Mark Seemann在.Net中有一本关于DI的很好的书
第二:组成根。确保整个设置在项目的入口点完成。您的其余代码应了解注入,而不是所使用的任何工具。
第三:构造函数注入是最可能的方法(在某些情况下,您不希望使用它,但数量并不多)。
第四:研究使用lambda工厂和其他类似功能,以避免仅出于注入目的而创建不必要的接口/类。
您的问题有两个部分-如何正确实现DI,以及如何重构大型应用程序以使用DI。
第一部分由@Miyamoto Akira(尤其是阅读Mark Seemann的“ .net中的依赖注入”书)的建议得到了很好的回答。Marks 博客也是一个很好的免费资源。
第二部分要复杂得多。
第一步是将所有实例都简单地移到类的构造函数中,而不是注入依赖关系,只需确保仅new
在构造函数中调用即可。
这将突出显示您一直在犯的所有违反SRP的行为,因此您可以开始将班级分解为较小的协作者。
下一个问题将是依赖于运行时参数进行构造的类。通常,您可以通过创建简单工厂来解决此问题,通常使用Func<param,type>
,在构造函数中对其进行初始化,然后在方法中对其进行调用。
下一步将是为您的依赖关系创建接口,并将除这些接口之外的其他构造函数添加到您的类中。您的无参数构造函数将更新具体实例,并将其传递给新的构造函数。这通常称为“ B * stard注射”或“穷人DI”。
这将使您能够进行一些单元测试,如果这是重构的主要目标,则可能是您停止的地方。新代码将使用构造函数注入编写,但是您的旧代码可以继续按编写的方式工作,但仍可以进行测试。
您当然可以走得更远。如果打算使用IOC容器,则下一步可能是将对无new
参数构造函数的所有直接调用替换为对IOC容器的静态调用,本质上是将其用作服务定位符。
这将抛出更多情况下的运行时构造函数参数要像以前一样处理。
完成此操作后,您可以开始删除无参数构造函数,并将其重构为纯DI。
最终,这将需要大量工作,因此请确保您决定要这么做的原因,并确定将受益于重构的代码库优先级
首先,我想提到的是,通过重构现有项目而不是开始新项目,您正在使自己的工作更加困难。
您说这是一个大型应用程序,因此请选择一个小的组件开始。最好是一个“叶节点”组件,其他任何组件都不使用。我不知道此应用程序上自动化测试的状态是什么,但是您将破坏该组件的所有单元测试。所以为此做好准备。步骤0为您要修改的组件编写集成测试(如果尚不存在)。作为最后的选择(没有测试基础结构;没有用于编写它的插件),找出一系列手动测试,即可验证此组件是否正常运行。
声明DI重构目标的最简单方法是,您要从此组件中删除“ new”运算符的所有实例。这些通常分为两类:
不变成员变量:这些变量仅设置一次(通常在构造函数中设置),并且在对象的生存期内不会重新分配。对于这些,您可以将对象的实例注入到构造函数中。通常,您不负责处理这些对象(我不想在这里永远不要说,但是您实际上不应该承担那种责任)。
变量成员变量/方法变量:这些变量将在对象的生存期内的某个时间点收集垃圾。对于这些,您需要将一个工厂注入类中以提供这些实例。您负责处理工厂创建的对象。
您的IoC容器(听起来很像)将负责实例化这些对象并实现您的工厂接口。无论使用什么组件,您都需要了解IoC容器,以便它可以检索您的组件。
完成上述步骤后,您将可以从所选组件的DI中获得任何希望获得的好处。现在是添加/修复那些单元测试的好时机。如果存在现有的单元测试,则必须决定是否要通过注入真实对象或使用模拟编写新的单元测试来将它们修补在一起。
“简单地”对应用程序的每个组件重复上述操作,随即向上移动对IoC容器的引用,直到只有主用户需要知道它。
正确的方法是使用构造函数注入(如果使用)
我正在考虑的是,我可以创建一些特定的工厂,其中将包含为某些特定的类类型创建对象的逻辑。基本上是一个静态类,带有一个调用此类中静态内核实例的Ninject Get()方法的方法。
那么您最终将获得服务定位器,而不是依赖项注入。
实际上,“正确”的方法是根本不使用工厂,除非绝对没有其他选择(例如在单元测试和某些模拟中-对于生产代码,您不使用工厂)!这样做实际上是一种反模式,应不惜一切代价避免这样做。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