前言:

在前一篇的 Anti-Pattern 中我們介紹了最經典的反模式:Control Freak 。而在這篇我們要介紹的另外一個常見的反模式可以說是他的進化版本,尤其是這個模式是知名的架構師 Martin Fowler 提出的。不過在數年後有人挑戰了這個想法,該文章的討論串比正文還要長,但思辨的過程還是值得一看。

常見類型

Anti-patternDescription
Control Freak依賴反轉(Inversion of Control)的對立面,直接控制實體
Service Locator使用一個內隱服務提供依賴給呼叫端
Ambient Context使用靜態存取子,提供單一的依賴
Constrained Construction讓建構子有一個特定的方法簽章

Service Locator

是一種軟體開發中的設計模式,通過一個集中註冊表(通常會是 Singleton)來處理請求並返回處理特定任務所需的必要訊息。最常見的實做方式通常就是利用 Static Factory 來實做這個註冊表。而系統中可以在系統進入點以外的地方呼叫這個註冊表的方法取得指定的實做。

表準案例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class HomeController : Controller
{
    public HomeController() { }

    public ViewResult Index()
    {
        IProductService service =
            Locator.GetService<IProductService>();

        var products = service.GetProducts();

        return this.View(products);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class ProductService : IProductService
{
    private readonly IProductRepository repository;

    public ProductService()
    {
        this.repository = Locator.GetService<IProductRepository>();
    }

    public IEnumerable<Product> GetProducts() { return this.repository.GetProducts(); }
}

這邊可以看到當要使用 IProductService 時,就呼叫註冊表的方法來取得實做,但這個依賴關係因為不是在建構時給予因此從外部會不容易知道他們兩者之間會有這樣的關係存在。另外這個作法同樣也會遇到 Static Factory 會遇到的問題,只是 Service Locator 將原本呼叫與依賴關係建立的那一層做了一個包裝。原本的 Static Factory 會與實做相同介面的類別緊緊的綁在一個 Factory 類別中,而包裝起來之後變成所有類型的依賴與實做會都綁在 Service Locator 這個類別裡,它變得與更多依賴有關。同時,在使用上每個需要取得依賴的地方都需要能存取 Service Locator 。

而這個 Pattern 重要的主角 Service Locator 大致上的程式會長的像下面這段 Code ps. 通常這個靜態的註冊表會使用 Singleton Pattern 來確保系統內就一張,避免依賴註冊到不同表的尷尬事發生。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public static class Locator
{
    private static Dictionary<Type, object> services =
        new Dictionary<Type, object>();

    public static void Register<T>(T service)
    {
        services[typeof(T)] = service;
    }

    public static T GetService<T>()
    {
        return (T)services[typeof(T)];
    }

    public static void Reset()
    {
        services.Clear();
    }
}

ps. 這邊只列出核心的邏輯與概念,所以沒有加上任何保護的區塊,也沒有提供取得複數個實做的方法。

Service Locator 真的不好嗎?

畢竟是大師提的 solution 它確實有解決一些問題,而且有一段時間很多人都沒有發現他的問題所在。它難以察覺的問題點和明顯的優點讓它當時相當具有吸引力。

  • 首先就是你可以平行來開發了,只要先定義好介面就可以開始。
  • 因為可以先訂好介面,可以專心在本身的邏輯與流程上。
  • 在測試上你可以 mock 該 interface 的實做,方便做到單元測試。

恩~我覺得光是讓單元測試可以更容易做到就讓這個 solution 在當時讓人難以抗拒,畢竟就像那句名言:「你能相信的不是大師寫出來的 Code,而是有被測試過得 Code。」。

Service Locator 的缺點

使用 Service Locator 最明顯的問題在於,它容易隱藏類別的依賴,在使用上若沒有事先註冊依賴的話,你只能在執行期間才發現問題,但是依賴關係卻無法從建構子中看出,像是若我不說的話你怎麼會知道 ProductService 竟然跟 IProductRepository 有關係,而且你 Locator 一定要在呼叫前就註冊好。當然,如果這個類別是由開發團隊所維護,或是方便能取得原始碼的話問題就沒這麼大,但若這是一個分享出去的元件呢?這個要註冊什麼東西的問題就會變得相當 tricky 而且容易被遺忘。

如何重構 Service Locator

Service Locator 的重構方式其實跟 Control Freak 有點相像,當使用 Service Locator 時你一定會在系統各處發現呼叫並使用 Locator 類別,這跟 Control Freak 可以到處發現 new 類別是相似的,也因此第一步就是將這些使用 Locator 的地方統合一來。

再來就是用 readonly 屬性,強迫讓這些依賴只能在一開始的時候賦予。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class HomeController : Controller
{
    private readonly IProductService productService;
    public HomeController() 
    {
        productService = Locator.GetService<IProductService>();
    }

    public ViewResult Index()
    {
        var products = productService.GetProducts();

        return this.View(products);
    }
}

若你已將這些依賴都放在建構子這邊的話,就可以改用建構子注入的方式來改寫這段程式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class HomeController : Controller
{
    private readonly IProductService productService;
    public HomeController() 
    {
        this.productService = Locator.GetService<IProductService>();
    }

    // 建構子注入的方式解除相依
    public HomeController(IProductService service)
    {
        this.productService = service
    }

    public ViewResult Index()
    {
        var products = productService.GetProducts();

        return this.View(products);
    }
}

在測試使用 DI Container 取得 Controller 實體沒有問題的話就可以將無建構子的方法給拔除,這樣就可以完成重構了。