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

来源:http://www.cnblogs.com/dongbeifeng/archive/2016/01/17/service-locator-is-an-anti-pattern.html
-Advertisement-
Play Games

服務定位器隱藏了類之間的依賴關係,導致錯誤從編譯時推遲到了運行時,並且,在引入破壞性更改時,這個模式導致代碼不清晰,增加了維護難度。


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

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

總結

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

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • [前言]在張銀奎老師的《軟體調試》一書中,詳細地講解了使用記憶體的分支記錄機制——BTS機制(5.3),並且給出了示例工具CpuWhere及其源代碼。但實際運行(VMware XP_SP3 單核)並沒有體現應有的效果,無法讀取到分支記錄。查看了源代碼並沒有發現任何問題,與書中所講一致。既然軟體本身沒有...
  • 最近,有個項目突然接到總部的安全漏洞報告,查看後知道是XSS攻擊。問題描述: 在頁面上有個隱藏域: 當前頁面提交到Controller時,未對action屬性做任何處理,直接又回傳到頁面上 如果此時action被用戶惡意修改為:***""*** 此時當頁面刷新時將執行alert(1),雖然錯...
  • 在插件實例修改3增加一個聯繫人功能配置文件 1 2 3 4 5 6 7 8 9 10 11 12...
  • define的用法小結define的用法只是一種純粹的替換功能,巨集定義的替換是預處理器處理的替換。 一:簡單的巨集定義用法 格式:#define 標識符 替換內容 替換的內容可以是數字,字元,字元串,特殊字元和空格,後面是什麼內容就會替換成什麼內容。 例如: #define N 5 效...
  • 參考書籍:Head First Java1、假設某方法是別人寫在某個類裡面的2、而此時你根本就不知道這個方法是否有風險(比如伺服器出故障會使程式受到影響);3、那最好的方法應該就是,在調用這個類的方法時,加上可能發生異常的處理方案,未雨綢繆。關鍵字:try……catch,throws,throw,f...
  • Day 2308 Udp接收端09 Udp鍵盤錄入數據方式10 Udp聊天11 TCP傳輸12 TCP傳輸213 TCP練習14 TCP複製文件08 Udp接收端需求:定義一個應用程式,用於接收udp協議傳輸的數據並處理。思路:1.定義UdpSocket服務。2.定義一個數據報包,因為要存儲接收到的...
  • 手動安裝django_chartit庫1 下載壓縮包2 解壓到python安裝目錄下,文件夾名為django_chartit,並檢查文件夾下是否有setup.py文件3 在cmd中進入django_chartit文件夾下,cmd命令為 cd C:\Python27\django_chartit4 輸...
  • 搭建一個簡單的跨平臺C開發的基礎框架, 主要是使用posix線程庫和 自己寫的一個分級 日誌庫,以後可以在這個基礎上搭建你需要的框架會容易一點. 在Window和Linux上測試通過.
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...