[Architecture] Repository

摘要:[Architecture] Repository

動機

Repository Pattern是一個在開發系統時,很常用的一個模式。在一些大師的著作:不管是在Martin Fowler所寫的PoEAA或者是Eric Eban著作的DDD裡,都有出現這個Pattern的身影。Repository Pattern最主要是定義如何切割BLL層跟DAL層之間的相依性,讓BLL層不用依賴於DAL層的實做。並且在有需要更換DAL目標的時候,可以有抽換DAL層的能力。

 

同時學習Repository Pattern,也為架構設計帶入了邊界的概念。在設計架構的時候,可以套用Repository Pattern來做為架構邊界的封裝。將外部的系統、模組、資料庫…等等,隔離在目標架構之外,讓目標架構有更高的內聚以及較少的耦合。

 

 

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

 

 

結構

 

接下來採用一個計算人員數量的服務UserCountService,當作範例的內容。這個UserCountService,計算系統內所有人員的數量、男性人員的數量,提供給外部系統使用。並且實做Repository Pattern來當作BLL層的系統邊界、切割BLL與DAL之間的相依。範例的結構如下:

 

 

 

主要的參與者有:

 

 

User
-系統運作使用的資料物件。
-UserID是這個物件的索引值。

 

 

UserCountService
-使用UserRepository載入User。
-使用載入的User計算各種Count。

 

 

UserRepository
-使用IUserRepositoryProvider載入User。

 

 

IUserRepositoryProvider
-資料物件 User進出系統邊界的介面。
-只提供查詢功能。

 

 

SqlUserRepositoryProvider
-繼承IUserRepositoryProvider。
-實做查詢資料庫產生User物件。

 

 

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

 

 

 

 

實做

 

範列下載
實做說明請參照範例程式內容。
RepositorySample點此下載

 

範列實做

 

首先建立RepositorySample.BLL專案,並且建立一個系統運作使用的資料物件User。

 


namespace RepositorySample.BLL
{
    public class User
    {
        // Constructor 
        public User(Guid userID)
        {
            #region Require

            if (userID == Guid.Empty) throw new ArgumentNullException();

            #endregion
        }


        // Properties
        public Guid UserID { get; set; }

        public string Name { get; set; }

        public bool IsMen { get; set; }
    }
}

 

接著建立提供BLL層資料進出的邊界物件,UserRepository物件及IUserRepositoryProvider介面。(這邊要特別說明的是,也可以直接建立IUserRepository介面當作BLL層資料進出的邊界物件。之所以建立UserRepository物件及IUserRepositoryProvider介面,這種組合式的邊界物件,只是架構設計的個人習慣,筆者比較不喜歡使用介面讓架構內部直接使用。)

 

 


namespace RepositorySample.BLL
{
    public class UserRepository
    {
        // Fields  
        private readonly IUserRepositoryProvider _userRepositoryProvider = null;


        // Constructor 
        public UserRepository(IUserRepositoryProvider userRepositoryProvider)
        {
            #region Require

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

            #endregion
            _userRepositoryProvider = userRepositoryProvider;
        }
        

        // Methods
        public IEnumerable<User> QueryAll()
        {
            return _userRepositoryProvider.QueryAll();
        }
    }
}

 


namespace RepositorySample.BLL
{
    public interface IUserRepositoryProvider
    {
        // Methods
        IEnumerable<User> QueryAll();
    }
}

 

接著建立BLL層最後一個物件,也就是真正提供計算人員數量服務的物件UserCountService。

 

 


namespace RepositorySample.BLL
{
    public class UserCountService
    {
        // Fields  
        private readonly UserRepository _userRepository = null;


        // Constructor 
        public UserCountService(UserRepository userRepository)
        {
            #region Require

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

            #endregion
            _userRepository = userRepository;
        }
        

        // Methods
        public int GetAllCount()
        {
            return _userRepository.QueryAll().Count();
        }

        public int GetMenCount()
        {
            int menCount = 0;
            foreach (User user in _userRepository.QueryAll())
            {
                if (user.IsMen == true)
                {
                    menCount++;
                }
            }
            return menCount;
        }        
    }
}

 

再來建立RepositorySample.DAL專案,以及存取Sql資料庫的DAL物件SqlUserRepositoryProvider。(因為是模擬範例就不實做查詢資料庫,改用直接建立的方式來示意。)

 

 


