概述
領域驅動設計也就是3D(Domain-Driven Design)已經有了10年的歷史,我相信很多人或多或少都聽說過這個名詞,但是有多少人真正懂得如何去運用它,或者把它運用好呢?于是有人說,DDD和TDD這些玩意是一些形而上的東西,只是一茶余飯后的談資,又或是放到簡歷上提升逼格而已。前面這句話我寫完之后猶豫了,猶豫要不要把它刪掉,因為它讓我看起來像個噴子,我確實感到不解,為什么別人10年前創造總結出來的東西,我們在10年之后對它的理解還處于這么低的一個層次。開篇就說遠了,我也是最近才開始認真學習領域驅動設計,并且得到了園子里面netfocus,劉標才和田園里的蟋蟀的幫助,在此再次表示感謝。希望能和大家一起把DDD普及下去。
我們之前有一個關于領域驅動設計的討論,另外dax.net也有一個關于領域驅動設計的系列寫得不錯,有興趣的同學可以看看。本文會以一個初學者的角度來講解DDD,讓我們一切從零開始,我相信你跟我一樣也會愛上它的。
本篇主要討論一下為什么我們要用DDD,它能夠為我們帶來什么?
領域驅動系列
初探領域驅動設計(1)為復雜業務而生
初探領域驅動設計(2)EF 和 Repository
初探領域驅動設計(3)寫好單元測試
......
目錄
一點廢話 ,我們需要好的設計么?
當我們學習一些設計模式或者框架的時候,總有人會站出來和你說“這些都沒有用,只要能實現功能就行了。” 在這里并非針對某個人,實際上我認為他們說的是對的,在資源有限的情況下,我們為了完成項目的交付,這是我們最好的選擇。但是別忘了,欠下的債總是要還的,以實現功能為導向的項目務必會造成維護性的大大降低,如果只是一個臨時隨便用用的東西倒是可以一試,但如果是要長期進行更新的產品,那后期就會拖該產品的后腿。
我們團隊現在維護著一個有著20多年歷史的產品,該產品是一個酒店、餐飲行業的POS系統,在美國和亞太地區都占有著比較大的市場份額。該產品從C,C++,VB6一路更新,直到現在的C#,但是很可惜不是整體替換,而是局部的,所以現在項目里面這4種代碼全都有。可能你會覺得這玩的是混搭,是潮流,但事實是,一旦產品上線之后,會有很多的新功能,老bug等在那里,再加上“重市場輕技術”的高層在那里制訂戰略,你壓根就沒有時間或者沒有多少時間去重構。日積月累,等著你的就是每一次改代碼都如履薄冰,一不小心就因為改一個bug而整出好幾個新bug出來,前不久我們為了新版本的發布就停下所有開發的任務,大家集體花了1個月的時間去做回歸測試了。因為前期發布新版本之后bug太多,所以這次老大們都不敢輕易發布了。:)
這是我們血的教訓,如果你前期只顧開發功能,最后就會讓你很難再開發新功能。所以真誠的希望大家不要再片面的說“只要實現功能就可以了!”,軟件開發的領域這么大,我們沒有必要把自己局限在某一個框框里面。對于大型系統來說,我們要學習的地方還有很多:
- 組織良好、可閱讀性高的代碼可以讓其它開發人員很容易的開始去修改代碼。
- 低耦合,高內聚 - 適合運用設計模式以及原則來設計一些好的框架可以降低修改代碼引發新bug的風險。
- 良好的單元測試以及集成測試可以及時的幫助我們檢測新增或修改的代碼是否會破壞原有的邏輯。
- 自動化測試絕對是省時省力的好幫手,也是項目質量的保證。
- 持續集成可以幫助我們更快速安全的進行迭代。
上面說了這么多也沒有提到DDD,那么為什么它能夠在構建復雜系統的時候有優勢呢?我們可以從以下幾個點去思考:
- 從設計階段出發,站在業務的角度思考問題
- 厘清業務主次
- 獨立領域業務層,打通開發和測試階段
- 干凈的代碼
從設計階段開始,站在業務的角度思考問題
除了DDD,現在還流行另外一個詞匯TDD。但是不知道大家有沒有注意到DDD(Domain-Driven Design)中的D代表著設計,而TDD(Test-Driven Development)中的D代表著開發,你有沒有曾幾何時把領域驅動設計說成領域驅動開發呢?當然我們確實是可以根據領域驅動來開發,但是DDD被設計出來的完美初衷卻是設計。TDD強調的已經是開發了,要求開發人員先寫單元測試然后再通過不斷的迭代重構讓單元測試通過,以此來實現功能。這樣做的好處是強迫讓開發人員清楚正確的理解需求,要知道這年頭沒有正確理解需求就開始寫代碼的程序員大有人在,并且我不認為需求就是業務,需求已經是將本來的業務理解之后,轉化為了通過計算機可以實現的一些功能定義,通常是業務分析師或者項目經理會去完成這個工作。而DDD中的D(領域)更像是本來的業務,所以在領域驅動設計的時候,開發人員或者架構師直接與領域專家(或者說客戶)進行溝通來建模,這些業務模型也是以后開發人員進行設計和實現的依據。
領域模型被當作開發人員之間,開發人員與領域專家之間溝通的橋梁,這樣可以閉免開發人員用錯誤的方式去實現功能。實際上很多優秀的開發人員,都會很自然的將現實世界中的問題進行抽象,然后用計算機的語言表示出來,我們稱之為面向對象。但是由于缺少親臨其境的體驗,往往會離真實的業務模型有一些距離。
我們舉一個例子來說明一下這個問題,假如我們要開發一個電子商務的網站,這個需求已經非常清楚了,現在那么多的電子商務網站直接照抄一個就可以了。現在我們來做一個下單的功能,來看看怎么去實現 。
作為一個高級程序員,我們得用面向對象的方式去開發,先建類。于是我們有了用戶,訂單,訂單項的類,用戶創建訂單然后往訂單里面添加商品,添加訂單項的時候為了方便,我們只需要傳入產品ID和數量就可以了,于是Order類有一個AddItem的方法。
作為一個高級程序員,一看這圖感覺很完美,有木有? 好,下面開始實現AddItem方法。
Order里面是一個OrderItem的集合,而這個AddItem的方法接收的是productId,我去哪里搞個Product對象給你?我不可能在這個實體里面直接去查數據庫吧?本來是沖著這個技術點想咨詢一下大家,后來在小組里面討論了一下,我恍然大悟,上面的實體就是我從代碼的層面去思考想出來的,下單嘛,當然是用戶,訂單和訂單項嘍。可是只要去網上買過東西都知道,用戶是不會直接往訂單里面加東西的,而是先把商品加入購物車,然后再通過“結算”一次性就根據購物車生成了一張訂單,壓根沒有往訂單里面添加訂單項的行為。這才是真正的用戶行為(領域邏輯)所以后來,我們的實體變成這樣了:
所以業務是這樣的:
未注冊用戶也可以將商品添加到購物車中,但是不能下訂單。
并且購物車中的商品不能保存起來,用戶離開這個網站(一般是關掉瀏覽器),購物車中的商品就會消失。
注冊用戶購物車中的商品可以長期永久保存,通過購物車的“結算功能”,將購物車中選中的商品轉化為訂單。
所以購物車,應該在用戶注冊的時候就應該創建好,對應我們上面的User實體中的CreatShoppingCart()方法。下面我們先來簡單實現一下注冊的代碼。
//User領域實體代碼

