Software Design - Pattern - Service Locator
Quick Chat
Service Locator 是一種 依賴尋找(Dependency Lookup) 的實作方式,它確實屬於控制反轉(IoC)的一種。
過去我曾頻繁使用這個模式,當時對於 DI(依賴注入)與 IoC 的概念還不熟悉,只覺得它讓依賴的取用變得非常方便,因為你可以在任何地方直接這樣取得所需物件:
var target = ServiceLocator.Resovle<Target>();
然而,這種作法存在幾個顯著問題:
- 隱性依賴:由於依賴的取得過程被隱藏起來,程式碼的真實依賴關係難以被一眼識別,增加了閱讀與維護的難度。
- 打破分層原則:任何元件都可以隨意取得服務,這可能會導致 View 層級的物件存取到不屬於其範疇的業務邏輯,破壞了應用程式的架構分層。
因此,當我對 DI 與 IoC 的概念更加熟悉,並開始使用 DI/IoC Container 後,就逐漸棄用了 Service Locator 這個模式。
Situation
這次的專案讓我重新思考了 Service Locator 的價值。我發現它非常適合做為從 Singleton 過渡到 DI/IoC Container 的中間階段。
我們目前專案嚴重依賴 Singleton 模式,團隊成員也已經習慣了這種寫法。若要直接切換到 DI/IoC Container,勢必會引發不小的陣痛期。
在這種情況下,Service Locator 成了理想的過渡方案,理由如下:
- 寫法近似 Singleton:它的使用方式與靜態的 Singleton 模式極為相似,能讓團隊成員無痛轉換,降低學習成本。
- 快速感受 IoC 好處:儘管只是過渡,但它能讓團隊成員立即感受到集中管理依賴所帶來的便利性,為後續全面採用 DI/IoC Container 鋪路。
- 集中管理依賴:它提供一個中央註冊點,讓所有依賴關係被統一看管。
Practice
這種模式使用一個中央註冊表(Service Locator)來根據請求返回執行特定任務所需的物件。
以下是一個簡化版的實作範例,它有別於一般的 IoC Container,具有以下特色:
- 不強制介面實作:註冊的物件可以不必實作特定介面。
- 單純的容器:主要功能就是作為物件的儲存庫。
- 不使用反射:為避免效能開銷,它搭配一個 Installer(組合根) 來進行物件的註冊。
// 這裡透過 static 達成全域存取
public class ServiceLocator
{
private static readonly Dictionary<Type, object> instances = new Dictionary<Type, object>();
// 註冊服務
public static void Register<T>(T instance)
{
instances[typeof(T)] = instance;
}
// 尋找服務
public static T Resolve<T>()
{
if (instances.TryGetValue(typeof(T), out var instance))
{
return (T)instance;
}
throw new Exception($"Service of type {typeof(T)} is not registered.");
}
// 釋放服務 (可選)
public static void Release<T>()
{
if (instances.ContainsKey(typeof(T)))
{
instances.Remove(typeof(T));
}
}
}
// 由 Installer 集中註冊物件
public class DemoBasicServiceLocatorsInstaller : MonoBehaviour
{
public DemoBasicMoneyUI moneyUI;
public MoneyType moneyType;
public int moneyValue = 100;
void Start()
{
switch (moneyType)
{
case MoneyType.Real: ServiceLocator.Register<IMoneyFormatConverter>(new RealMoneyFormatConverter()); break;
case MoneyType.Coin: ServiceLocator.Register<IMoneyFormatConverter>(new CoinMoneyFormatConverter()); break;
default: throw new System.NotImplementedException();
}
moneyUI.Show(moneyValue);
}
}
// 在需要的地方從容器取得服務
public class DemoBasicMoneyUI : MonoBehaviour
{
[SerializeField] private TMP_Text text;
private IMoneyFormatConverter moneyFormatConverter;
public void Show(int moneyValue)
{
// 透過 ??= 確保只在第一次使用時尋找服務
moneyFormatConverter ??= ServiceLocator.Resolve<IMoneyFormatConverter>();
text.text = moneyFormatConverter.Convert(moneyValue);
}
}