[Architecture] Service Locator

摘要:[Architecture] Service Locator

動機

Service Locator是一個在開發系統時,很常用的一個模式。在Martin Fowler寫的Inversion of Control Containers and the Dependency Injection pattern裡,可以發現這個Pattern的身影。Service Locator最主要是定義BLL層內物件生成、物件存放、物件取得的職責,讓系統在取得物件時不需要知道物件是如何生成及存放,有效降低系統的耦合性。

 

同時學習Service Locator,也為架構設計帶入了空間的概念。在設計架構的時候,可以套用Service Locator來做為架構空間的封裝。將經物件生成建立的物件,「存放」在Service Locator內,讓目標架構「存在」一組物件可以提供使用。

 

 

本篇文章介紹一個Service Locator的實做,這個實做定義物件之間的職責跟互動,用來完成Service Locator應該提供的功能及職責。為自己做個紀錄,也希望能幫助到有需要的開發人員。

 

 

結構

 

接下來沿用[Architecture Pattern] Repository建立的UserCountService,來當作範例的內容。另外在BLL層使用Service Locator來封裝UserCountService的物件生成、物件存放、物件取得。範例的結構如下:

 

 

 

主要的參與者有:

 

 

ServiceLocator
-提供存放物件的功能給系統使用。
-提供存放的物件給系統使用。

 

 

Client
-使用ServiceLocator內存放的UserCountService。

 

 

UserCountService
-使用系統內User資料計算各種Count。
-由Client生成或是ServiceLocator生成。

 

 

透過下面的圖片說明,可以了解相關物件之間的互動流程。

 

 


 

 

實做

 

Service Locator由兩種運作邏輯所組成:定位邏輯、生成邏輯。定位邏輯是整個Service Locator的核心,它定義物件存放、物件取得的職責。而物件必須要被生成才能夠使用,生成邏輯就是定義物件生成的職責。接著透過實做一組Service Locator,解析Service Locator內的運作邏輯,幫助開發人員理解Service Locator模式。

 

範列下載

 

實做說明請參照範例程式內容:ServiceLocatorSample點此下載

 

定位邏輯

 

首先建立ServiceLocatorSample.BLL專案,並且建立ServiceLocator物件用來封裝Service Locator的定位邏輯:ServiceLocator提供SetInstance方法,讓系統可以存放各種型別的物件。並且ServiceLocator提供GetInstance方法,讓系統可以取得先前存放的物件。

 

public partial class ServiceLocator
{
    // Fields  
    private readonly Dictionary<Type, object> _serviceDictionary = new Dictionary<Type, object>();


    // Methods
    public TService GetInstance<TService>() where TService : class
    {
        // Result
        TService service = default(TService);

        // Exist
        if (_serviceDictionary.ContainsKey(typeof(TService)) == true)
        {
            service = _serviceDictionary[typeof(TService)] as TService;
        }

        // Return
        return service;
    }

    public void SetInstance<TService>(TService service) where TService : class
    {
        #region Require

        if (service == null) throw new ArgumentNullException();

        #endregion

        // Set
        if (_serviceDictionary.ContainsKey(typeof(TService)) == false)
        {
            _serviceDictionary.Add(typeof(TService), service);
        }
        else
        {
            _serviceDictionary[typeof(TService)] = service;
        }
    }
}

 

另外ServiceLocator也套用了Singleton pattern,讓系統能方便的使用ServiceLocator。

 

 

public partial class ServiceLocator
{
    // Singleton
    private static ServiceLocator _current;

    public static ServiceLocator Current
    {
        get
        {
            if (_current == null)
            {
                _current = new ServiceLocator();
            }
            return _current;
        }
        set
        {
            _current = value;
        }
    }
}

 

外部生成邏輯

 

再來建立一個Console專案,透過ServiceLocator取得UserCountService用來列印的人員數量。而UserCountService是由Console專案來生成、注入ServiceLocator。

 

class Program
{
    static void Main(string[] args)
    {
        // Initialize
        InitializeServiceLocator();
            
        // UserCountService
        UserCountService userCountService = ServiceLocator.Current.GetInstance<UserCountService>();

        // Print
        Console.WriteLine("All Count : " + userCountService.GetAllCount());
        Console.WriteLine("Men Count : " + userCountService.GetMenCount());

        // End
        Console.ReadLine();
    }

