概述
上一篇我們算是粗略的介紹了一下DDD,我們提到了實體、值類型和領域服務,也稍微講到了DDD中的分層結構。但這只能算是一個很簡單的介紹,并且我們在上篇的末尾還留下了一些問題,其中大家討論比較多的,也是我本人之前有一些疑問的地方就是Repository。我之前覺得IRepository和三層里面的IDAL很像,為什么要整出這么個東西來;有人說用EF的話就不需要Repository了;IRepository是雞肋等等。 我覺得這些問題都很好,我自己也覺得有問題,帶著這些問題我們就來看一看Repository在DDD中到底起著一個什么樣的角色,它為什么存在?有一句真理不是說“存在即合理”么? 那我們就要找到它存在的理由,去更好的理解它,或者說我們能不能針對不同的需求去改造它呢?注:本文討論的是Repository在DDD中的應用,與EF該不該用Repoistory不是同一個話題。
領域驅動系列
初探領域驅動設計(1)為復雜業務而生
初探領域驅動設計(2)Repository在DDD中的應用
初探領域驅動設計(3)寫好單元測試
......
目錄
EF與Repository
在上一篇《初探領域驅動設計(1)為復雜業務而生》中,我們已經實現了一個用戶注冊的例子,但是并不完整。我們還沒有具體的實現Repository,即使是在測試的時候我們使用的也是一個Mock。那么今天,我們就來實現一個EntityFramework的Repository。有人說EF沒有必要套一個Repository,我是同意的。但是不同的場景,不同的使用方法,我們下面再具體講。我們在上一篇中已經提到了IRepository的接口定義,下面是我們的簡單實現:
// EFRepository.cs

1 namespace RepositoryAndEf.Data 2 { 3 public class EfRepository<T> : IRepository<T> where T : BaseEntity 4 { 5 private DbContext _context; 6 7 public EfRepository(DbContext context) 8 { 9 if (context == null) 10 { 11 throw new ArgumentNullException("context"); 12 } 13 _context = context; 14 } 15 16 public T GetById(Guid id) 17 { 18 return _context.Set<T>().Find(id); 19 } 20 21 public bool Insert(T entity) 22 { 23 _context.Set<T>().Add(entity); 24 _context.SaveChanges(); 25 return true; 26 } 27 public bool Update(T entity) 28 { 29 _context.Set<T>().Attach(entity); 30 _context.Entry<T>(entity).State = EntityState.Modified; 31 _context.SaveChanges(); 32 return true; 33 } 34 35 public bool Delete(T entity) 36 { 37 _context.Set<T>().Remove(entity); 38 _context.SaveChanges(); 39 return true; 40 } 41 42 43 public IEnumerable<T> Get(Expression<Func<T, bool>> predicate) 44 { 45 return _context.Set<T>().Where(predicate).ToList(); 46 } 47 } 48 }
// 應用層UserService.cs

1 public class UserService : IUserService 2 { 3 private IRepository<User> _userRepository; 4 5 public UserService(IRepository<User> userRepository) 6 { 7 _userRepository = userRepository; 8 } 9 10 public User Register(string email, string name, string password) 11 { 12 var domainUserService = new Domain.UserService(_userRepository); 13 var user = domainUserService.Register(email, name, password); 14 return user; 15 } 16 }
// 領域層UserService.cs

1 namespace RepositoryAndEf.Domain 2 { 3 public class UserService 4 { 5 private IRepository<User> _userRepository; 6 7 public UserService(IRepository<User> userRepsoitory) 8 { 9 _userRepository = userRepsoitory; 10 } 11 12 public virtual User Register(string email, string name, string password) 13 { 14 if (_userRepository.Get().Any(u => u.Email == email)) 15 { 16 throw new ArgumentException("The email is already taken"); 17 } 18 19 var user = new User 20 { 21 Id = Guid.NewGuid(), 22 Email = email, 23 Name = name, 24 Password = password 25 }; 26 27 user.CreateShoppingCart(); 28 _userRepository.Insert(user); 29 return user; 30 } 31 } 32 }
上面領域層UserService中的代碼和我們上一篇中的代碼是一樣的,netfocus兄提出來一個問題“是不是把user對象加入到repository中就算完成注冊了?” 現在看來,如果代碼這樣寫,好像就已經完成了注冊的功能。 但是如果真這樣寫,我又覺得問題更大,也就是為什么我會在上篇的未必留下那個問題,“Domain -> Repository -> Database” 和“BLL -> Dal -> Database” 有區別么?撇開這個問題不說,看看我們上面的EfRepository有沒有什么問題? 好用么?現在好像沒有辦法使用事務啊!帶著這個問題我們來看看Unit Of Work能怎么幫我們。
Unit Of Work 與 Repository
我們EfRepository的實現中,每一次Insert/Update/Delete操作被執行之后,變更就會立即同步到數據庫中去。第一,我們沒有為多個操作添加一個事務的能力;第二,這會為我們帶來性能上的損失。而Unit Of Work模式正好解決了我們的問題,下面是Martin Fowler 對于該模式的解釋:
“A Unit of Work keep track of everything you do during a business transaction that can affect the database. When you’re done, it figures out everything that need to be done to alter the database as a result of your work.”
Unit of Work負責跟蹤所有業務事務過程中數據庫的變更。當事務完成之后,它找出需要處理的變更,并更新數據庫。
正如我們大家一直討論的那樣,在EF中,DBContext它本身就已經是一個Unit Of Work的模式,因為上面說的功能它都有。那我們有必要自己再給它包上一層嗎?我的答案是肯定的,這個和我們為Repository建立接口是一樣的,EF中的IDbSet就是一個Repository模式,但是他們都是EF里面的東西,如果哪天我們換成NHibernate了,我們不可能為了這一個接口和基類把EF這個dll也加進來是么? 我們要做的并不多,因為DbContext.SaveChanges它本身就是有事務的,所以我們只需要創建一個帶有SaveChanges的接口就可以了。
// IUnitOfWork.cs

1 namespace RepositoryAndEf.Core.Data 2 { 3 public interface IUnitOfWork : IDisposable 4 { 5 int SaveChanges(); 6 } 7 }
接著就是讓我們的Context,繼承DbContex和我們上面的接口。

