最近我读过有关Service Locator反模式的Mark Seemann's article。
作者指出ServiceLocator为反模式的两个主要原因:
API使用问题(我完全没问题)
当类使用服务定位器时,很难看到它的依赖关系,因为在大多数情况下,类只有一个PARAMETERLESS构造函数。
与ServiceLocator相比,DI方法通过构造函数的参数显式地暴露了依赖关系,因此在IntelliSense中很容易看到依赖关系。
维护问题(让我感到困惑)
请考虑以下示例
我们有一个'MyType'类,它采用了服务定位器方法:
public class MyType
{
public void MyMethod()
{
var dep1 = Locator.Resolve<IDep1>();
dep1.DoSomething();
}
}
现在我们要为类'MyType'添加另一个依赖项
public class MyType
{
public void MyMethod()
{
var dep1 = Locator.Resolve<IDep1>();
dep1.DoSomething();
// new dependency
var dep2 = Locator.Resolve<IDep2>();
dep2.DoSomething();
}
}
这就是我的误解开始的地方。作者说:
要判断您是否正在进行重大改变,这将变得更加困难。您需要了解使用Service Locator的整个应用程序,编译器不会帮助您。
但是等一下,如果我们使用DI方法,我们将在构造函数中引入另一个参数的依赖项(在构造函数注入的情况下)。问题仍然存在。如果我们忘记设置ServiceLocator,那么我们可能忘记在IoC容器中添加新的映射,而DI方法会遇到相同的运行时问题。
此外,作者还提到了单元测试的难点。但是,我们不会有DI方法的问题吗?我们不需要更新所有实例化该类的测试吗?我们将更新它们以传递一个新的模拟依赖项,以使我们的测试可编译。我没有看到更新和时间花费带来任何好处。
我不是想捍卫Service Locator方法。但这种误解让我觉得我失去了一些非常重要的东西。有人可以消除我的怀疑吗?
更新(摘要):
我的问题“服务定位器是反模式”的答案实际上取决于具体情况。我绝对不会建议你从工具列表中删除它。当您开始处理遗留代码时,它可能会变得非常方便。如果你很幸运能够在项目的最初阶段,那么DI方法可能是更好的选择,因为它比Service Locator有一些优势。
以下是主要区别,这些差异使我不相信我的新项目使用Service Locator:
有关详细信息,请阅读下面给出的优秀答案。
答案 0 :(得分:114)
如果你将模式定义为反模式只是因为在某些情况下它不适合,那么YES就是反模式。但由于这种推理,所有模式也都是反模式。
相反,我们必须查看是否存在模式的有效用法,而对于Service Locator,有几个用例。但是,让我们先看看你给出的例子。
public class MyType
{
public void MyMethod()
{
var dep1 = Locator.Resolve<IDep1>();
dep1.DoSomething();
// new dependency
var dep2 = Locator.Resolve<IDep2>();
dep2.DoSomething();
}
}
该类的维护噩梦是依赖项被隐藏。如果您创建并使用该类:
var myType = new MyType();
myType.MyMethod();
如果使用服务位置隐藏它们,则您不明白它具有依赖关系。现在,如果我们改为使用依赖注入:
public class MyType
{
public MyType(IDep1 dep1, IDep2 dep2)
{
}
public void MyMethod()
{
dep1.DoSomething();
// new dependency
dep2.DoSomething();
}
}
您可以直接发现依赖项,并且在满足它们之前不能使用这些类。
在典型的业务应用程序中,您应该避免使用服务位置。它应该是没有其他选项时使用的模式。
没有。
例如,没有服务定位,控制容器的反转就无法工作。这是他们内部解决服务的方式。
但更好的例子是ASP.NET MVC和WebApi。您认为控制器中的依赖注入可能是什么?这是对的 - 服务地点。
但等一下,如果我们使用DI方法,我们会介绍一个 与构造函数中的另一个参数的依赖关系(如果是 构造函数注入)。问题仍然存在。
还有两个更严重的问题:
使用容器进行构造函数注入时,可以免费获得。
如果可以的话 忘记设置ServiceLocator,然后我们可能忘记添加一个新的 在我们的IoC容器和DI方法中的映射将具有相同的 运行时问题。
这是真的。但是使用构造函数注入,您不必扫描整个类来确定缺少哪些依赖项。
一些更好的容器也会在启动时验证所有依赖项(通过扫描所有构造函数)。因此,使用这些容器,您可以直接获得运行时错误,而不是稍后的时间点。
此外,作者还提到了单元测试的难点。但是,我们不会有DI方法的问题吗?
没有。因为您没有依赖静态服务定位器。您是否尝试过使用静态依赖项进行并行测试?这不好玩。
答案 1 :(得分:36)
我还想指出,如果你重构遗留代码,服务定位器模式不仅不是反模式,而且它也是一种实际需要。没有人会在数百万行代码上挥动魔杖,突然所有的代码都准备就绪了。因此,如果您想开始将DI引入现有代码库,通常情况下您会将内容更改为DI服务,并且引用这些服务的代码通常不会是DI服务。因此,这些服务将需要使用服务定位器,以获取已转换为使用DI的那些服务的实例。
因此,当重构大型遗留应用程序以开始使用DI概念时,我会说服务定位器不仅不是反模式,而且它是将DI概念逐渐应用于代码库的唯一方法。
答案 2 :(得分:7)
从测试的角度来看,Service Locator很糟糕。从分钟8:45开始,请参阅Misko Hevery的Google Tech Talk代码示例http://youtu.be/RlfLCWKxHJ0的精彩解释。我喜欢他的比喻:如果你需要25美元,直接要钱,而不是从钱包里拿钱包。他还将服务定位器与具有您需要的针头的干草堆进行比较,并知道如何检索它。使用Service Locator的类很难重用,因为它。
答案 3 :(得分:4)
维护问题(让我感到困惑)
在这方面使用服务定位器的原因有两个不同。
简单明了:一个带有服务定位器的类比通过其构造函数接受其依赖关系的更难以重用。
考虑以下情况:您需要使用其作者决定使用的
LibraryA
服务ServiceLocatorA
以及LibraryB
的作者决定使用ServiceLocatorB
的服务。除了在我们的项目中使用2个不同的服务定位器,我们别无选择。如果我们没有好的文档,源代码或快速拨号上的作者,那么需要配置多少依赖项就是猜谜游戏。如果没有这些选项,我们可能需要使用反编译器 来确定依赖关系。我们可能需要配置2个完全不同的服务定位器API,根据设计,可能无法简单地包装现有的DI容器。根本不可能在两个库之间共享一个依赖关系的实例。如果服务定位器实际上并没有与我们需要的服务实际驻留在同一个库中,那么项目的复杂性甚至会更加复杂 - 我们隐含地将其他库引用拖入我们的项目中。现在考虑使用构造函数注入提供的相同的两个服务。添加对
LibraryA
的引用。添加对LibraryB
的引用。在DI配置中提供依赖关系(通过Intellisense分析所需的内容)。完成。Mark Seemann有StackOverflow answer that clearly illustrates this benefit in graphical form,它不仅适用于使用其他库中的服务定位器,也适用于在服务中使用外部默认值。
答案 4 :(得分:1)
我的知识不足以判断这一点,但总的来说,我认为如果某些东西在特定情况下有用,那并不一定意味着它不能成为一种反模式。特别是,当您处理第三方库时,您无法完全控制所有方面,最终可能会使用不是最好的解决方案。
以下是来自自适应代码通过C#的段落:
“不幸的是,服务定位器有时是不可避免的反模式。在某些应用程序类型中 - 特别是Windows Workflow Foundation-基础结构不适合构造函数注入。在这些情况下,唯一的选择是使用服务定位器这比不注入依赖项要好。对于我反对(反)模式的所有讽刺,它比手动构建依赖项要好得多。毕竟,它仍然允许那些允许装饰器的接口提供的所有重要扩展点。 ,适配器和类似的好处。“
- 霍尔,加里麦克莱恩。通过C#的自适应代码:具有设计模式和SOLID原则的敏捷编码(开发人员参考)(p.309)。皮尔逊教育。
答案 5 :(得分:0)
作者的理由&#34;编译器不会帮助你&#34; - 这是真的。 当你设计一个课程时,你会想要仔细选择它的界面 - 除了其他目标之外,还要让它独立......因为它有意义。
让客户端通过显式接口接受对服务(对依赖项)的引用,
你是对的DI有其问题/缺点,但到目前为止提到的优势超过了他们...... IMO。 你是对的,在DI中,接口(构造函数)中引入了一个依赖关系 - 但希望你需要的依赖关系,以及你想要显示和检查的依赖关系。
答案 6 :(得分:0)
是的,服务定位器是violates encapsulation和solid的反模式。
答案 7 :(得分:0)
服务定位器(SL)
Service Locator
解决了 [DIP
+ DI
] 问题。它允许通过接口名称满足需求
class A {
IB ib
init() {
ib = ServiceLocator.resolve<IB>();
}
}
这里的问题是不清楚客户端(A)究竟使用了哪些类(IB 的实现)。服务定位器可以作为单例,也可以传递给构造函数。
提案:
class A {
IB ib
init(ib: IB) {
self.ib = ib
}
}
SL vs DI IoC 容器(框架)
SL 是关于保存实例,而 DI IoC 容器(框架)更多的是关于创建实例。
SL 在检索构造函数中的依赖项时用作 PULL 命令 DI IoC Container(framework) 在将依赖项放入构造函数时作为 PUSH 命令工作