在容器中使用依赖项注入与使用服务定位器有什么区别?


107

我知道直接实例化类内部的依赖关系被认为是不好的做法。这是有道理的,因为这样做紧密地结合了所有内容,从而使测试变得非常困难。

我遇到的几乎所有框架似乎都倾向于使用容器进行依赖注入而不是使用服务定位器。通过允许程序员指定当类需要依赖时应返回哪个对象,两者似乎都实现了相同的目的。

两者有什么区别?为什么我要选择一个?


3
这已被问(并回答)在计算器上:stackoverflow.com/questions/8900710/...
Kyralessa

2
在我看来,开发人员欺骗其他人以为他们的服务定位器是依赖注入是很普遍的。之所以这样做是因为依赖注入通常被认为是更高级的。
Gherman


1
令我惊讶的是,没有人提到使用服务定位器(Service Locators),很难知道何时可以破坏瞬态资源。我一直认为这是主要原因。
Buh Buh

Answers:


126

当对象本身负责请求其依赖关系时(而不是通过构造函数接受它们),它就隐藏了一些基本信息。它仅比new用于实例化其依赖关系的紧密耦合情况要好。它减少了耦合,因为您实际上可以更改它获得的依赖关系,但是它仍然具有不可撼动的依赖关系:服务定位器。那变成了一切都依赖的东西。

通过构造函数参数提供依赖关系的容器可以提供最大的清晰度。我们马上就可以看到一个对象同时需要一个AccountRepository和一个对象PasswordStrengthEvaluator。使用服务定位器时,该信息不太明显。您会立即看到一个对象具有17个依赖项的情况,然后对自己说:“嗯,这看起来很多。那里发生了什么?” 对服务定位器的调用可以散布在各种方法中,并隐藏在条件逻辑的后面,您可能没有意识到自己已经创建了“神类”(God class),它可以完成所有工作。也许可以将该类重构为3个较小的类,这些类更加集中,因此更具可测试性。

现在考虑进行测试。如果对象使用服务定位器来获取其依赖项,则测试框架还将需要服务定位器。在测试中,您将配置服务定位器以将依赖项提供给被测试的对象(可能是a FakeAccountRepository和a)VeryForgivingPasswordStrengthEvaluator,然后运行测试。但是,这比在对象的构造函数中指定依赖项要复杂得多。而且您的测试框架也变得依赖于服务定位器。您必须在每个测试中进行配置,这使编写测试的吸引力降低。

在Mark Seeman的文章中查找“ Serivce Locator是一种反模式”。如果您在.Net世界中,请获取他的书。这很好。


1
阅读我想到的关于通过C#编写的有关Adaptive Code的问题,这与该答案非常吻合。它有一些很好的类比,例如服务定位器是安全的关键。当传递给一个类时,它可能会随意创建可能不容易发现的依赖项。
丹尼斯

10
IMO需要添加到constructor supplied dependenciesvs中的一件事service locator是,前者可以在编译时验证,而后者只能在运行时验证。
andras

2
另外,服务定位器不会阻止组件具有许多依赖关系。如果必须在构造函数中添加10个参数,那么您的代码一定有问题。但是,如果您碰巧对服务定位器进行了10次静态调用,则可能不容易发现问题。我已经使用服务定位器完成了一个大型项目,这是最大的问题。短路一条新路径而不是静静地思考,重新设计和重构,这太容易了。
佩斯

1
关于测试- But that's more work than specifying dependencies in the object's constructor.我想提出反对。使用服务定位器,您只需指定测试实际需要的3个依赖关系。对于基于构造函数的DI,即使7个未使用,也需要指定全部10个。
Vilx-

4
@ Vilx-如果您有多个未使用的构造函数参数,则您的类可能违反了“单一职责”原则。
RB。

79

想象一下,你是一家制造的工厂的工人。

您负责组装鞋子,因此您需要做很多事情。

  • 皮革
  • 卷尺
  • 钉子
  • 锤子
  • 剪刀
  • 鞋带

等等。

您正在工厂工作,可以开始工作了。您具有有关如何进行操作的说明列表,但还没有任何材料或工具。

一个服务定位器就像是一个工头,可以帮助你得到你所需要的。

每当您需要某些东西时,您都会询问服务定位器,然后他们会为您找到它。我们已提前告知服务定位器您可能要问的内容以及如何找到它。

您最好希望您不要要求任何意外的事情。如果没有提前告知Locator特定的工具或材料,他们将无法为您找到它,他们只会对您耸耸肩。

服务定位器

一个依赖注入(DI)容器就是这样被充满一切,每个人都需要在一天开始的大箱子。

随着工厂的启动,被称为“ 合成根”的大老板抓住了容器并将所有东西交给了生产线经理