namespace RepositorySample.DAL
{
    public class SqlUserRepositoryProvider : IUserRepositoryProvider
    {
        // Methods
        public IEnumerable<User> QueryAll()
        {
            User user = null;
            List<User> userList = new List<User>();

            user = new User(Guid.NewGuid());
            user.Name = "Clark";
            user.IsMen = true;
            userList.Add(user);

            user = new User(Guid.NewGuid());
            user.Name = "Jane";
            user.IsMen = false;
            userList.Add(user);

            return userList;
        }
    }
}

 

最後剩下的就是建立一個Console專案,來使用UserCountService。這個Console專案裡,會生成UserCountService,並且列印的人員數量。(因為是模擬範例,UserCountService的生成,採用直接建立的方式來示意。實際專案可以採用各種IoC Framework來做生成注入的動作,避免Console專案與DAL專案有高度的耦合。)

 

 


namespace RepositorySample
{
    class Program
    {
        static void Main(string[] args)
        {
            // UserCountService
            UserCountService userCountService = CreateUserCountService();

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

            // End
            Console.ReadLine();
        }

        static UserCountService CreateUserCountService()
        {
            // UserRepositoryProvider
            SqlUserRepositoryProvider userRepositoryProvider = new SqlUserRepositoryProvider();

            // UserRepository
            UserRepository userRepository = new UserRepository(userRepositoryProvider);

            // UserCountService
            UserCountService userCountService = new UserCountService(userRepository);

            // Return
            return userCountService;
        }
    }
}

 

 

 

情景

 

接著採用幾個情景,來驗證系統的重用性。並且說明如何利用Repository Pattern提供的彈性,來滿足各種的需求。

 

系統更換資料來源

 

賣系統給客戶的時候,客戶的企業環境內不允許裝設SQL Server,必須要採用別種資料存放媒介(例如:CSV檔)。

 

這時可以依照客戶的需求,寫一個新的CsvUserRepositoryProvider物件,用來取代SqlUserRepositoryProvider,系統透過這個新的CsvUserRepositoryProvider物件來查詢CSV檔案內的User資料。透過這樣的方式,系統就可以更換資料來源。範例的程式碼如下:

 

 

首先在RepositorySample.DAL專案內,建立存取CSV檔案的DAL物件CsvUserRepositoryProvider。(因為是模擬範例就不實做剖析檔案內容,改用直接建立的方式來示意。)

 

 


namespace RepositorySample.DAL
{
    public class CsvUserRepositoryProvider : IUserRepositoryProvider
    {
        // Methods
        public IEnumerable<User> QueryAll()
        {
            User user = null;
            List<User> userList = new List<User>();

            user = new User(Guid.NewGuid());
            user.Name = "Jeff";
            user.IsMen = true;
            userList.Add(user);

            return userList;
        }
    }
}

 

接著因為範例沒有採用IoC Framework來做生成注入,所以必須手動修改生成注入這個工作。

 

 


static UserCountService CreateUserCountService()
{
    // UserRepositoryProvider
    CsvUserRepositoryProvider userRepositoryProvider = new CsvUserRepositoryProvider();

    // UserRepository
    UserRepository userRepository = new UserRepository(userRepositoryProvider);

    // UserCountService
    UserCountService userCountService = new UserCountService(userRepository);

    // Return
    return userCountService;
}

 

最後看看運行結果,可以發現計算出來的人員數量,是以CsvUserRepositoryProvider提供的資料來做計算。

 

 

 

 

系統增加資料來源

 

另外在賣系統給客戶過了一陣子之後,客戶增加了一個外部的使用者資料來源。這個新的使用者資料來源,必須要可以跟原本系統內的使用者資料一起使用。

 

這時可以依照客戶的需求,寫一個UnionUserRepositoryProvider物件,用來合併新使用者資料來源與舊使用者資料來源,系統透過這個UnionUserRepositoryProvider物件就可以取得兩種資料來源的User資料。透過這樣的方式,系統就可以加入額外的資料來源。範例的程式碼如下:

 

 

首先在RepositorySample.DAL專案內,建立一個UnionUserRepositoryProvider物件。這個物件可以合併多個IUserRepositoryProvider提供的資料。

 

 


