IoC.Resolve与构造函数注入的区别

46

我听到很多人说使用IoC.Resolve()是一种不好的实践,但我从未听到过一个好的原因(如果这与测试有关,那么你只需要模拟容器即可)。

现在使用Resolve而不是构造函数注入的优点是您不需要创建具有5个构造函数参数的类,每当您要创建该类的实例时,您都不需要为其提供任何内容。

8个回答

53

IoC.Resolve<>服务定位器 模式的一个示例。该模式对于构造函数注入没有的一些限制:

  • 由于静态调用,对象不能拥有更细粒度的上下文,最多只能是应用程序域。
  • 对象决定解析哪些依赖关系的版本。同一类的所有实例将得到相同的依赖项配置。
  • 很容易将代码与容器耦合在一起,例如,而不是创建意图明确的工厂。
  • 单元测试需要容器配置,否则可以直接创建和使用类。(当你想测试同一类的多个配置时,由于上述第二个问题,这将会特别麻烦。)
  • 无法从公共API中推断出应用程序的结构。(构造函数参数是一件好事。它们不是你应该感觉需要解决的问题。)

在我看来,这些限制将服务定位器模式归为大型代码堆积和依赖注入之间的中间地带:如果必须使用它,则很有用,但绝不是最佳选择。


3
感谢您指出“解析器”实际上是服务定位器模式,我以前从未考虑过这一点。+1 - Chris Marisic
13
除了一个错别字外,一切都好。应该写成:“IoC.Resolve<>”是服务定位器反模式的一个例子。 - Krzysztof Kozmic
3
细粒度上下文是指在同一个应用程序中以不同的方式配置相同的契约(接口)。例如,您可以有一个IEmailService,它有两个实现:一个发送实时电子邮件,另一个仅发送到本地服务器。如果类Foo通过IoC.Resolve<>请求IEmailService,它只能选择其中一个 - 该决策不考虑将使用哪个上下文来解决IEmailService配置的因素。所有Foo的实例都将获取相同的IEmailService配置。 - Bryan Watts
5
依赖版本允许您拥有同一合同(接口)的多个实现。通常使用字符串名称或类似方式来实现。在上面的示例中,活动电子邮件配置将在“ IEmailService ”下注册(表示它是该合同的默认实现)。本地配置将在“ IEmailService(“Local”)”下注册,表示必须按名称引用它。对于构造函数注入,对象不知道它得到的是哪个版本;对于服务定位器,它必须决定它得到哪个版本。 - Bryan Watts
1
服务定位器(Service Locator)明显违反了“最小惊讶原则”。非常感谢@BryanWatts提供的这篇小文章! - zzfima
显示剩余8条评论

26
如果你创建的类有5个依赖,那么你除了IoC.Resolve之外还会遇到其他问题。
通过拉取依赖项(而不是通过构造函数将它们推送)完全忽略了使用IoC框架的重点。你需要反转依赖关系,让你的类不依赖于IoC框架,而是相反的。
如果在某些情况下你不需要所有依赖项,那么也许你应该拆分你的类,或者通过使它们成为属性依赖项来使一些依赖项变成可选的。
你的类依赖于容器。如果您不提供容器,它们将无法工作。无论是真实的容器还是虚假的容器都没有关系。它们通过静态依赖与容器紧密相连。这对你额外增加了工作量,因为任何时候你想使用你的类,你都需要将容器与它们一起使用。这没有任何的好处!服务定位器只是一个全局的容器,违反了面向对象编程的原则。

8
+1 构造函数注入会让你痛苦地意识到何时违反了单一职责原则(SRP):) - Mark Seemann
3
你的类取决于容器。除非提供了容器,否则它们无法工作。无论是真的还是假的都没关系。它们通过静态依赖与容器紧密相连。 - Krzysztof Kozmic
3
这与更改容器实现无关,而是与事实有关:调用某个对象(此处的IoC.Resolve())并不是IoC!你正在请求依赖项,而不是让某个东西为您提供它们。如果使用得当,您不需要在容器周围创建自己的“抽象层”,因为您的代码根本不知道容器的存在! - ColinD
3
我认为应尽可能避免使用服务定位器(Service Locator)...它只是一种替代了理解依赖注入(DI)而常用的支撑工具,而不像 DI 一样可以使代码变得更好。虽然我同意,在某些情况下引用容器是有用的,但这种情况很少见,并且大多数询问此类问题的人并不处于实际需要这样做的情况。在这种情况下,应强烈建议他们在完全理解 DI 之前不要引用容器。 - ColinD
3
@Omu,不完全是这样。通过使用SL,您正在借贷,以后必须偿还。您可能认为使用SL可以更快地编写代码,但实际上就像在长跑之前不系鞋带一样 - 很快您会发现自己面朝下。SL是一个看起来很有吸引力的想法,但从长远来看,它们会造成更多问题而不是解决问题。 - Krzysztof Kozmic
显示剩余16条评论

7
Ioc.Resolve本质上是服务定位器模式。它有其适用的场合,但并不理想。从架构的角度来看,构造函数注入更受欢迎,因为依赖关系更加明确,而SL则将依赖关系隐藏在类中。这降低了可测试性,并使过程比必要的复杂。如果可以的话,我建议您阅读我的系列文章,介绍了减少代码耦合的技术,其中包括SL、DI和IoC。请参考这里

这是一个很好的答案,它解释了一点,但在某些情况下,当进行一些超级静态和通用的事情时,服务定位器还是有点有用的。 - Omu

3
优点之一是使用构造函数注入时,所有类依赖关系都可以一目了然。而使用 .Resolve 时,你必须阅读代码才能确定依赖关系。

它们都可以通过构造函数注入看到,但是你在使用 Resolve 时不需要知道它们(只有在测试时需要)。 - Omu
2
实际上,当您更改或重构应用程序时,需要弄清楚它们,这正是IoC真正发挥作用的时候。 - Nicholas Blumhardt

1

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

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属性的枚举来给我3个值选项,但在这种情况下,第三个选项也需要是字符串而不是整数,因此我创建了一个接口,其中包含这3个属性,并从中继承所有域对象。然后我让容器扫描我的程序集并自动注册所有对象,然后要提取它们,我只需

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

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

1

由于这个问题有争议,我不会说“使用这个或那个”

如果你可以依赖Service Locator并且我们通常在某些DI框架上这样做,那么使用Service Locator似乎并不是一件坏事。通过DI,我们可以轻松更改框架,而使用Service Locator则会创建与框架的SL部分的耦合。

关于Bryan Watts的回答,当你稍后阅读 Service Locator vs Dependency Injection

......[构造函数]注入没有明确的请求,服务出现在应用程序类中-因此是控制反转

控制反转是框架的常见特征,但它是需要付出代价的。它往往很难理解,并且在尝试调试时会导致问题。所以总的来说,除非需要,否则我更喜欢避免使用它。这并不是说它是一件坏事,只是我认为它需要证明自己比更简单的替代方案更好。

然后,如果您稍后阅读,就会发现另一个使用构造函数注入(控制反转)的正当理由。

我认为在小型项目中使用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而不是构造函数注入的优点在于,您不需要创建具有5个参数构造函数的类。但是,您可能会为单元测试制作更多的"管道"。

0

我会说这是注入的参数数量太多了。

应该努力实现只有一个参数,最多两个参数,而且在几乎所有情况下都应该是可能的,并且当然应该是一个接口。如果超过这个数量,那么我就会嗅到设计缺陷的味道。


0
你不需要创建构造函数有5个参数的类,每当你要创建该类的实例时,你都不需要提供任何东西。
几点说明:
  • 如果你使用 DI 容器,它应该为你创建该类的实例。在这种情况下,你不必自己提供任何东西来进行生产使用。对于测试,你将不得不通过构造函数提供依赖项,但是:
  • 如果该类依赖(以某种方式使用)你所说的那5个东西,你将不得不以一种或另一种方式提供它们。在测试时(这是你唯一需要自己调用构造函数的时候),你可以通过构造函数将这些东西传递给它,或者你可以编写代码设置容器并将这5个东西添加到其中,以便在调用 IoC.Resolve() 时,它们实际上存在。我认为通过构造函数传递它们会更容易。

即使您不通过类的API(在这种情况下是构造函数)明确表明,依赖关系仍将存在。然而,试图隐藏其依赖关系的类将更难理解和测试。


这是我的控制器类(asp.net mvc),它们有时会有3到7个参数(每个参数都是一个执行其任务的接口,这是SRP原则)。 - Omu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接