直属经理现在拥有日常工作所需的东西。他们拿走自己拥有的东西,并将所需的东西传递给下属。

此过程继续进行,依存关系沿生产线滴下。最终,您的工头将出现一堆材料和工具。

现在,您的领班将您所需要的东西准确地分配给您和其他工作人员,而您甚至不需要他们。

基本上,只要您上班了,您所需的一切就已经在盒子里等待着您。您无需了解如何获取它们。

依赖注入容器


45
这是对这两件事的精妙描述,也是超级漂亮的图表!但这不能回答“为什么要选择一个”的问题?
Matthieu M.

21
“您最好希望您不要要求出乎意料的事情。如果未提前告知Locator特定工具或材料的信息,”那么对于DI容器也可以如此。
Kenneth K.

5
@FrankHopkins如果省略向DI容器注册接口/类,则您的代码仍将编译,并且在运行时会迅速失败。
Kenneth K.

3
两者均可能失败,具体取决于它们的设置方式。但是,使用DI容器,您更有可能在构造类X时而不是稍后在类X实际上需要一个依赖项时遇到问题。可以说它更好,因为它更容易遇到问题,找到并解决它。我确实同意Kenneth的观点,即答案表明此问题仅存在于服务定位器中,而事实并非如此。
GolezTrol

3
很好的答案,但我认为它错过了另一件事。也就是说,如果您在任何时候都要求领班给大象,而他确实知道如何获得大象,即使您不需要,他也不会问您任何问题。有人试图在地板上调试问题(如果愿意,请开发人员)会怀疑wtf是在这张桌子上做的事情,因此使他们更难于推断出实际出了什么问题。而使用DI时,您的工作者类会开始工作之前预先声明所需的每个依赖关系,从而使推理更加容易。
Stephen Byrne

10

在搜寻网络时,我发现了几个额外的要点:

  • 将依赖项注入到构造函数中,可以更轻松地理解类的需求。现代IDE会提示构造函数接受哪些参数以及它们的类型。如果使用服务定位器,则必须先通读类,然后才能知道需要哪些依赖项。
  • 依赖注入似乎比服务定位符更遵守“不要问”的原则。通过强制依赖项为特定类型,可以“告诉”需要哪些依赖项。没有传递所需的依赖关系就无法实例化该类。使用服务定位器,您可以“询问”服务,如果服务定位器的配置不正确,则可能无法获得所需的内容。

4

我参加这个聚会迟到了,但我无法抗拒。

在容器中使用依赖项注入与使用服务定位器有什么区别?

有时一点都没有。有所不同的是对什么有所了解。

您知道当寻找依赖项的客户端知道容器时,您正在使用服务定位器。服务定位器模式是一个即使在通过容器获取依赖关系时也知道如何找到其依赖关系的客户端。

这是否意味着如果您想避免使用服务定位器,就不能使用容器?不。您只需要使客户不了解该容器即可。关键区别在于使用容器的位置。

可以说Client需求Dependency。容器有一个Dependency

class Client { 
    Client() { 
        BeanFactory beanfactory = new ClassPathXmlApplicationContext("Beans.xml");
        this.dependency = (Dependency) beanfactory.getBean("dependency");        
    }
    Dependency dependency;
}

我们刚刚遵循了服务定位器模式,因为它Client知道如何查找Dependency。当然,它使用了硬编码,ClassPathXmlApplicationContext但是即使您注入,由于Client调用,您仍有服务定位器beanfactory.getBean()

为了避免服务定位器,您不必放弃此容器。您只需要将其移出即可,Client因此Client一无所知。

class EntryPoint { 
    public static void main(String[] args) {
        BeanFactory beanfactory = new ClassPathXmlApplicationContext("Beans.xml");
        Client client = (Client) beanfactory.getBean("client");

        client.start();
    }
}

<?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="dependency" class="Dependency">
    </bean>

    <bean id="client" class="Client">
        <constructor-arg value="dependency" />        
    </bean>
</beans>

注意Client现在如何不知道该容器是否存在:

class Client { 
    Client(Dependency dependency) { 

        this.dependency = dependency;        
    }
    Dependency dependency;
}

将容器移出所有客户端,然后将其粘贴在main上,以便可以为所有长期存在的对象构建对象图。选择其中一个对象以提取并调用一个方法,然后开始对整个图形进行滴答。

这将所有静态构造移入了容器XML,但使所有客户端都非常不了解如何查找其依赖项。

但是main仍然知道如何定位依赖项!是的,它确实。但是,通过不向您传播这些知识,可以避免服务定位器的核心问题。现在可以在一个地方做出使用容器的决定,并且可以在不重写数百个客户端的情况下进行更改。


1