1 namespace RepositoryAndEf.Domain 2 { 3 public class User : BaseEntity 4 { 5 public string Name { get; set; } 6 public string Email { get; set; } 7 public string Password { get; set; } 8 public Guid ShoppingCartId { get; set; } 9 public virtual ShoppingCart ShoppingCart { get; set; } 10 public virtual ICollection<Order> Orders { get; set; } 11 12 public void CreateShoppingCart() 13 { 14 ShoppingCart = new ShoppingCart 15 { 16 Id = Guid.NewGuid(), 17 Customer = this, 18 CustomerId = Id, 19 }; 20 21 ShoppingCartId = ShoppingCart.Id; 22 } 23 } 24 }
//領域層 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 var user = new User 15 { 16 Id = Guid.NewGuid(), 17 Email = email, 18 Name = name, 19 Password = password 20 }; 21 22 user.CreateShoppingCart(); 23 _userRepository.Insert(user); 24 return user; 25 } 26 } 27 }
//應用層 UserService.cs代碼

1 namespace RepositoryAndEf.Service 2 { 3 public class UserService : IUserService 4 { 5 protected Domain.UserService DomainuUserService 6 { 7 get 8 { 9 return EngineContext.Current.Resolve<Domain.UserService>(); 10 } 11 } 12 13 public User Register(string email, string name, string password) 14 { 15 var user = DomainuUserService.Register(email, name, password); 16 return user; 17 } 18 } 19 }
上面是我們一次建模的過程,是一個將業務轉變成代碼,將現實世界抽象成軟件世界的過程。我們需要畫出模型不斷的與業務人員(領域專家)去溝通,然后不斷的重構去完善我們的模型,以至于這個模型能最準確的反映真實的業務。這是在最開始的設計階段,是需求溝通階段就需要做的工作,并且會一直貫穿我們后面的開發甚至維護階段,沒有人可以一開始就把領域模型建的100%準確,需求是復雜的,并且需求還是隨時變化的,所以模型也會一直發生改變。它將作為開發人員與業務人員、測試人員以及開發人員自己之間溝通的橋梁。而DDD與其它方法論的區別之處就在于,它還提供了一整套的體系來保證后續對領域模型的重構不會讓系統變得四分五裂,比如架構分層,倉儲,依懶注入等等,我們后面再慢慢探討。
在DDD中,領域模型分為三種:
- 實體
- 值對象
- 領域服務
區分實體、值對象和領域服務
我們不打算去解釋以上的概念,我相信只要你搜索一下就可以得到很全面準確的答案。但是重要的是我們一定要理解3者之間的區別,什么時候是實體,什么時候是值對象,又是什么時候我們該用領域服務呢?我想這是剛接觸DDD的人都難免會有些糾結的地方吧,在這里就強調一下。
實體相對于值對象而言擁有“標識”的概念,標識可以讓我們持續性的跟蹤實體。標識和數據庫里面的“主鍵”是不一樣的概念,主鍵是技術上的概念,但是標識是業務上的概念。
在我們上面的例子中用戶ID是標識,我們用它來持續性的跟蹤我們的用戶。訂單ID是標識,我們用它來持續性的跟蹤訂單,同時我們的用戶和訂單都是有著不同的狀態。但是對于用戶的地址來說,我們用什么來做標識呢?在電子商務網站這樣的業務里面,我們不需要去持續的跟蹤這個地址信息,它在我們的系統里面也不會有著像訂單從“創建”、“已付款”、“已發貨”、“已收貨”等這樣的狀態,所以地址信息的我們系統中就是一個值對象。
但是我們如果換了一個系統,比如說死慢的長城寬帶,他們把地址作為跟蹤對象。同一個地址,誰都可以去注冊,但是同一個時間只允許一個人去注冊,那么這個地址對于長城寬帶來說就去要去持續性的跟蹤,有“開戶”,“銷戶”的狀態。那么地址信息對于長城寬帶來說就是一個實體。
解決完實體和值對象,領域服務就好說了,一些重要的領域操作,既不屬于實體也不屬于值對象,那就可以把它放到服務中了。比如說我們上面的領域服務UserService里面的注冊操作,注冊這個操作可以說就是將這個用戶保存到我們的系統中。在注冊之間,這個用戶是不存在的,我們又怎么能把注冊這個操作放到User實體中去呢?所以把它放到領域服務中成了我們最好的選擇。
即使是這樣,哪些操作應該放到領域服務中對于很多初學者來說還是一件比較難選擇的問題。也許只有慢慢的對業務越來越了解,對DDD應用的越來越熟,我們就會少一點糾結。
厘清業務主次-聚合與聚合根
在上面的模型中,我們有很多關系的存在:用戶-購物車(1對1),用戶-訂單-訂單項-產品(1對多,1對1),購物車-購物車項-產品等。在DDD中,我們把這樣多個模型用關聯串起來組成一個聚合(aggregation)。
在我們的模型中,購物車-購物車項是一個聚合,訂單-訂單項是一個聚合。我們通常需要保護這些聚合的一致性,比如說我們把一個訂單刪掉了,那么這個訂單的訂單項也需要一起刪除,否則他們存在也沒有任何的意義。以前我們還會用到觸發器,但是大家都知道這個東西維護起來比較麻煩,寫起來也不方便等,所以后來大家都是在代碼中來控制。但是一直沒有一個好的約束說我們如何去更好的控制這些一致性,代碼一直都很散亂,直到DDD,我們有了聚合和聚合根的概念,“我們通過為每一個聚合選擇一個根,并通過根來控制所有對邊界內的對象的訪問。外部對象只能持有根的引用;由于根控制了訪問,因此我們無法繞過它去修改內部元素。我們后面還會說到只能為根來建立Repository,這也是為了確保我們這里面講的數據的一致性。
在我們上面的聚合中,只能通過購物車實體來操作購物車項,而不能你自己寫一個保存的方法直接就把購物車項給保存到數據庫中去了。這就是聚合和聚合根起到的作用。我們來看一下我們購物車實體的代碼:

