[翻译] 服务定位器是反模式

原文: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);

所以,从维护人员的观点看,改善并不多。增加新的依赖项时,还是不知道是不是引入了破坏性的更改。

总结

使用服务定位器产生的问题,不是由于特定的实现造成的(尽管这也可能是个问题),而是因为它是反模式。它对于开发者和维护者来说都有问题。使用构造函数注入依赖项时,编译器可以给使用者和开发者很多帮助,如果使用服务定位器,这些好处就没有了。

 

posted @ 2016-01-17 00:48  东北风!  阅读(1962)  评论(2编辑  收藏  举报