我知道直接实例化类内部的依赖关系被认为是不好的做法。这是有道理的,因为这样做紧密地结合了所有内容,从而使测试变得非常困难。
我遇到的几乎所有框架似乎都倾向于使用容器进行依赖注入而不是使用服务定位器。通过允许程序员指定当类需要依赖时应返回哪个对象,两者似乎都实现了相同的目的。
两者有什么区别?为什么我要选择一个?
我知道直接实例化类内部的依赖关系被认为是不好的做法。这是有道理的,因为这样做紧密地结合了所有内容,从而使测试变得非常困难。
我遇到的几乎所有框架似乎都倾向于使用容器进行依赖注入而不是使用服务定位器。通过允许程序员指定当类需要依赖时应返回哪个对象,两者似乎都实现了相同的目的。
两者有什么区别?为什么我要选择一个?
Answers:
当对象本身负责请求其依赖关系时(而不是通过构造函数接受它们),它就隐藏了一些基本信息。它仅比new
用于实例化其依赖关系的紧密耦合情况要好。它减少了耦合,因为您实际上可以更改它获得的依赖关系,但是它仍然具有不可撼动的依赖关系:服务定位器。那变成了一切都依赖的东西。
通过构造函数参数提供依赖关系的容器可以提供最大的清晰度。我们马上就可以看到一个对象同时需要一个AccountRepository
和一个对象PasswordStrengthEvaluator
。使用服务定位器时,该信息不太明显。您会立即看到一个对象具有17个依赖项的情况,然后对自己说:“嗯,这看起来很多。那里发生了什么?” 对服务定位器的调用可以散布在各种方法中,并隐藏在条件逻辑的后面,您可能没有意识到自己已经创建了“神类”(God class),它可以完成所有工作。也许可以将该类重构为3个较小的类,这些类更加集中,因此更具可测试性。
现在考虑进行测试。如果对象使用服务定位器来获取其依赖项,则测试框架还将需要服务定位器。在测试中,您将配置服务定位器以将依赖项提供给被测试的对象(可能是a FakeAccountRepository
和a)VeryForgivingPasswordStrengthEvaluator
,然后运行测试。但是,这比在对象的构造函数中指定依赖项要复杂得多。而且您的测试框架也变得依赖于服务定位器。您必须在每个测试中进行配置,这使编写测试的吸引力降低。
在Mark Seeman的文章中查找“ Serivce Locator是一种反模式”。如果您在.Net世界中,请获取他的书。这很好。
constructor supplied dependencies
vs中的一件事service locator
是,前者可以在编译时验证,而后者只能在运行时验证。
But that's more work than specifying dependencies in the object's constructor.
我想提出反对。使用服务定位器,您只需指定测试实际需要的3个依赖关系。对于基于构造函数的DI,即使7个未使用,也需要指定全部10个。
想象一下,你是一家制造鞋的工厂的工人。
您负责组装鞋子,因此您需要做很多事情。
等等。
您正在工厂工作,可以开始工作了。您具有有关如何进行操作的说明列表,但还没有任何材料或工具。
一个服务定位器就像是一个工头,可以帮助你得到你所需要的。
每当您需要某些东西时,您都会询问服务定位器,然后他们会为您找到它。我们已提前告知服务定位器您可能要问的内容以及如何找到它。
您最好希望您不要要求任何意外的事情。如果没有提前告知Locator特定的工具或材料,他们将无法为您找到它,他们只会对您耸耸肩。
一个依赖注入(DI)容器就是这样被充满一切,每个人都需要在一天开始的大箱子。
随着工厂的启动,被称为“ 合成根”的大老板抓住了容器并将所有东西交给了生产线经理。
直属经理现在拥有日常工作所需的东西。他们拿走自己拥有的东西,并将所需的东西传递给下属。
此过程继续进行,依存关系沿生产线滴下。最终,您的工头将出现一堆材料和工具。
现在,您的领班将您所需要的东西准确地分配给您和其他工作人员,而您甚至不需要他们。
基本上,只要您上班了,您所需的一切就已经在盒子里等待着您。您无需了解如何获取它们。
在搜寻网络时,我发现了几个额外的要点:
我参加这个聚会迟到了,但我无法抗拒。
在容器中使用依赖项注入与使用服务定位器有什么区别?
有时一点都没有。有所不同的是对什么有所了解。
您知道当寻找依赖项的客户端知道容器时,您正在使用服务定位器。服务定位器模式是一个即使在通过容器获取依赖关系时也知道如何找到其依赖关系的客户端。
这是否意味着如果您想避免使用服务定位器,就不能使用容器?不。您只需要使客户不了解该容器即可。关键区别在于使用容器的位置。
可以说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仍然知道如何定位依赖项!是的,它确实。但是,通过不向您传播这些知识,可以避免服务定位器的核心问题。现在可以在一个地方做出使用容器的决定,并且可以在不重写数百个客户端的情况下进行更改。
我认为,了解两者之间的区别以及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);
}
}
现在,我已将服务定位器添加为依赖项。以下是显而易见的问题:
那么,为什么不将服务定位器设为静态类呢?让我们来看看:
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)完全没有测试。而且,您还剩下一个不太灵活的解决方案。
因此,服务定位器类具有被注入到构造函数。这意味着我们剩下前面提到的特定问题。服务定位器需要更多的代码,告诉其他开发人员它不需要的东西,鼓励其他开发人员编写更糟糕的代码,并给我们带来更少的灵活性。
简单地说,服务定位符会增加应用程序中的耦合,并鼓励其他开发人员编写高度耦合的代码。