我认为,了解两者之间的区别以及DI容器为什么比服务定位器好得多的最简单方法是首先考虑一下为什么要进行依赖关系反转。

我们进行依赖倒置,以便每个类明确声明其操作所依赖的内容。我们这样做是因为这会产生我们可以实现的最宽松的耦合。耦合越松,测试和重构就越容易(由于代码更简洁,将来通常需要最少的重构)。

让我们看下面的类:

public class MySpecialStringWriter
{
  private readonly IOutputProvider outputProvider;
  public MySpecialFormatter(IOutputProvider outputProvider)
  {
    this.outputProvider = outputProvider;
  }

  public void OutputString(string source)
  {
    this.outputProvider.Output("This is the string that was passed: " + source);
  }
}

在此类中,我们明确指出需要一个IOutputProvider并不需要其他任何东西来使该类正常工作。这是完全可测试的,并且依赖于单个接口。我可以将此类移动到应用程序中的任何地方,包括一个不同的项目,它所需要的只是对IOutputProvider接口的访问。如果其他开发人员想要向此类添加新的东西(需要第二个依赖项),则必须明确说明构造函数中需要的东西。

使用服务定位器查看同一类:

public class MySpecialStringWriter
{
  private readonly ServiceLocator serviceLocator;
  public MySpecialFormatter(ServiceLocator serviceLocator)
  {
    this.serviceLocator = serviceLocator;
  }

  public void OutputString(string source)
  {
    this.serviceLocator.OutputProvider.Output("This is the string that was passed: " + source);
  }
}

现在,我已将服务定位器添加为依赖项。以下是显而易见的问题:

  • 这样做的第一个问题是要花费更多的代码才能达到相同的结果。更多代码是不好的。它不是更多的代码,但还有更多。
  • 第二个问题是我的依赖关系不再明确。我仍然需要在课堂上注入一些东西。除了现在,我要的东西不是很明确。它隐藏在我要求的东西的属性中。现在,如果我想将类移动到其他程序集,则需要同时访问ServiceLocator和IOutputProvider。
  • 第三个问题是,其他开发人员可能会增加一个附加的依赖关系他们甚至在向类添加代码时都没有意识到自己正在接受它。
  • 最后,此代码更难测试(即使ServiceLocator是一个接口),因为我们必须模拟ServiceLocator和IOutputProvider而不是IOutputProvider

那么,为什么不将服务定位器设为静态类呢?让我们来看看:

public class MySpecialStringWriter
{
  public void OutputString(string source)
  {
    ServiceLocator.OutputProvider.Output("This is the string that was passed: " + source);
  }
}

这要简单得多,对吗?

错误。

假设IOutputProvider是由运行时间很长的Web服务实现的,该服务将字符串写入世界各地的15个不同数据库中,并且需要很长时间才能完成。

让我们尝试测试该类。我们需要测试的IOutputProvider的不同实现。我们如何编写测试?

为此,我们需要在静态ServiceLocator类中进行一些精美的配置,以在测试调用IOutputProvider时使用不同的IOutputProvider实现。即使写那句话也很痛苦。实施它将是一种折磨,这将是维护的噩梦。我们永远不需要修改专门用于测试的类,尤其是如果该类不是我们实际尝试测试的类时。

因此,现在剩下的是:a)一个测试,该测试导致不相关的ServiceLocator类中的代码更改过大;或b)完全没有测试。而且,您还剩下一个不太灵活的解决方案。

因此,服务定位器类具有被注入到构造函数。这意味着我们剩下前面提到的特定问题。服务定位器需要更多的代码,告诉其他开发人员它不需要的东西,鼓励其他开发人员编写更糟糕的代码,并给我们带来更少的灵活性。

简单地说,服务定位符会增加应用程序中的耦合,鼓励其他开发人员编写高度耦合的代码


服务定位器(SL)和依赖注入(DI)都以相似的方式解决了相同的问题。如果您是“告诉” DI或“询问” SL依赖项,则唯一的区别。
马修·怀特

@MatthewWhited主要区别是您使用服务定位器获取的隐式依赖项的数量。这对代码的长期可维护性和稳定性产生了巨大的影响。
斯蒂芬

真的不是。两种方法中的依赖项数量相同。
马修·怀特

最初是的,但是您可以通过服务定位器获取依赖关系,而无需意识到自己正在获取依赖关系。这在很大程度上消除了我们首先进行依赖反转的原因的大部分。一类取决于属性A和B,另一类取决于属性B和C。服务定位器突然变成了神类。DI容器没有,这两个类分别仅分别依赖于A和B以及B和C。这使得重构它们更容易一百倍。服务定位器是隐蔽的,因为它们看起来与DI相同,但是却不相同。
斯蒂芬
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.