IoC.Resolve 与构造函数注入

Posted

技术标签:

【中文标题】IoC.Resolve 与构造函数注入【英文标题】:IoC.Resolve vs Constructor Injection 【发布时间】:2011-01-11 17:17:36 【问题描述】:

我听到很多人说使用 IoC.Resolve() 是一种不好的做法,但我从来没有听到一个很好的理由(如果这完全是为了测试而不是你可以模拟容器,那么你就是完成)。

现在使用 Resolve 代替 Constructor Injection 的优点是你不需要创建 在构造函数中具有 5 个参数的类,以及每当您要创建 那个类你不需要为它提供任何东西

【问题讨论】:

【参考方案1】:

IoC.Resolve<> 是Service Locator 模式的一个示例。该模式施加了一些构造函数注入没有的限制:

由于静态调用,对象不能拥有比应用程序域更细粒度的上下文 对象决定要解决的依赖关系的版本。某个类的所有实例都将获得相同的依赖配置。 将代码耦合到容器的诱惑很大,例如,而不是创建意图揭示工厂。 单元测试需要容器配置,其中可以创建类并以其他方式使用。 (由于上面的第二个问题,当您想测试同一类的多个配置时,这尤其麻烦。) 无法从其公共 API 推断应用程序的结构。 (构造函数参数是一个的东西。它们不是你应该觉得需要解决的问题。)

在我看来,这些限制将服务定位器模式置于大泥球和依赖注入之间的中间地带:如果你必须使用它,它很有用,但目前还不是最佳选择。

【讨论】:

+1 表示 Resolve 实际上是服务定位器模式,我之前从未考虑过。 一切都好,除了一个错字。它应该是:IoC.Resolve<> 是服务定位器 anti 模式的一个示例。 什么是细粒度上下文以及“要解决哪些版本的依赖项”是什么意思? 细粒度上下文意味着您在同一个应用程序中以不同的方式配置了相同的合约(接口)。例如,您可能有一个 IEmailService 具有两种实现:一种发送实时电子邮件,另一种仅发送到本地服务器。如果Foo 类通过IoC.Resolve<> 请求IEmailService,它只能选择其中一个——在决定要解决哪个IEmailService 配置时,不会考虑使用它的上下文。 Foo 的所有实例都将获得与 IEmailService 完全相同的配置。 依赖版本允许你拥有同一个合约(接口)的多个实现。这通常使用字符串名称或类似的东西来完成。在上面的示例中,实时电子邮件配置将在IEmailService 下注册(表明它是该合约的默认实现)。本地配置将在IEmailService ("Local") 下注册,表明它必须通过名称引用。使用构造函数注入,对象不知道它得到的是哪个版本;使用服务定位器,它必须决定它获得哪个版本。【参考方案2】:

如果你创建的类有 5 个依赖项,你会遇到 IoC.Resolve 以外的问题。

拉取依赖(而不是通过构造函数推送它们)完全忽略了使用 IoC 框架的意义。您想反转依赖关系。不是让你的类依赖于 IoC 框架,而是反过来。

如果您在某些情况下不需要所有依赖项,那么也许您应该拆分您的类,或者通过使它们成为属性依赖项来使某些依赖项成为可选。


您的类取决于容器。除非您为他们提供一个,否则它们将无法工作。是真的还是假的都无所谓。它们本质上是通过静态依赖绑定到容器的。这会给你带来额外的工作来对你的类做任何事情。任何时候你想使用你的类,你都需要用它们拖动容器。没有任何好处!服务定位器只是一个全局包,这可能违反了面向对象编程的所有原则。

【讨论】:

+1 构造函数注入让您在违反 SRP 时非常明显:) 你的类依赖于容器。除非您为他们提供一个,否则它们将无法工作。是真的还是假的都无所谓。它们本质上是通过静态依赖绑定到容器的。 这与更改容器实现无关。这与调用某个对象(在这种情况下为 IoC.Resolve())是 not IoC 的事实有关!你在为你的依赖要求一些东西,而不是让一些东西给你。使用得当,您不需要围绕容器创建自己的“抽象”,因为您的代码根本不知道它的存在! @Chris 我认为应该尽可能避免使用服务定位器......它是用来代替实际理解 DI 的拐杖,并且不会像 DI 那样使代码更好。虽然我同意在某些情况下对容器的引用很有用,但我认为它们很少而且相差甚远,而且大多数提出此类问题的人并不处于他们真正需要这样做的情况。在这种情况下,强烈建议他们在完全理解 DI 之前不要引用容器。 @Omu,不是真的。通过使用 SL,您获得了一笔贷款,您必须在以后偿还。您可能会因此认为您正在更快地编写代码,但实际上这就像在长期运行之前不系鞋带 - 您很快就会发现自己面朝下。 SL 是这些想法之一,一开始可能看起来很吸引人,但从长远来看,它们带来的问题比解决的问题要多。【参考方案3】:

Ioc.Resolve 本质上是服务定位器模式。它有它的位置,但并不理想。从架构的角度来看,构造函数注入是首选,因为依赖关系更加明确,而 SL 将依赖关系隐藏在类中。这会降低可测试性,并使过程变得比需要的更复杂。

如果可以的话,我建议您阅读 my series,了解减少代码耦合的技术,其中包括 SL、DI 和 IoC。

【讨论】:

这是一个很好的答案,它解释了一点,但在某些情况下,当做一些超级静态和通用的事情时,服务定位器有点有用【参考方案4】:

一个优点是使用构造函数注入,所有类依赖项都是预先可见的。

使用.Resolve,您必须阅读代码才能找出依赖关系。

【讨论】:

它们在构造函数注入中都是可见的,但你不需要通过 Resolve 知道它们(仅在测试时) 其实在改变或重构应用程序的时候,你需要弄清楚它们,而这正是 IoC 真正大放异彩的时候。【参考方案5】:

我必须指出,跳过构造函数注入并使用静态注入并不一定是邪恶的。这有很多很好的应用,最具体的例子是在工厂模式实现中使用它。

public static class ValidationFactory

    public static Result Validate<T>(T obj)
    
        try
        
            var validator = ObjectFactory.GetInstance<IValidator<T>>();
            return validator.Validate(obj);
        
        catch (Exception ex)
        
            var result = ex.ToResult();
            ...
            return result;
        
        

我将它与 StructureMap 一起使用来处理我的验证层。

编辑:我直接使用容器的另一个例子是让你的一些域对象成为单例而不使它们成为静态类并引入静态类所做的所有奇怪。

在我的某些观点中,我连接了一些这样的实体。通常我会给我们一个带有 Description 属性的 Enum 来给我 3 个值选择,但在这种情况下,第 3 个也需要是一个字符串而不是一个 int,所以我创建了一个具有这 3 个属性的接口并继承了所有域对象它。然后我让我的容器扫描我的程序集并自动注册所有它们然后将它们拉出我刚刚拥有的

SomeObject ISomeView.GetMyObject

    get  return new SomeObject  EmpoweredEnumType = 
            ObjectFactory.GetNamedInstance<IEmpEnum>("TheObjectName");
        

【讨论】:

我认为在这种情况下不需要引用容器。使用此代码,您的类可以调用静态 ValidationFactory.Validate... 进行测试,然后您必须使用所需类型的 IValidators 设置容器。相反,应该将一些对象验证接口注入到这些类中,使测试变得容易(可以很容易地传入一个始终为真或为假的验证器)。 ValidationFactory 的非静态版本可能是一种实现,这没问题,但即便如此,也应该有其他不引用容器的选项。 我真的没有看到你所说的任何增加价值,我的工厂很容易测试,是的,它可能需要实现 ReturnFalseValidator : IValidator 如果我需要经常模拟它,我可以轻松包装将其转换为抽象基类型并在我的容器配置中使用它。我个人喜欢我所有的测试都依赖于我的容器来工作,因为这就是它在现实生活中的工作方式。测试你的代码而不让测试真正从注入中获取它们的依赖关系,这让我觉得测试非常不可靠。 我使用 resolve 自动映射从 dtos 到实体并返回,我为每种类型的转换(type1 -> type2)都有一个 ValueInjecter,这个 ValueInjecter 使用 Resolve 来获取获取数据的服务(所有这些使用约定的东西是非常自动的)【参考方案6】:

由于这个问题值得商榷,我不会说“使用这个或那个”

看起来使用服务定位器不是一件坏事,如果你可以依赖它(我们通常在某些 DI 框架上做任何事情)。借助 DI,我们可以轻松地更改框架,借助 Service Locator,我们可以创建与框架的 SL 部分的耦合。

关于 Bryan Watts 在您稍后阅读时的回答 Service Locator vs Dependency Injection

...使用 [constructor] 注入没有显式请求,服务出现在应用程序类中 - 因此 控制反转

控制反转是框架的一个常见特性,但它是有代价的。当您尝试调试时,它往往难以理解并导致问题。 所以总的来说,除非我需要它,否则我宁愿避免它。这并不是说这是一件坏事,只是我认为它需要证明自己比更直接的选择更合理。

然后,如果您稍后阅读,这是实际使用构造函数注入(控制反转)的另一个理由。

我的观点是,在小型项目中使用 SL 是可以的,因为主要不是在我们自定义开发的类之间创建耦合。

使用 StructureMap 示例,这应该是可以接受的:

public class Demo

    private ISomething something = ObjectFactory.GetInstance<ISomething>();
    private IFoo foo = ObjectFactory.GetInstance<IFoo>();

是的,代码取决于 SM Frx,但是您多久更改一次 DI Frx?

对于单元测试,可以设置一个模拟

public class SomeTestClass

    public SomeTest()
    
        ObjectFactory.Inject<ISomething>(SomeMockGoesHere);
        ObjectFactory.Inject<IFoo>(SomeMockGoesHere);

        Demo demo = new Demo() //will use mocks now
    

使用 Resolve 代替 Constructor Injection 的优点是您不需要在构造函数中创建具有 5 个参数的类

但您最终可能会为单元测试制作更多“管道”。

【讨论】:

【参考方案7】:

我会说这是要注入的大量参数。

争取一个参数,最多可能2个,这在几乎所有场景中都是可能的,当然应该是一个接口。不仅如此,我还闻到了老鼠的味道(设计缺陷)。

【讨论】:

【参考方案8】:

您不需要在构造函数中创建具有 5 个参数的类,并且无论何时要创建该类的实例,您都不需要为它提供任何东西

几点:

如果您使用的是 DI 容器,它应该会为您创建该类的实例。在这种情况下,您不必自己为生产使用提供任何东西。为了进行测试,您必须通过构造函数为其提供依赖项,但是: 如果该类依赖于(以某种方式使用)您所说的提供给构造函数的那 5 件事(如果不提供,您将不会提供它们),您将必须与它们一起提供方式或其他。测试时(这是您唯一应该自己调用构造函数的时间),您可以通过构造函数将这些东西传递给它,或者您可以编写代码来设置容器并将这 5 个东西添加到其中,这样当@987654321 @ 被调用,他们实际上在那里。我会说,将它们传递给构造函数要容易得多。

即使您没有通过类的 API(在本例中为构造函数)表明依赖关系也会存在。然而,像这样试图隐藏它们的依赖关系的类会更难理解和测试。

【讨论】:

这是我的控制器类(asp.net mvc),有时它们有 3 到 7 个参数(每个参数都是一个接口,它是 srp 的东西)

以上是关于IoC.Resolve 与构造函数注入的主要内容,如果未能解决你的问题,请参考以下文章

构造函数注入与字段注入[重复]

注入 dbContext 时返回“找不到与给定参数匹配的构造函数”

Nest.js 模型依赖注入与 Mongoose 没有构造函数

Spring 框架中 Setter 注入 和 构造器注入 方式的区别 与优劣

Spring基础篇(8)-Spring构造函数注入—实现子类的动态注入

ZF2:在服务构造函数中注入变量