1 namespace RepositoryAndEf.Domain 2 { 3 public class ShoppingCart : BaseEntity 4 { 5 public ShoppingCart() 6 { 7 Items = new List<ShoppingCartItem>(); 8 } 9 10 #region Properties 11 12 public Guid CustomerId { get; set; } 13 public virtual User Customer { get; set; } 14 public virtual ICollection<ShoppingCartItem> Items { get; set; } 15 16 #endregion 17 18 #region Methods 19 public void AddItem(Product product, int quantity) 20 { 21 // 如果該產品ID已經存在于購物車中,我們直接更改數量即可 22 var repetitiveCartItem = Items.FirstOrDefault( 23 i => i.ProductId == product.Id); 24 25 if (repetitiveCartItem != null) 26 { 27 repetitiveCartItem.Quantity += quantity; 28 return; 29 } 30 31 Items.Add(new ShoppingCartItem 32 { 33 Product = product, 34 ProductId = product.Id, 35 Quantity = quantity, 36 }); 37 } 38 39 // 更改購物車數量 40 public void ChangeProductQuantity(Guid productId, int newQuantity) 41 { 42 var items = Items as ICollection<ShoppingCartItem>; 43 var existingCartItem = items.FirstOrDefault( 44 i => i.ProductId == productId); 45 46 if (existingCartItem == null) 47 { 48 throw new InvalidOperationException( 49 "Cannot find the product in shopping cart"); 50 } 51 existingCartItem.Quantity = newQuantity; 52 } 53 54 // 從購物車中移除該產品 55 public void RemoveItem(Guid productId) 56 { 57 var items = Items as ICollection<ShoppingCartItem>; 58 var existingCartItem = items.FirstOrDefault( 59 i => i.ProductId == productId); 60 61 if (existingCartItem == null) 62 { 63 throw new InvalidOperationException( 64 "Cannot find the product in shopping cart"); 65 } 66 67 items.Remove(existingCartItem); 68 } 69 #endregion 70 } 71 }
大家可以看到我們購物車實體的邏輯很清晰,因為我們很明確購物車擁有哪些操作。當然還有另一種做法即把這些操作都放到用戶實體中去,因為最終其實是用戶做的這些操作。那我們的聚合就變成了用戶-購物車-購物車項,這樣也沒有什么不可以,反而更符合真實的場景。但是會導致我們的聚合過龐大,也就是說我必須要先有用戶實體才能進行操作,用戶用戶可能會綁上很多的東西:購物車、訂單、地址等等。在現在都是ajax來操作的大型網站中,我們需要在服務端把這個用戶請求加載出來再執行添加購物車的操作呢?還是可以直接加載購物車實體來操作呢?這就是一個粒度的問題,不同的問題和場景,大家可以區別來對待。總之聚合是可以根據業務或者一些特定需求來做出調整的。比如說購物車-購物車項-產品,這也是一個聚合,但是由于產品的特殊性,我們可以把產品也作為一個聚合根來單獨進行訪問。
我們來看一下應用層ShoppingCartService的代碼:

1 public class ShoppingCartService : IShoppingCartService 2 { 3 private IRepository<ShoppingCart> _shoppingCartRepository; 4 private IRepository<Product> _productRepository; 5 6 public ShoppingCartService(IRepository<ShoppingCart> shoppingCartRepository, 7 IRepository<Product> productRepository) 8 { 9 _shoppingCartRepository = shoppingCartRepository; 10 _productRepository = productRepository; 11 } 12 13 public ShoppingCart AddToCart(Guid cartId, Guid productId, int quantity) 14 { 15 var cart = _shoppingCartRepository.GetById(cartId); 16 var product = _productRepository.GetById(productId); 17 cart.AddItem(product, quantity); 18 19 _shoppingCartRepository.Update(cart); 20 return cart; 21 } 22 23 }
此應用層代碼一出,大家就會發現,這代碼太簡潔了,有木有?因為所有的邏輯、業務都被放到領域實體那里面去處理了。即使我們業務邏輯改變了,或者我們需要重構了,它們都在領域實體那里面,改那里就好了。接下來的問題是,如何確保安全,正確的一次又一次的對領域實體進行重構呢?畢竟它也是各種關聯,各種依懶呀?您請接著往下看我們的單元測試環節。
獨立領域業務層 - 高內聚,低耦合,可測試
講到這里,請允許我從網上盜一張圖,當然這張圖早就已經是被引用過無數次了,它就是DDD中使用的分層結構。
關于這個分層,每一層是干什么的,具體怎么玩,大家可以看一下dax的這一篇文章講解的很清楚。總之,我們的領域模型以及相關的類比如工廠等會被獨立成為一層來與應用層和基礎設計層交互。
領域層是獨立的,首先它是應用層的下層,所以肯定不會有對應用層的依懶,但是領域有一些模型或者服務少不了是要與數據庫打交道的,比如說我們在注冊用戶的時候需要去驗證當前的郵箱是不是已經被占用了。而這一類操作都是屬于基礎設施層做的事情,包含像一些數據庫操作,日志,緩存等等。那么我們如何避免領域層對基礎設施層的依懶呢?感謝面向對象設計 - 面向接口編程,只不過這里面的場景特別有代表性,它是一個非常常見的問題,于是它成為了一個模式:倉儲(Repository)。

1 namespace RepositoryAndEf.Core.Data 2 { 3 public partial interface IRepository<T> where T : BaseEntity 4 { 5 T GetById(object id); 6 7 IEnumerable<T> Get( 8 Expression<Func<T, Boolean>> predicate); 9 10 bool Insert(T entity); 11 bool Update(T entity); 12 bool Delete(T entity); 13 } 14 }
一般情況下,我們會把倉儲的接口放到領域層,或者也可以再建一個Core層來作個項目最下面的那一層提供一些最公共的組件部分。關于倉儲的代碼,大家在上面領域服務UserService中的注冊代碼中就已經見到過了。可能需要注意的是,Repository用來將數據庫與其它的業務和技術分離,所以我們在領域層中使用它,還在應用層中使用它。
Repository讓我們專注于模型,不用去考慮持久化的問題。更為重要的一點是,因為它是接口,所以我們可以很方便的替代它,或者模擬一個實現來對我們的領域模型進行單元測試。下面是我們實現的MockRepository的代碼:

1 public class MockRepository<T>: IRepository<T> where T : BaseEntity 2 { 3 private List<T> _list = new List<T>(); 4 5 public T GetById(Guid id) 6 { 7 return _list.FirstOrDefault(e => e.Id == id); 8 } 9 10 public IEnumerable<T> Get(Expression<Func<T, bool>> predicate) 11 { 12 return _list.Where(predicate.Compile()); 13 } 14 15 public bool Insert(T entity) 16 { 17 if (GetById(entity.Id) != null) 18 { 19 throw new InvalidCastException("The id has already existed"); 20 } 21 22 _list.Add(entity); 23 return true; 24 } 25 26 public bool Update(T entity) 27 { 28 var existingEntity = GetById(entity.Id); 29 if (existingEntity == null) 30 { 31 throw new InvalidCastException("Cannot find the entity."); 32 } 33 34 existingEntity = entity; 35 return true; 36 } 37 38 public bool Delete(T entity) 39 { 40 var existingEntity = GetById(entity.Id); 41 if (existingEntity == null) 42 { 43 throw new InvalidCastException("Cannot find the entity."); 44 } 45 46 _list.Remove(entity); 47 return true; 48 }
下面我們給我們User領域實體的注冊方法加一個檢查Email是否存在的邏輯。

1 public virtual User Register(string email, string name, string password) 2 { 3 if (_userRepository.Get().Any(u => u.Email == email)) 4 { 5 throw new ArgumentException("email has already existed"); 6 } 7 8 var user = new User 9 { 10 Id = Guid.NewGuid(), 11 Email = email, 12 Name = name, 13 Password = password 14 }; 15 16 user.CreateShoppingCart(); 17 _userRepository.Insert(user); 18 return user; 19 }
在我們真實的Repository出來之前,不管我們是打算是EF,還是NHibernate,我們現在只要對這個Mock的Repository來編程或者進行單元測試就可以了。
//UserService領域服務在單元測試

1 public class UserServiceTests 2 { 3 private IRepository<User> _userRepository = new MockRepository<User>(); 4 5 [Fact] 6 public void RegisterUser_ExpectedParameters_Success() 7 { 8 var userService = new UserService(_userRepository); 9 var registeredUser = userService.Register( 10 "hellojesseliu@outlook.com", 11 "Jesse", 12 "Jesse"); 13 14 var userFromRepository = _userRepository.GetById(registeredUser.Id); 15 16 userFromRepository.Should().NotBe(null); 17 userFromRepository.Email.Should().Be("hellojesseliu@outlook.com"); 18 userFromRepository.Name.Should().Be("Jesse"); 19 userFromRepository.Password.Should().Be("Jesse"); 20 } 21 22 [Fact] 23 public void RegisterUser_ExistedEmail_ThrowException() 24 { 25 var userService = new UserService(_userRepository); 26 var registeredUser = userService.Register( 27 "hellojesseliu@outlook.com", 28 "Jesse", 29 "Jesse"); 30 31 var userFromRepository = _userRepository.GetById(registeredUser.Id); 32 userFromRepository.Should().NotBe(null); 33 34 Action action = () => userService.Register( 35 "hellojesseliu@outlook.com", 36 "Jesse_01", 37 "Jesse"); 38 action.ShouldThrow<ArgumentException>(); 39 } 40 }
我們用的XUnit.net作單元測試框架,同時用了Fluent Assertions。
結果很漂亮,有木有?有了單元測試來為我們的領域模型保駕護航,我們就可以安全的進行重構了。
干凈漂亮的代碼
經常有人說代碼是一件藝術,碼農都是藝術家。我很喜歡這句話,如果你也認同,那就請像對待藝術品一樣對待我們的代碼,精心的打磨它。并且你不一定要非常的有經驗才可以干這件事情;
如果你剛入行,那至少保證一代碼可讀性好(好的命名,代碼邏輯清晰等);
再往上一點,你要能夠更好的組織代碼(類,函數);
等到你也成為專家了,那就開始考慮一些重用性,可擴展性,可維護性,可測試性的這些比較范的東西了;
而最后就上升到架構層面,考慮系統各個組件之間通訊,分層,等等。最后你就成為碼神了。
DDD里面引入的一些思路包括分層、依懶注入、倉儲等,可以給我們一些指導,大家從上面的代碼也可以看出這些代碼組織的很好,邏輯也不會散亂的到處都是。當然這個項目代碼量有限,說服力是有限的,后面我們還會嘗試去加入應用層的代碼。代碼已經放到CodePlex上去了:http://repositoryandef.codeplex.com
歡迎大家Follow。注意代碼還沒有寫完,只是一個初級版本,我們后面會慢慢完善。這個項目會使用EF來作業ORM框架,Autofac作依懶注入容器,用Xunit作單元測試框架的同時引入了Fluent Assertions。
小結
本文主要介紹了DDD的一些基礎概念:
- 領域模型:領域實體、領域服務以及值對象;建模一定要從真實的領域業務出發,多與領域專家進行溝通來完善模型。
- 聚合與聚合根:它的主要作用是用來確保各種關系下的實體的數據一致性;但是確認聚合根這個過程,實際上也是對業務的梳理過程。
- 架構分層: 每一層都職責清楚;依懶于接口來降低耦合。
- 封裝和測試: 所有的業務都放到領域層,同時對領域層進行單元測試來確保最核心的邏輯不會遭到破壞。
個人感覺沒有必要太強調Repository的概念,從領域實體的生命周期(創建-持久化到數據庫-銷毀-從數據庫重建)你會發現其實這個過程很普遍,并不是只有DDD才有的。所以我認為Repository主要是將數據訪問功能給隔離開,避免領域實體對基礎設施層的依懶。那它和三層有什么區別? BLL 引用DAL不也是依懶于接口么?給我的感覺是,DDD的領域實體持久化這一塊就是三層里面的思路。這可能是在學習DDD初期的想法,因為真實的大型項目中是不會直接把領域實體給持久化的,那個叫DTO,于是Repository<>里面放的就不是我們的領域實體了,而是將領域實體轉換成對應的DTO。
是否一定要使用DTO呢?領域實體和DTO互相轉換,最后到了表現層DTO還要和ViewModel轉換,會不會帶來復雜性和性能上的損失?Repository和EF還有Unit Of Work怎么來協調?抱怨寫單元測試么?怎么樣讓寫單元測試不變成只是走過場而已? 這些問題留給我們后面再解決吧。
文章列表