    static void InitializeServiceLocator()
    {
        // UserRepository
        IUserRepositoryProvider userRepositoryProvider = new SqlUserRepositoryProvider();
        UserRepository userRepository = new UserRepository(userRepositoryProvider);
        UserCountService userCountService = new UserCountService(userRepository);

        // SetInstance
        ServiceLocator.Current.SetInstance<UserCountService>(userCountService);
    }
}

 

 

 

內部生成邏輯

 

到目前為止範例程式,已經可以透過ServiceLocator取得UserCountService用來列印的人員數量。但UserCountService是由ServiceLocator之外的函式來生成、存放至ServiceLocator。這造成每次重用的時候,必須要重新建立物件生成、存放的功能。

 

為了增加ServiceLocator的重用性,所以修改ServiceLocator物件,封裝Service Locator的生成邏輯:ServiceLocator提供CreateInstance方法,讓系統可以建立各種型別的物件。並且變更GetInstance的運作流程,讓系統取不到先前存放的物件時,會去使用CreateInstance來生成物件。

 

 

(因為是模擬範例,簡化了很多UserCountService的設計,並且採用直接建立的方式來示意。實際專案可以採用各種IoC Framework來做生成注入,或是套用各種Factory pattern,這些都能提高ServiceLocator的重用性。)

 

 

public partial class ServiceLocator
{
    // Fields  
    private readonly Dictionary<Type, object> _serviceDictionary = new Dictionary<Type, object>();


    // Methods
    protected virtual TService CreateInstance<TService>() where TService : class
    {
        // Result
        TService service = default(TService);

        // UserCountService
        if (typeof(TService) == typeof(UserCountService))
        {
            IUserRepositoryProvider userRepositoryProvider = new CsvUserRepositoryProvider();
            UserRepository userRepository = new UserRepository(userRepositoryProvider);
            UserCountService userCountService = new UserCountService(userRepository);
            service = userCountService as TService;
        }

        // Return
        return service;
    }

    public TService GetInstance<TService>() where TService : class
    {
        // Result
        TService service = default(TService);

        // Exist
        if (_serviceDictionary.ContainsKey(typeof(TService)) == true)
        {
            service = _serviceDictionary[typeof(TService)] as TService;
        }
        if (service != null) return service;

        // Create
        service = this.CreateInstance<TService>();
        if (service != null) this.SetInstance<TService>(service);

        // Return
        return service;
    }

    public void SetInstance<TService>(TService service) where TService : class
    {
        #region Require

        if (service == null) throw new ArgumentNullException();

        #endregion

        // Set
        if (_serviceDictionary.ContainsKey(typeof(TService)) == false)
        {
            _serviceDictionary.Add(typeof(TService), service);
        }
        else
        {
            _serviceDictionary[typeof(TService)] = service;
        }
    }
}

 

 

 

最後修改Console專案,移除專案內生成UserCountService的邏輯。並且同樣透過ServiceLocator取得UserCountService用來列印的人員數量。

 

 

class Program
{
    static void Main(string[] args)
    {
        // UserCountService
        UserCountService userCountService = ServiceLocator.Current.GetInstance<UserCountService>();

        // Print
        Console.WriteLine("All Count : " + userCountService.GetAllCount());
        Console.WriteLine("Men Count : " + userCountService.GetMenCount());

        // End
        Console.ReadLine();
    }
}

 

後記

 

整個Service Locator實做看來下,眼尖的開發人員會發現,它跟IoC Framework有異曲同工的意味。Service Locato跟IoC Framework的差異,主要是在:IoC Framework的主要職責是物件生成,Service Locator的主要職責是物件存放、物件取得。只是發展到了後來,兩者幾乎都實做了物件生成、物件存放、物件取得三個職責。換個方式說,大多的IoC Framework都封裝了Service Locator的職責。部分Service Locator也封裝了IoC Framework的職責。雖然說結果看起來是一樣,但兩者設計的出發點是有差異的。

 

而Service Locator有很多實做版本,這些實做版本依照需求分割、設計,BLL層內物件生成、物件存放、物件取得的職責,用以提高整體架構的重用性、可維護性。一個系統的成敗,除了最基本的滿足客戶需求之外,這些額外的非功能需求也是很重要的一環。這讓後續接手維護的開發人員,能夠早點回家吃晚餐。:D

 

期許自己
能以更簡潔的文字與程式碼,傳達出程式設計背後的精神。
真正做到「以形寫神」的境界。