服務定位器隱藏了類之間的依賴關係,導致錯誤從編譯時推遲到了運行時,並且,在引入破壞性更改時,這個模式導致代碼不清晰,增加了維護難度。
原文: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);
所以,從維護人員的觀點看,改善並不多。增加新的依賴項時,還是不知道是不是引入了破壞性的更改。
總結
使用服務定位器產生的問題,不是由於特定的實現造成的(儘管這也可能是個問題),而是因為它是反模式。它對於開發者和維護者來說都有問題。使用構造函數註入依賴項時,編譯器可以給使用者和開發者很多幫助,如果使用服務定位器,這些好處就沒有了。