1 namespace RepositoryAndEf.Data 2 { 3 public class RepositoryAndEfContext : DbContext, IUnitOfWork 4 { 5 public RepositoryAndEfContext() { } 6 7 public RepositoryAndEfContext(string nameOrConnectionString) 8 : base(nameOrConnectionString) 9 { 10 Configuration.LazyLoadingEnabled = true; 11 } 12 13 protected override void OnModelCreating(DbModelBuilder modelBuilder) 14 { 15 var typesToRegister = Assembly.GetExecutingAssembly().GetTypes() 16 .Where(type => !String.IsNullOrEmpty(type.Namespace)) 17 .Where(type => type.BaseType != null 18 && type.BaseType.IsGenericType 19 && type.BaseType.GetGenericTypeDefinition() == typeof(EntityTypeConfiguration<>)); 20 21 foreach (var type in typesToRegister) 22 { 23 dynamic configurationInstance = Activator.CreateInstance(type); 24 modelBuilder.Configurations.Add(configurationInstance); 25 } 26 //...or do it manually below. For example, 27 //modelBuilder.Configurations.Add(new LanguageMap()); 28 29 base.OnModelCreating(modelBuilder); 30 } 31 } 32 }
哦,對了,別忘了把Repository里面的SaveChanges方法去掉。

1 namespace RepositoryAndEf.Data 2 { 3 public class EfRepository<T> : IRepository<T> where T : BaseEntity 4 { 5 private DbContext _context; 6 7 public EfRepository(IUnitOfWork uow) 8 { 9 if (uow == null) 10 { 11 throw new ArgumentNullException("uow"); 12 } 13 _context = uow as DbContext; 14 } 15 16 public T GetById(Guid id) 17 { 18 return _context.Set<T>().Find(id); 19 } 20 21 public bool Insert(T entity) 22 { 23 _context.Set<T>().Add(entity); 24 return true; 25 } 26 27 public bool Update(T entity) 28 { 29 _context.Set<T>().Attach(entity); 30 _context.Entry<T>(entity).State = EntityState.Modified; 31 return true; 32 } 33 34 public bool Delete(T entity) 35 { 36 _context.Set<T>().Remove(entity); 37 return true; 38 } 39 40 41 public IEnumerable<T> Get(Expression<Func<T, bool>> predicate) 42 { 43 return _context.Set<T>().Where(predicate).ToList(); 44 } 45 } 46 }
那么我們應用層的UserService就可以這樣寫了。

namespace RepositoryAndEf.Service { public class UserService : IUserService { private IRepository<User> _userRepository; private IUnitOfWork _uow = EngineContext.Current.Resolve<IUnitOfWork>(); public UserService(IRepository<User> userRepository) { _userRepository = userRepository; } public User Register(string email, string name, string password) { var domainUserService = new Domain.UserService(_userRepository); var user = domainUserService.Register(email, name, password); // 在調用SaveChnages()之前,做其它的更新操作 // 它們會一起在同一個事務中執行。 _uow.SaveChanges(); return user; } } }
如果光看這段代碼有沒有覺得很奇怪?沒有任何對_userRepository的操作,就做了SaveChanges,因為我們在領域服務里面就已經把新創建的用戶實體放到那個userRepository中去了。我想這個問題@田園的蟋蟀糾結過很久:) ,也就是領域服務那里面持有repository的引用,它可以自己將要更新的實體添加到repository中,但是如果對于一些不涉及到領域服務的操作,那這一點就需要在應用層來做了,比如添加商品到購物車的操作。
// 應用層ShoppingCartService.cs

1 namespace RepositoryAndEf.Service 2 { 3 public class ShoppingCartService : IShoppingCartService 4 { 5 private IRepository<ShoppingCart> _shoppingCartRepository; 6 private IRepository<Product> _productRepository; 7 private IUnitOfWork _uow; 8 9 public ShoppingCartService(IUnitOfWork uow, 10 IRepository<ShoppingCart> shoppingCartRepository, 11 IRepository<Product> productRepository) 12 { 13 _uow = uow; 14 _shoppingCartRepository = shoppingCartRepository; 15 _productRepository = productRepository; 16 } 17 18 public ShoppingCart AddToCart(Guid cartId, 19 Guid productId, 20 int quantity) 21 { 22 var cart = _shoppingCartRepository.GetById(cartId); 23 var product = _productRepository.GetById(productId); 24 cart.AddItem(product, quantity); 25 26 _shoppingCartRepository.Update(cart); 27 _uow.SaveChanges(); 28 return cart; 29 } 30 } 31 }
這就是屬于職責定義不明確的問題,特別是上面注冊用戶的例子。應用層也有_userRepository,并且領域服務還給我返回了一個user的實體,那我是把它加到這個_userRepository中呢還是不加好呢?
我覺得我們應該有這樣的一個定義,在領域層那里不使用repository的更新類操作(即Insert/Update/Delete),只使用查詢類操作即(GetById,或者是Get)。把所有的更新類操作都放到應用層,這樣由應用層去決定什么時候把實體更新到repository,以及什么時候去提交到數據庫中。那我們就徹底與持久層,甚至領域實體生命期管理的功能撇開有關系了,從此用更OO的方式專注于業務。
后面我們要做的更改就是把_userRepository.Insert(user)從我們User的領域服務中移除掉,并且在應用層的Register方法中加入這句話。 我想到這里,也算是回答了我自己的問題: IRepository正如它的名字一樣,它就像一個容器,允許我們把東西放進去或者取出來,它離真正的數據庫還有一步之遙,并且通過Unit Of Work,把對事務以及持久化的控制都交到了外面。而不是像DAL那樣直接就反映到數據庫中去了。除此之外呢?IRepository解除了領域層對基礎設施層的依懶,這個也是大家經常提到了Repository的優點之一。但是未必這一點一定非得需要IRepository,把IDAL接口移個位置同樣也可以實現,不信您看看洋蔥架構。
洋蔥架構與IRepository
洋蔥架構很早就有,只不過08年的時候Jeffery給它取了個名字,讓它成為了一個模式。說起來好像很高大上,但是希望大家不要被這些名字所迷惑,所正如Jeffery所說,在這種設計有了一個名字之后,更方便大家去討論和傳播以及使用這種模式。 并且洋蔥架構也是一種多層架構,所以會出現“傳統” 的多層架構 和“現代”的多層架構。 我更是認為,所謂的洋蔥架構只是作出了一點點思想層面上的轉變,僅此而已。 究竟是哪一點思想上的轉變,可以讓它成為一種模式呢? 依懶關系!
Jeffery說在傳統的多層架構中,上層對下層有著較強的依懶關系,UI沒了BLL就沒法工作,BLL少了DAL也無法正常運行。當然他說這句話的時候是08年,并且他的確是在前面加了“傳統” 兩個字。 我們很難找到到底是什么時候,這種傳統的多層架構演變成了“現代” 的多層架構,但是我們能知道的是在08年7月以后我們對于多層架構又有了一個新的名詞。即便如此,它的轉變卻是非常簡單的 —— 也就是把IDAL接口從DAL層分離出去。
如果把IDAL接口定義在DataAccess層,第一是造成了BLL對DataAccess的依懶;第二是造成了IDAL的責任不明確。如果說小A負責開發BLL,小C負責開發DAL,他們是不是需要協調該怎么樣去定義IDAL接口? 是DAL為BLL服務,還是BLL的最終目地是把自己移交給DAL? 在最開始的時候,大家對IDAL的定義是為了支持不同的訪問層設計,大家想的都是現在我們用SQL,將來有可能會有MySql。所以IDAL放在哪里也就無所謂了,為了方便就直接和實現一起放在DAL吧。
把IDAL接口從DAL移出去之后會發生什么 ?
在把IDAL接口移到BLL層之后,箭頭的方向就變了。現在一切都是以BLL為中心,BLL也不需要依懶于任何其它層了,作為獨立的一塊,我們可以更容易的進行單元測試,重構等。另外也明確了IDAL是為BLL服務的,也就是解決了我們上面提到的第二個問題。
這個一個很簡單的轉變就是洋蔥架構的主要思想,如果你還不能很好的領悟洋蔥架構和傳統多層架構之間的區別,希望下面這張圖能用最直接,最簡單的方式告訴你。
傳統多層架構與現代(洋蔥架構)多層架構的區別
你要是愿意,把IDAL直接放到Bll里面也是可以的。當Jeffery給這種架構起名叫“洋蔥架構”再往前推4年,DDD問世的時候已經包含了這種思想。IRepository屬于領域層而非基礎架構層中的數據訪問模塊,就直接避免了領域層對基礎設施層的依懶,或者說不定這種思想也是從DDD引申出來的,所以你會發現很多人現在依然用DAL。但是并沒有什么問題,因為在這種新的多層架構下,擴展性和可維護性同樣也可以被保持的很好。
重新定義IRepository
現在,我們再回過頭去看Repository。它的兩大職責:
- 對領域實體的生命周期進行管理(從數據庫重建,以及持久化到數據庫) ——被推遲到了應用層
- 解除領域層對基礎設施的依懶
在第一點生效后,所有更新類的操作都推遲到應用層去執行。那IRepository中的那些更新類方法放在領域層是不是就多余了呢? 畢竟我們現在只需要用到查詢的功能。我們可以單獨建一個IQuery的接口給領域層使用。
// IQuery.cs

1 namespace RepositoryAndEf.Core.Data 2 { 3 public interface IQuery<T> 4 { 5 T GetById(Guid id); 6 IQueryable<T> Table { get; } 7 } 8 }
// IRepository.cs

1 namespace RepositoryAndEf.Core.Data 2 { 3 public partial interface IRepository<T>: 4 IQuery<T> where T : BaseEntity 5 { 6 bool Insert(T entity); 7 bool Update(T entity); 8 bool Delete(T entity); 9 } 10 }
我們直接讓IRepository繼承了IQuery,IQuery就相當于IRepository的一個功能子集,只提供讀的功能。 而在EfRepository中,我們只要暴露DbSet<T>.AsQueryAble()就可以了。
// EfRepository IQuery的實體部分

1 public T GetById(Guid id) 2 { 3 return _context.Set<T>().Find(id); 4 } 5 6 public IQueryable<T> Table 7 { 8 get 9 { 10 return _context.Set<T>().AsQueryable(); 11 } 12 }
// 領域層 UserService.cs

1 namespace RepositoryAndEf.Domain 2 { 3 public class UserService 4 { 5 private IQuery<User> _userQuery; 6 7 public UserService(IQuery<User> userQuery) 8 { 9 _userQuery = userQuery; 10 } 11 12 public virtual User Register(string email, string name, string password) 13 { 14 if (_userQuery.Table.Any(u => u.Email == email)) 15 { 16 throw new ArgumentException("The email is already taken"); 17 } 18 19 var user = new User 20 { 21 Id = Guid.NewGuid(), 22 Email = email, 23 Name = name, 24 Password = password 25 }; 26 27 user.CreateShoppingCart(); 28 return user; 29 } 30 } 31 }
// 客戶端調用應用層Service代碼

1 var uow = new RepositoryAndEfContext("ConnStr"); 2 var userRepository = new EfRepository<Domain.User>(uow); 3 var userService = new UserService(uow, userRepository); 4 var newUser = userService.Register( 5 "hellojesseliu@outlook.com", 6 "Jesse Liu", 7 "jesseliu");
現在,恐怕你再想在領域模型里面去使用Repository的更新類操作也不行了吧。 Table作為IQueryable返回,那我們想怎么查就隨意了。因為是IQueryable,所以也是只會返回我們所查詢的內容,和直接用EF查詢是一個道理。下面是我們_userQuery.Table.Any()所生成的SQL語句。

1 exec sp_executesql N'SELECT 2 CASE WHEN ( EXISTS (SELECT 3 1 AS [C1] 4 FROM [dbo].[Users] AS [Extent1] 5 WHERE ([Extent1].[Email] = @p__linq__0) OR (([Extent1].[Email] IS NULL) AND (@p__linq__0 IS NULL)) 6 )) THEN cast(1 as bit) ELSE cast(0 as bit) END AS [C1] 7 FROM ( SELECT 1 AS X ) AS [SingleRowTable1]',N'@p__linq__0 nvarchar(4000)',@p__linq__0=N'hellojesseliu@outlook.com'
可有可無的Repository
我們把IRepository移出領域層之后,再加上我們對洋蔥架構的理解。我們就可以知道Repository在應用層已經可以被替換成別的東西,IDAL也可以啊:)。當然有人也許會建議直接拿EF來用多好,其實我不建議這樣去做,考慮到以后把EF換掉的可能性。并且我們加這樣一個接口真的不會礙著我們什么事。如果有人覺得在讀取數據的時候加一個Repository在中間,少掉了很多EF提供的功能,覺得很不爽,倒是可以試試像我們的IQuery接口一樣直接對DbSet來查詢。我們甚至可以學習CQRS架構,將“讀”的服務完全分離開,我們就可以單獨針對“讀”來獨立設計。
但是Repository給我們帶來的優點,這些優點也是我們不能輕易丟掉它的原因:
- 提供一個簡單的模型,來獲取持久對象并管理期生命周期
- 把應用和領域設計從持久技術、多種數據庫策略解耦出來
- 容易被替換成啞實現(Mock)以便我們在測試中使用
如果你的項目屬于短期的項目,或者說你不用考慮更換數據訪問層,那么你就可以忽略第一和第二個優點。而第三個優點,借助于一些測試框架我們也可以實現,所以如果你不想用Repository,那就不用,前提條件是你所做的項目允許你這樣做,并且你也能夠找到好的替代方案來彌補Repository的優勢。比如說對洋蔥架構中的IDAL再進行一些改造等等。關于更多單元測試的話題,我們將在下一篇中一起來探討。如果大家對Repository有什么其它的看法,也歡迎一起參與討論。
文章列表