軟件開發就像是一個江湖,而設計模式就是一本高深的秘籍每讀一次、用一次、想一次都能得到新的領悟,讓我們的設計技能有所提高。開始時我們可能會“為了模式而模式”,讓代碼變得亂78糟甚至難以讓人理解,但隨着設計能力的提高與模式的運用和理解,慢慢地我們可能會忘掉模式隨心所欲,此時再讀讀代碼或者你已經發現自己的工程已融合模式之美—"模式為設計而生,設計為需求而活"。在開篇突然想分享一下這10幾年用模式的一點小小的領悟。
IRepository 與 IUnitOfWork
在2011年我曾在Codeproject 發表過題為 "The Repository Pattern with EF Code First & Dependency Injection in ASP.NET MVC3" 的文章,當時講述的是Repository模式和Ioc如何結合EF在ASP.NET MVC3上應用。歷時3年多對Repository的使用,最近又有新的感悟。 Repository 是這幾年我用得最多的模式之一,而且也是我認為最常用和最容易掌握的一種模式,我使用Repository的目的有二:
- 將數據的實例化解耦,通過Ioc或是工廠方法構造數據對象而不是用 "new"
- 將數據的邏輯行為(CURD)解耦,這樣會便於我更換任何形式的數據庫,無論底層數據庫使用的是MSSQL還是MongoDB我都可以采用相同的訪問邏輯。
以下是我期望在代碼中看到的效果
public class PostController:Contrller { private IRepository repository; public MyController(IRepository repository){ this.repository=repository; } public Action Get(int id) { return View(repository.Find(id)); } [HttpPost] public Action Create(Post post) { repository.Add(post);
repository.Submit(); return this.Get(post.ID); }
... }
這里使用了構造注入,由MVC向Controller注入Repository的實例,這樣我就不需要在使Repository的時候去創建它了。(這種例子你Google會發現很多,包括在asp.net上也不少)
單一地使用Repository很容易理解,但運用在項目中的實例情況可以這樣嗎?大多數答案是否定的,如果當前的Controller控制的不單純是一個實體,而是多個實體時,如果使用純Repository的話代碼會變糟:
public class PostController:Contrller { private IRepository posts; private IRepository blogs; private IRepository owners; public MyController(IRepository blogs,IRepository posts,IRepository owners){ this.posts=posts; this.blogs=blogs; this.owners=owners; } public Action Get(int id) { var post=posts.Find(id) var blog=blogs.Find(post.BlogID); var owner=posts.Find(post.Owner); ... return View(new { Post=post, Blog=blog, Owner=owner }); } ... }
一看上去似乎沒什么大問題,只是在構造時多了兩個Repository的注入,但是我們的目只有一個Controller嗎? 而每個實體都需要去實現一個Repository? 這樣的寫法在項目中散播會怎么樣呢?
最后你會得到一大堆雞肋式的"Repository",他們的相似度非常大。或是你使用一個Repository實現處理所有的實體,但你需要在構造時進行配置 (注:構造注入並不代表不構造,而是將構造代碼放在了一個統一的地方),更糟的情況是,如果使用的是繼承於IRepository的子接口,那么Controller就會與IRepository的耦合度加大。
很明顯 Repository 不是一種獨立的模式,它自身和其它模式有很強的相關度,如果只是拿它來獨立使用局限性會很大。我們需要其它的接口去統一“管理”Repository和避免在Controller內顯式的出現Repository的類型聲明。將上面的代碼改一下:
public class PostController:Contrller { private IUnitOfWorks works; public PostController(IUnitOfWorks works){ this.works=works; } public Action Get(int id) { var post=works.Posts.Find(id) var blog=works.Blogs.Find(post.BlogID); var owner=works.Posts.Find(post.Owner); ... return View(new { Post=post, Blog=blog, Owner=owner }); } [HttpPost] public Action Create(Post post) { works.Add(post); ... } }
這里就引入了 UnitOfWorks 模式,將對所有的Repository的耦合消除(把所有的Repository變量的聲明去掉了),
IRepository與IUnitOfWorks兩個模式是最佳組合,我們可以通過對UnitOfWorks一個類進行IoC處理,而調用方代碼則集中從IUnitOfWorks對象獲取所需的Repository,接下來看看他們的定義吧
IRepository 接口
/// <summary> /// 定義通用的Repository接口 /// </summary> /// <typeparam name="T"></typeparam> public interface IRepository<T>: IDisposable where T : class { /// <summary> /// 獲取所有的實體對象 /// </summary> /// <returns></returns> IQueryable<T> All(); /// <summary> /// 通過Lamda表達式過濾符合條件的實體對象 /// </summary> IQueryable<T> Filter(Expression<Func<T, bool>> predicate); /// <summary> /// Gets the object(s) is exists in database by specified filter. /// </summary> bool Contains(Expression<Func<T, bool>> predicate); /// <summary> /// 獲取實體總數 /// </summary> int Count(); int Count(Expression<Func<T, bool>> predicate); /// <summary> /// 通過鍵值查找並返回單個實體 /// </summary> T Find(params object[] keys); /// <summary> /// 通過表達式查找復合條件的單個實體 /// </summary> /// <param name="predicate"></param> T Find(Expression<Func<T, bool>> predicate); /// <summary> /// 創建實體對象 /// </summary> T Create(T t); /// <summary> /// 刪除實體對象 /// </summary> void Delete(T t); /// <summary> /// 刪除符合條件的多個實體對象 /// </summary> int Delete(Expression<Func<T, bool>> predicate); /// <summary> /// Update object changes and save to database. /// </summary> /// <param name="t">Specified the object to save.</param> T Update(T t); /// <summary> /// Clear all data items. /// </summary> /// <returns>Total clear item count</returns> void Clear(); /// <summary> /// Save all changes. /// </summary> /// <returns></returns> int Submit(); }
IUnitOfWorks 接口
public interface IUnitOfWorks { IQueryable<T> Where<T>(Expression<Func<T, bool>> predicate) where T : class; IQueryable<T> All<T>() where T : class; int Count<T>() where T : class; int Count<T>(Expression<Func<T, bool>> predicate) where T : class; T Find<T>(object id) where T : class; T Find<T>(Expression<Func<T, bool>> predicate) where T : class; T Add<T>(T t) where T : class; IEnumerable<T> Add<T>(IEnumerable<T> items) where T : class; void Update<T>(T t) where T : class; void Delete<T>(T t) where T : class; void Delete<T>(Expression<Func<T, bool>> predicate) where T : class; void Clear<T>() where T : class;
int SaveChanges(); void Config(IConfiguration settings); }
仔細一看你會發現IRepository和IUnitOfWorks的定義非常相似,在使用的角度是UnitOfWorks就是所有實例化的Repository的一個統一“包裝”, 這是我在發表 "The Repository Pattern with EF Code First & Dependency Injection in ASP.NET MVC3" 后對IUnitOfWorks在實用上的一種擴展。以前的方式是直接在UnitOfWorks的屬性的中暴露一個Repository實例給外部使用,但這樣做的話會降低IUnitOfWorks的通用性。所以我讓IUnitOfWorks使用起來更像是一個IRepository.看一段代碼的比較:
//之前的做法,Posts是一個IRepository的實現,是不是很像EF var post=works.Posts.Add(new Post()); //C works.Posts.Update(post);//U var post=works.Posts.Get(id); //R works.Posts.Delete(post);//D //優化后的IUnitOfWorks var post=works.Add(new Post());//C works.Update(post);//U var post=works.Get(id); //R works.Delete(post);//D
如果將IRepository通過屬性的方式暴露給調用方,IUnitOfWorks的擴展性就會下降,而且會令IUnitOfWorks的實現類與調用方建立很緊密的耦合。我對IUnitOfWorks優化后以泛型決定使用哪一個Repository,這樣可以將IUnitOfWorks與調用方進行解耦。所有的實體通過一個通用Repository實現,這樣可以避免為每一個實體寫一個Repository。而對於具有特殊處理邏輯的Repository才通過屬性暴露給調用方。
IRepository 與 IUnitOfWorks的實現
在這里我會先實現一套使用EF訪問數據庫的通用 Repository 和 UnitOfWorks
EntityRepository
public class EntityRepository<TContext, TObject> : IRepository<TObject> where TContext : DbContext where TObject : class { protected TContext context; protected DbSet<TObject> dbSet; protected bool IsOwnContext = false; /// <summary> /// Gets the data context object. /// </summary> protected virtual TContext Context { get { return context; } } /// <summary> /// Gets the current DbSet object. /// </summary> protected virtual DbSet<TObject> DbSet { get { return dbSet; } } /// <summary> /// Dispose the class. /// </summary> public void Dispose() { if ((IsOwnContext) && (Context != null)) Context.Dispose(); GC.SuppressFinalize(this); } /// <summary> /// Get all objects. /// </summary> /// <returns></returns> public virtual IQueryable<TObject> All() { return DbSet.AsQueryable(); } /// <summary> /// Gets objects by specified predicate. /// </summary> /// <param name="predicate">The predicate object.</param> /// <returns>return an object collection result.</returns> public virtual IQueryable<TObject> Filter(Expression<Func<TObject, bool>> predicate) { return DbSet.Where(predicate).AsQueryable<TObject>(); } public bool Contains(Expression<Func<TObject, bool>> predicate) { return DbSet.Count(predicate) > 0; } /// <summary> /// Find object by keys. /// </summary> /// <param name="keys"></param> /// <returns></returns> public virtual TObject Find(params object[] keys) { return DbSet.Find(keys); } public virtual TObject Find(Expression<Func<TObject, bool>> predicate) { return DbSet.FirstOrDefault(predicate); } public virtual TObject Create(TObject TObject) { var newEntry = DbSet.Add(TObject); if (IsOwnContext) Context.SaveChanges(); return newEntry; } public virtual void Delete(TObject TObject) { var entry = Context.Entry(TObject); DbSet.Remove(TObject); if (IsOwnContext) Context.SaveChanges(); } public virtual TObject Update(TObject TObject) { var entry = Context.Entry(TObject); DbSet.Attach(TObject); entry.State = EntityState.Modified; if (IsOwnContext) Context.SaveChanges(); return TObject; } public virtual int Delete(Expression<Func<TObject, bool>> predicate) { var objects = DbSet.Where(predicate).ToList(); foreach (var obj in objects) DbSet.Remove(obj); if (IsOwnContext) return Context.SaveChanges(); return objects.Count(); } public virtual int Count() { return DbSet.Count(); } public virtual int Count(Expression<Func<TObject, bool>> predicate) { return DbSet.Count(predicate); } public int Submit() { return Context.SaveChanges(); } public virtual void Clear() { } }
UnitOfWorks
public class UnitOfWorks<TDBContext> : IUnitOfWorks where TDBContext :DbContext { protected TDBContext dbContext; public UnitOfWorks<TDBContext>(TDBContext context) { dbContext=context; } //構造通用的Repository private IDictionary<Type,object> repositoryTable = new Dictionary<Type,object>();
//注冊其它的Repository public void Register<T>(IRepository<T> repository) { var key=typeof(T); if (repositoryTable.ContainsKey(key)) repositoryTable[key].Add(repository); } private IRepository<T> GetRepository<T>() where T:class { IRepository<T> repository = null; var key=typeof(T); if (repositoryTable.ContainsKey(key)) repository = (IRepository<T>)repositoryTable[key]; else { repository = GenericRepository<T>(); repositoryTable.Add(key, repository); } return repository; } protected virtual IRepository<T> GenericRepository<T>() where T : class { return new EntityRepository<T>(dbContext); } public T Find<T>(object id) where T : class { return GetRepository<T>().Find(id); } public T Add<T>(T t) where T : class { return GetRepository<T>().Create(t); } public IEnumerable<T> Add<T>(IEnumerable<T> items) where T : class { var list = new List<T>(); foreach (var item in items) list.Add(Add(item)); return list; } public void Update<T>(T t) where T : class { GetRepository<T>().Update(t); } public void Delete<T>(T t) where T : class { GetRepository<T>().Delete(t); } public void Delete<T>(Expression<Func<T, bool>> predicate) where T : class { GetRepository<T>().Delete(predicate); } public int SaveChanges(bool validateOnSave = true) { if (!validateOnSave) dbContext.Configuration.ValidateOnSaveEnabled = false; return dbContext.SaveChanges(); } public void Dispose() { if (dbContext != null) dbContext.Dispose(); GC.SuppressFinalize(this); } public System.Linq.IQueryable<T> Where<T>(System.Linq.Expressions.Expression<Func<T, bool>> predicate) where T:class { return GetRepository<T>().Filter(predicate); } public T Find<T>(System.Linq.Expressions.Expression<Func<T, bool>> predicate) where T : class { return GetRepository<T>().Find(predicate); } public System.Linq.IQueryable<T> All<T>() where T : class { return GetRepository<T>().All(); } public int Count<T>() where T : class { return GetRepository<T>().Count(); } public int Count<T>(System.Linq.Expressions.Expression<Func<T, bool>> predicate) where T : class { return GetRepository<T>().Count(predicate); } public void Config(IConfiguration settings) { var configuration=settings as DbConfiguration ; if (configuration != null) { this.dbContext.Configuration.AutoDetectChangesEnabled = configuration.AutoDetectChangesEnabled; this.dbContext.Configuration.LazyLoadingEnabled = configuration.LazyLoadingEnabled; this.dbContext.Configuration.ProxyCreationEnabled = configuration.ProxyCreationEnabled; this.dbContext.Configuration.ValidateOnSaveEnabled = configuration.ValidateOnSaveEnabled; } } public void Clear<T>() where T : class { GetRepository<T>().Clear(); } int IUnitOfWorks.SaveChanges() { return this.SaveChanges(); } }
接下來看看如何使用這套基於EF的實現,首先是對Model的定義
public class Category { [Key] public int ID { get; set; } public virtual string Name { get; set; } public virtual string Title { get; set; } public virtual ICollection<Product> Products { get; set; } } public class Product { [Key] public int ID { get; set; } public int CategoryID { get; set; } [ForeignKey("CategoryID")] public virtual Category Category {get;set;} public string Name { get; set; } public string Title { get; set; } public string Description{get;set;} public decimal Price { get; set; } } public class DB : DbContext { public DB() : base("DemoDB") { } public DbSet<Category> Categories { get; set; } public DbSet<Product> Products { get; set; } }
調用方:使用UnitOfWorks和Repository
var works=new UnitOfWorks(new DB()); var pc=works.Add(new Category() { Name="PC", Title="電腦" }); workds.Add(new Product(){ Category=pc, Name="iMac", Title="iMac" Price=9980 }) works.SaveChanges();
注:如果需要使用IoC方式構造UnitOfWorks 可參考我在 "The Repository Pattern with EF Code First & Dependency Injection in ASP.NET MVC3" 一文中提及如何在MVC內通過Unity 實現DI.
小結
以上述例子為例,如果我們想將Category存儲於文本文件而不想改動調用方的代碼。我們可以實現一個 FileBaseCategoryRepository,然后在UnitOfWorks在構造后調用Register方法將默認的Category Repository替換掉,同理,這就可以建立不同的Repository去配置UnitOfWork
var works=new UnitOfWorks(new DB()); works.Register(new FileBaseCategoryRepository()); var pc=works.Add(new Category() { Name="PC", Title="電腦" }); workds.Add(new Product(){ Category=pc, Name="iMac", Title="iMac" Price=9980 }) works.SaveChanges();
使用IUnitOfWorks+IRepository模式你就可以靈活地配置你的數據訪問方式,可以通過Repository極大地提通實體存儲邏輯代碼的重用性。
相關參考
- Repository Pattern - by Martin fowler
- The Repository Pattern with EF Code First & Dependency Injection in ASP.NET MVC3 - by Ray