Service Locator 是一个众所周知的模式,按照 Martin Fowler 的说法,那它一定不错,对吧?
不,它实际上是一种反模式,应该避免。
为什么会这样? 简而言之,Service Locator 的问题在于它隐藏了类的依赖关系,导致运行时错误而不是编译时错误,并且因为不清楚何时引入破坏性的变更,使代码更难维护。
OrderProcessor 的例子
我们挑一个最近很火的,与 DI (依赖注入)相关的例子:OrderProcessor。
要处理订单,OrderProcessor 必须验证订单并在有效时发货。
下面是一个使用静态 Service Locator 的示例:
public class OrderProcessor : IOrderProcessor
{
public void Process(Order order)
{
var validator = Locator.Resolve<IOrderValidator>();
if (validator.Validate(order))
{
var shipper = Locator.Resolve<IOrderShipper>();
shipper.Ship(order);
}
}
}
Service Locator 是使用 new 操作符的替代品,它的实现如下
public static class Locator
{
private readonly static Dictionary<Type, Func<object>>
services = new Dictionary<Type, Func<object>>();
public static void Register<T>(Func<T> resolver)
{
Locator.services[typeof(T)] = () => resolver();
}
public static T Resolve<T>()
{
return (T)Locator.services[typeof(T)]();
}
public static void Reset()
{
Locator.services.Clear();
}
}
我们可以使用 Register 方法把一个类注册到 Service Locator。一个“真正的” Service Locator 远比这更复杂,但这个例子说明了核心设计。
这个设计灵活且可扩展,它甚至支持用 Test Doubles(替代测试) 替换服务,我们很快就会看到。
那么,问题是什么呢?
作为 API 使用时的问题
假设 OrderProcessor 不是我们自己写的,是别人以二进制形式给我们的,我们也还没有在 Reflector 查看它。
这是我们从 Visual Studio 中的 IntelliSense 得到的提示:
好的,所以这个类有一个默认的构造函数。这意味着我可以简单地创建它的一个新实例并立即调用 Process 方法:
var order = new Order();
var sut = new OrderProcessor();
sut.Process(order);
很遗憾,运行此代码会抛出 KeyNotFoundException,因为 IOrderValidator 从未在 Locator 中注册过。
这不仅令人惊讶,如果我们无法访问源代码,这还会让人感到莫名其妙。
通过仔细阅读源代码(或使用 Reflector)或查阅文档(恶心!),我们可能最终会发现我们需要在 Locator(一个完全不相关的静态类)中注册一个 IOrderValidator 实例,然后才能正常工作。
在单元测试测试中,可以这样做:
var validatorStub = new Mock<IOrderValidator>();
validatorStub.Setup(v => v.Validate(order)).Returns(false);
Locator.Register(() => validatorStub.Object);
更烦人的是,由于 Locator 的内部存储是静态的,我们需要在每次单元测试后调用 Reset 方法。当然,此问题仅限于单元测试。
总而言之,这种设计给开发者带来了不愉快的体验。
维护问题
从消费者的角度来看,这种 Service Locator 的使用存在问题。然而,对于维护开发人员来说,看似简单的事情很快也会成为一个问题。
假设我们需要扩展 OrderProcessor 的行为以同时调用 IOrderCollector.Collect 方法。这很容易,是吗?
public void Process(Order order)
{
var validator = Locator.Resolve<IOrderValidator>();
if (validator.Validate(order))
{
var collector = Locator.Resolve<IOrderCollector>();
collector.Collect(order);
var shipper = Locator.Resolve<IOrderShipper>();
shipper.Ship(order);
}
}
乍一看,这很容易:我们只需添加对 Locator.Resolve 的新调用并调用 IOrderCollector.Collect。
这是一个破坏性的变更吗?
这可能出奇地难以回答。它可以通过编译,但破坏了我的一个单元测试。
如果是在生产环境中,会发生什么?
IOrderCollector 接口可能已经注册到服务定位器,因为它已经被其他组件使用,这种情况下它可以顺利工作。如果 IOrderCollector 尚未被注册,问题就很大了。
最重要的是,判断您是否正在引入重大更改变得更加困难。您需要了解使用服务定位器的整个应用程序,并且编译器不会帮助您。
变体:Concrete Service Locator 实体服务定位器
我们能以某种方式解决这些问题吗?
一种常见的变体是使服务定位器成为一个具体的类,像这样使用:
public void Process(Order order)
{
var locator = new Locator();
var validator = locator.Resolve<IOrderValidator>();
if (validator.Validate(order))
{
var shipper = locator.Resolve<IOrderShipper>();
shipper.Ship(order);
}
}
但是,要让其可被配置,仍然需要一个静态内存存储:
public class Locator
{
private readonly static Dictionary<Type, Func<object>>
services = new Dictionary<Type, Func<object>>();
public static void Register<T>(Func<T> resolver)
{
Locator.services[typeof(T)] = () => resolver();
}
public T Resolve<T>()
{
return (T)Locator.services[typeof(T)]();
}
public static void Reset()
{
Locator.services.Clear();
}
}
换句话说:具体的服务定位器和我们已经讨论过的静态服务定位器之间没有结构上的区别。它有同样的问题,但没有解决任何问题。
变体: Abstract Service Locator 抽象服务定位器
此种服务定位器是一个实现接口的具体类,这个变体似乎更加符合 DI 的设计哲学。
public interface IServiceLocator
{
T Resolve<T>();
}
public class Locator : IServiceLocator
{
private readonly Dictionary<Type, Func<object>> services;
public Locator()
{
this.services = new Dictionary<Type, Func<object>>();
}
public void Register<T>(Func<T> resolver)
{
this.services[typeof(T)] = () => resolver();
}
public T Resolve<T>()
{
return (T)this.services[typeof(T)]();
}
}
通过这种变体,有必要将服务定位器注入消费者。
构造函数注入始终是注入依赖项的不错选择,因此 OrderProcessor 变形为以下实现:
public class OrderProcessor : IOrderProcessor
{
private readonly IServiceLocator locator;
public OrderProcessor(IServiceLocator locator)
{
if (locator == null)
{
throw new ArgumentNullException("locator");
}
this.locator = locator;
}
public void Process(Order order)
{
var validator =
this.locator.Resolve<IOrderValidator>();
if (validator.Validate(order))
{
var shipper =
this.locator.Resolve<IOrderShipper>();
shipper.Ship(order);
}
}
}
从开发人员的角度来看, IntelliSense 终于能整点有用的了。。。吗?
其实没什么大用。
OrderProcessor 需要一个 ServiceLocator——这比以前多了一些信息,但它仍然没有告诉我们需要哪些服务。
以下代码可以编译,但会因与之前相同的 KeyNotFoundException 而崩溃:
var order = new Order();
var locator = new Locator();
var sut = new OrderProcessor(locator);
sut.Process(order);
从维护开发人员的角度来看,情况也没有太大改善。
如果我们需要添加一个新的依赖项,我们仍然得不到任何帮助:它是否是一个破坏性的变更?和以前一样难说。
总结
使用服务定位器的问题不在于您依赖于特定的服务定位器实现(尽管这也可能是一个问题),而是它是一个真正的反模式。
它会给 API 的使用者带来糟糕的开发体验,并且会让您作为维护开发人员的生活变得更糟,因为您需要动用大量的脑力来理解您所做的每一个更改的含义。
当使用构造函数注入时,编译器可以为消费者和生产者提供很多帮助,但是对于依赖服务定位器的 API,这些帮助都没有。
您可以在我的书中阅读更多关于 DI 模式和反模式的信息。
2014-05-20 更新:
另一种解释服务定位器负面影响的方式是它违反了 SOLID。
2015-10-26 更新:
Service Locator 的根本问题在于它违反了封装。