[翻译] 服务定位器是反模式
原文:Service Locator is an Anti-Pattern
服务定位器模式广为人知,Martin Fowler在文章中专门描述过它(译文)。所以它一定是好的,对不对?
并不是这样。服务定位器实际上是个反模式,应该避免使用。我们来研究一下。简单来讲,服务定位器隐藏了类之间的依赖关系,导致错误从编译时推迟到了运行时,并且,在引入破坏性更改时,这个模式导致代码不清晰,增加了维护难度。
OrderProcessor 示例
我们用依赖注入话题中常见的OrderProcessor示例作说明。OrderProcessor 处理订单的过程是:先验证,通过后再发货。下面的代码使用静态的服务定位器:
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); } } }
这里,我们用服务定位器替换了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方法用来配置服务定位器。真实项目中的服务定位器要复杂的多,不过这些代码在这里足够用了。这个实现灵活且可扩展,也可以替换服务进行测试。那么,问题在哪?
API 使用问题
先假设我们是 OrderProcessor 类的消费者,它不是我们自己写的,而是由第三方提供的。我们还没有用Reflector来看它的代码。编码时 Visual Studio 智能感知会给出以下提示:
我们看到一个默认构造函数,就是说,我们可以创建一个新实例然后立刻调用Process 方法:
var order = new Order(); var sut = new OrderProcessor(); sut.Process(order);
这段代码在执行时会抛出 KeyNotFoundException,因为 IOrderValidator 还没有注册到服务定位器。在没看到 OrderProcessor 源码之前很难知道哪里出了问题。只有在仔细检查源码(或者用Reflector)或者查阅文档之后才能搞清楚,原来在使用 OrderProcessor 之前要首先向服务定位器(它是个完全不相干的静态类)注册一个 IOrderValidator 实例。
进行单元测试时,我们可能像下面这么写:
var validatorStub = new Mock<IOrderValidator>(); validatorStub.Setup(v => v.Validate(order)).Returns(false); Locator.Register(() => validatorStub.Object);
但是,由于定位器内部的存储变量是静态的,每个测试执行完,都需要调用 Reset 方法来清理一下,这是单元测试时的问题。
所以,很难说这样的 API 设计提供了好的开发体验。
维护问题
除了消费者会遇到问题以外,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 可能已经注册过了,比如其他组件使用过它,这种情况下就不会报错。但是也可能没有注册过。
这里的本质问题是很难说清这个更改是不是破坏性的。你必须理解整个程序是怎么使用服务定位器的,编译器在这里帮不上忙。
变种:非静态类的服务定位器
那么有没有办法修复这些问题?一个变种是使用非静态的服务定位器,像这样:
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(); } }
换言之,不管定位器是不是静态的,都没有结构上的差异,问题还在。
变种:抽象的服务定位器
另一个变种似乎更符合依赖注入的做法:把服务定位器作为接口的实现。
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); } } }
现在事情好些了没有?
使用时,我们看到的是这样的提示:
作用其实很有限,仅仅是 OrderProcessor 需要一个 ServiceLocator ---- 比无参构造函数的版本好了一点,但是仍然不知道 OrderProcessor 具体需要哪些服务。下面的代码可以编译,但在运行时还是会抛出 KeyNotFoundException:
var order = new Order(); var locator = new Locator(); var sut = new OrderProcessor(locator); sut.Process(order);
所以,从维护人员的观点看,改善并不多。增加新的依赖项时,还是不知道是不是引入了破坏性的更改。
总结
使用服务定位器产生的问题,不是由于特定的实现造成的(尽管这也可能是个问题),而是因为它是反模式。它对于开发者和维护者来说都有问题。使用构造函数注入依赖项时,编译器可以给使用者和开发者很多帮助,如果使用服务定位器,这些好处就没有了。