namespace RepositorySample.DAL
{
    public class UnionUserRepositoryProvider : IUserRepositoryProvider
    {
        // Fields  
        private readonly List< IUserRepositoryProvider> _userRepositoryProviderList = null;


        // Constructor 
        public UnionUserRepositoryProvider(List<IUserRepositoryProvider> userRepositoryProviderList)
        {
            #region Require

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

            #endregion
            _userRepositoryProviderList = userRepositoryProviderList;
        }


        // Methods
        public IEnumerable<User> QueryAll()
        {
            List<User> userList = new List<User>();

            foreach (IUserRepositoryProvider userRepositoryProvider in _userRepositoryProviderList)
            {
                foreach (User user in userRepositoryProvider.QueryAll())
                {
                    userList.Add(user);
                }
            }

            return userList;
        }
    }
}

 

另外因為範例沒有採用IoC Framework來做生成注入,所以必須手動修改生成注入這個工作。

 

 


static UserCountService CreateUserCountService()
{
    // UserRepositoryProvider  
    List<IUserRepositoryProvider> userRepositoryProviderList = new List<IUserRepositoryProvider>();
    userRepositoryProviderList.Add(new CsvUserRepositoryProvider());
    userRepositoryProviderList.Add(new SqlUserRepositoryProvider());
    UnionUserRepositoryProvider userRepositoryProvider = new UnionUserRepositoryProvider(userRepositoryProviderList);
            
    // UserRepository
    UserRepository userRepository = new UserRepository(userRepositoryProvider);

    // UserCountService
    UserCountService userCountService = new UserCountService(userRepository);

    // Return
    return userCountService;
}

 

最後看看運行結果,可以發現計算出來的人員數量,是以合併CsvUserRepositoryProvider、SqlUserRepositoryProvider提供的資料來做計算。

 

 

 

 

系統加入快取功能

 

接著客戶使用系統一陣子之後,發現資料較多的時候,系統就會變慢。經過各種效能工具的檢查,發現是剖析CSV檔是整個系統效能的瓶頸。

 

這時可以依照客戶的需求,寫一個CacheUserRepositoryProvider物件,用來快取CsvUserRepositoryProvider提供的資料,系統只有在第一次取資料的時候,才會去剖析CSV檔案。透過這樣的方式,系統就可以加入快取資料來源的功能,提高系統的效能。範例的程式碼如下:

 

 


namespace RepositorySample.DAL
{
    public class CacheUserRepositoryProvider : IUserRepositoryProvider
    {
        // Fields  
        private readonly IUserRepositoryProvider _userRepositoryProvider = null;

        private IEnumerable<User> _cache = null;


        // Constructor 
        public CacheUserRepositoryProvider(IUserRepositoryProvider userRepositoryProvider)
        {
            #region Require

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

            #endregion
            _userRepositoryProvider = userRepositoryProvider;
        }
        

        // Methods
        public IEnumerable<User> QueryAll()
        {
            if (_cache == null)
            {
                _cache = _userRepositoryProvider.QueryAll();
            }
            return _cache;
        }
    }
}

 

當然啦,因為範例沒有採用IoC Framework來做生成注入,所以手動修改生成注入這個工作還是要做。

 

 


static UserCountService CreateUserCountService()
{
    // UserRepositoryProvider
    CsvUserRepositoryProvider csvUserRepositoryProvider = new CsvUserRepositoryProvider();
    CacheUserRepositoryProvider userRepositoryProvider = new CacheUserRepositoryProvider(csvUserRepositoryProvider);

    // UserRepository
    UserRepository userRepository = new UserRepository(userRepositoryProvider);

    // UserCountService
    UserCountService userCountService = new UserCountService(userRepository);

    // Return
    return userCountService;
}

 

後記

 

整個Repository Pattern實做看來下,眼尖的開發人員會發現,它跟IoC有異曲同工的意味。而Repository Pattern跟IoC的差異,主要是取決於設計時的顆粒度。IoC是從程式設計面,去看待切割相依性這件事情。而Repository Pattern則是從架構設計面,去看待切割相依性這件事情。

 

在架構設計裡加入Repository Pattern的設計,可以為系統架構提供了抽換DAL層的彈性。一個系統的成敗,除了最基本的滿足客戶需求之外,這些額外的非功能需求也是很重要的一環。動手的時候多想一點點,未來維護的開發人員,會感激你的。

 

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