摘要:[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層的彈性。一個系統的成敗,除了最基本的滿足客戶需求之外,這些額外的非功能需求也是很重要的一環。動手的時候多想一點點,未來維護的開發人員,會感激你的。
能以更簡潔的文字與程式碼,傳達出程式設計背後的精神。
真正做到「以形寫神」的境界。