文章出處

概述

  上一篇我們算是粗略的介紹了一下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 }
View Code

 // 應用層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 }
View Code

// 領域層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 }
View Code

  
  上面領域層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 }
View Code

  接著就是讓我們的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 }
View Code

  哦,對了,別忘了把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 }
View Code

   那么我們應用層的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;
        }
    }
}
View Code

  如果光看這段代碼有沒有覺得很奇怪?沒有任何對_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 }
View Code

 

  這就是屬于職責定義不明確的問題,特別是上面注冊用戶的例子。應用層也有_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。它的兩大職責:

  1. 對領域實體的生命周期進行管理(從數據庫重建,以及持久化到數據庫)  ——被推遲到了應用層
  2. 解除領域層對基礎設施的依懶 

  在第一點生效后,所有更新類的操作都推遲到應用層去執行。那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 }
View Code

 // 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 }
View Code

   我們直接讓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 }
View Code

// 領域層 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 }
View Code

// 客戶端調用應用層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");
View Code

  現在,恐怕你再想在領域模型里面去使用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'
View Code

 可有可無的Repository

  我們把IRepository移出領域層之后,再加上我們對洋蔥架構的理解。我們就可以知道Repository在應用層已經可以被替換成別的東西,IDAL也可以啊:)。當然有人也許會建議直接拿EF來用多好,其實我不建議這樣去做,考慮到以后把EF換掉的可能性。并且我們加這樣一個接口真的不會礙著我們什么事。如果有人覺得在讀取數據的時候加一個Repository在中間,少掉了很多EF提供的功能,覺得很不爽,倒是可以試試像我們的IQuery接口一樣直接對DbSet來查詢。我們甚至可以學習CQRS架構,將“讀”的服務完全分離開,我們就可以單獨針對“讀”來獨立設計。

  但是Repository給我們帶來的優點,這些優點也是我們不能輕易丟掉它的原因:

  1. 提供一個簡單的模型,來獲取持久對象并管理期生命周期
  2. 把應用和領域設計從持久技術、多種數據庫策略解耦出來
  3. 容易被替換成啞實現(Mock)以便我們在測試中使用

  如果你的項目屬于短期的項目,或者說你不用考慮更換數據訪問層,那么你就可以忽略第一和第二個優點。而第三個優點,借助于一些測試框架我們也可以實現,所以如果你不想用Repository,那就不用,前提條件是你所做的項目允許你這樣做,并且你也能夠找到好的替代方案來彌補Repository的優勢。比如說對洋蔥架構中的IDAL再進行一些改造等等。關于更多單元測試的話題,我們將在下一篇中一起來探討。如果大家對Repository有什么其它的看法,也歡迎一起參與討論。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()