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

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

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

评论

这已在StackOverflow上问过(并回答了):stackoverflow.com/questions/8900710/…

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

martinfowler.com/articles/…

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

#1 楼

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

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

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

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

评论


读了我通过Gary McLean Hall编写的有关C#的有关自适应代码的问题,该问题与该答案非常吻合。它有一些很好的类比,例如服务定位器是安全的关键。当传递给一个类时,它可能会随意创建可能不容易发现的依赖项。

–丹尼斯
19年4月23日在6:07

IMO需要添加到提供的依赖项vs服务定位器的构造函数中的一件事是,前者可以在编译时验证,而后者只能在运行时验证。

– andras
19年4月23日在8:10

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

–步伐
19年4月23日在18:51

关于测试-但是,这比在对象的构造函数中指定依赖项要付出更多的努力。我想反对。使用服务定位器,您只需指定测试实际需要的3个依赖关系。对于基于构造函数的DI,即使没有使用7个,也需要指定全部10个。

– Vilx-
19年4月24日在13:51

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

– RB。
19年4月25日在13:47

#2 楼

想象一下,您是一家制造鞋子的工厂的工人。

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


皮革
卷尺
胶水
钉子
锤子
剪刀
鞋带

等等。

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

服务定位器就像是领班,可以帮助您获得所需的信息。

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

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



依赖注入(DI)容器就像一个大盒子,一开始就充满了每个人需要的一切。

随着工厂的启动,Big Boss广为人知当“合成根”抓住容器并将所有东西交给生产线经理时。

生产线经理现在拥有履行日常职责所需的一切。他们拿走自己拥有的东西,并将所需的东西传递给下属。

这个过程继续进行,依赖关系不断滴灌生产线。最终,您的领班会出现一揽子材料和工具。

您的领班现在向您和其他工人准确分发了您所需要的东西,甚至您都不需要他们。

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



评论


这是对这两个东西分别的极好的描述,也是超级漂亮的图表!但这不能回答“为什么要选择一个”的问题?

– Matthieu M.
19年4月23日在7:49

“您最好希望您不要要求任何意外的东西。如果没有提前告知Locator特定工具或材料的信息,”那么对于DI容器也是如此。

–肯尼斯·K。
19年4月23日在14:28

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

–肯尼斯·K。
19-4-24在4:02



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

– GolezTrol
19-4-24在12:59



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

–斯蒂芬·伯恩
19-4-24在14:12



#3 楼

在浏览Web时,我发现了几个额外的要点:将依赖项注入到构造函数中可以更轻松地理解类的需求。现代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仍然知道如何找到依赖项!是的,它确实。但是,通过不向您传播这些知识,可以避免服务定位器的核心问题。现在可以在一个地方做出使用容器的决定,并且可以在不重写数百个客户端的情况下进行更改。

#5 楼

我认为,了解两者之间的区别以及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时使用其他实现。即使写那句话也很痛苦。实施它将是一种折磨,这将是维护的噩梦。我们永远不需要修改专门用于测试的类,尤其是如果该类不是我们实际上要测试的类。

所以现在剩下的要么是a)导致不相关的ServiceLocator类中的强制性代码更改;或b)完全没有测试。而且您还剩下不太灵活的解决方案。

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

简单地将服务定位器增加了耦合应用程序,并鼓励其他开发人员编写高度耦合的代码。

评论


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

–马修·怀特(Matthew Whited)
19年4月25日在15:26

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

–斯蒂芬
19年4月28日在23:50

真的不是。两种方式中,您具有相同数量的依赖项。

–马修·怀特(Matthew Whited)
19年4月29日在14:36

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

–斯蒂芬
19年4月30日在0:43