[翻譯] 服務定位器是反模式


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

所以,從維護人員的觀點看,改善並不多。增加新的依賴項時,還是不知道是不是引入了破壞性的更改。

總結

使用服務定位器產生的問題,不是由於特定的實現造成的(盡管這也可能是個問題),而是因為它是反模式。它對於開發者和維護者來說都有問題。使用構造函數注入依賴項時,編譯器可以給使用者和開發者很多幫助,如果使用服務定位器,這些好處就沒有了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM