初探領(lǐng)域驅(qū)動設(shè)計:為復(fù)雜業(yè)務(wù)而生
概述
領(lǐng)域驅(qū)動設(shè)計也就是3D(Domain-Driven Design)已經(jīng)有了10年的歷史,我相信很多人或多或少都聽說過這個名詞,但是有多少人真正懂得如何去運(yùn)用它,或者把它運(yùn)用好呢?于是有人說,DDD和TDD這些玩意是一些形而上的東西,只是一茶余飯后的談資,又或是放到簡歷上提升逼格而已。前面這句話我寫完之后猶豫了,猶豫要不要把它刪掉,因為它讓我看起來像個噴子,我確實感到不解,為什么別人10年前創(chuàng)造總結(jié)出來的東西,我們在10年之后對它的理解還處于這么低的一個層次。開篇就說遠(yuǎn)了,我也是最近才開始認(rèn)真學(xué)習(xí)領(lǐng)域驅(qū)動設(shè)計,并且得到了園子里面netfocus,劉標(biāo)才和田園里的蟋蟀的幫助,在此再次表示感謝。希望能和大家一起把DDD普及下去。
我們之前有一個關(guān)于領(lǐng)域驅(qū)動設(shè)計的討論,另外dax.net也有一個關(guān)于領(lǐng)域驅(qū)動設(shè)計的系列寫得不錯,有興趣的同學(xué)可以看看。本文會以一個初學(xué)者的角度來講解DDD,讓我們一切從零開始,我相信你跟我一樣也會愛上它的。
本篇主要討論一下為什么我們要用DDD,它能夠為我們帶來什么?
當(dāng)我們學(xué)習(xí)一些設(shè)計模式或者框架的時候,總有人會站出來和你說“這些都沒有用,只要能實現(xiàn)功能就行了。” 在這里并非針對某個人,實際上我認(rèn)為他們說的是對的,在資源有限的情況下,我們?yōu)榱送瓿身椖康慕桓?,這是我們最好的選擇。但是別忘了,欠下的債總是要還的,以實現(xiàn)功能為導(dǎo)向的項目務(wù)必會造成維護(hù)性的大大降低,如果只是一個臨時隨便用用的東西倒是可以一試,但如果是要長期進(jìn)行更新的產(chǎn)品,那后期就會拖該產(chǎn)品的后腿。
我們團(tuán)隊現(xiàn)在維護(hù)著一個有著20多年歷史的產(chǎn)品,該產(chǎn)品是一個酒店、餐飲行業(yè)的POS系統(tǒng),在美國和亞太地區(qū)都占有著比較大的市場份額。該產(chǎn)品從C,C++,VB6一路更新,直到現(xiàn)在的C#,但是很可惜不是整體替換,而是局部的,所以現(xiàn)在項目里面這4種代碼全都有。可能你會覺得這玩的是混搭,是潮流,但事實是,一旦產(chǎn)品上線之后,會有很多的新功能,老bug等在那里,再加上“重市場輕技術(shù)”的高層在那里制訂戰(zhàn)略,你壓根就沒有時間或者沒有多少時間去重構(gòu)。日積月累,等著你的就是每一次改代碼都如履薄冰,一不小心就因為改一個bug而整出好幾個新bug出來,前不久我們?yōu)榱诵掳姹镜陌l(fā)布就停下所有開發(fā)的任務(wù),大家集體花了1個月的時間去做回歸測試了。因為前期發(fā)布新版本之后bug太多,所以這次老大們都不敢輕易發(fā)布了。:)
這是我們血的教訓(xùn),如果你前期只顧開發(fā)功能,最后就會讓你很難再開發(fā)新功能。所以真誠的希望大家不要再片面的說“只要實現(xiàn)功能就可以了!”,軟件開發(fā)的領(lǐng)域這么大,我們沒有必要把自己局限在某一個框框里面。對于大型系統(tǒng)來說,我們要學(xué)習(xí)的地方還有很多:
- 組織良好、可閱讀性高的代碼可以讓其它開發(fā)人員很容易的開始去修改代碼。
- 低耦合,高內(nèi)聚 - 適合運(yùn)用設(shè)計模式以及原則來設(shè)計一些好的框架可以降低修改代碼引發(fā)新bug的風(fēng)險。
- 良好的單元測試以及集成測試可以及時的幫助我們檢測新增或修改的代碼是否會破壞原有的邏輯。
- 自動化測試絕對是省時省力的好幫手,也是項目質(zhì)量的保證。
- 持續(xù)集成可以幫助我們更快速安全的進(jìn)行迭代。
上面說了這么多也沒有提到DDD,那么為什么它能夠在構(gòu)建復(fù)雜系統(tǒng)的時候有優(yōu)勢呢?我們可以從以下幾個點(diǎn)去思考:
- 從設(shè)計階段出發(fā),站在業(yè)務(wù)的角度思考問題
- 厘清業(yè)務(wù)主次
- 獨(dú)立領(lǐng)域業(yè)務(wù)層,打通開發(fā)和測試階段
- 干凈的代碼
從設(shè)計階段開始,站在業(yè)務(wù)的角度思考問題
除了DDD,現(xiàn)在還流行另外一個詞匯TDD。但是不知道大家有沒有注意到DDD(Domain-Driven Design)中的D代表著設(shè)計,而TDD(Test-Driven Development)中的D代表著開發(fā),你有沒有曾幾何時把領(lǐng)域驅(qū)動設(shè)計說成領(lǐng)域驅(qū)動開發(fā)呢?當(dāng)然我們確實是可以根據(jù)領(lǐng)域驅(qū)動來開發(fā),但是DDD被設(shè)計出來的完美初衷卻是設(shè)計。TDD強(qiáng)調(diào)的已經(jīng)是開發(fā)了,要求開發(fā)人員先寫單元測試然后再通過不斷的迭代重構(gòu)讓單元測試通過,以此來實現(xiàn)功能。這樣做的好處是強(qiáng)迫讓開發(fā)人員清楚正確的理解需求,要知道這年頭沒有正確理解需求就開始寫代碼的程序員大有人在,并且我不認(rèn)為需求就是業(yè)務(wù),需求已經(jīng)是將本來的業(yè)務(wù)理解之后,轉(zhuǎn)化為了通過計算機(jī)可以實現(xiàn)的一些功能定義,通常是業(yè)務(wù)分析師或者項目經(jīng)理會去完成這個工作。而DDD中的D(領(lǐng)域)更像是本來的業(yè)務(wù),所以在領(lǐng)域驅(qū)動設(shè)計的時候,開發(fā)人員或者架構(gòu)師直接與領(lǐng)域?qū)<遥ɑ蛘哒f客戶)進(jìn)行溝通來建模,這些業(yè)務(wù)模型也是以后開發(fā)人員進(jìn)行設(shè)計和實現(xiàn)的依據(jù)。
領(lǐng)域模型被當(dāng)作開發(fā)人員之間,開發(fā)人員與領(lǐng)域?qū)<抑g溝通的橋梁,這樣可以閉免開發(fā)人員用錯誤的方式去實現(xiàn)功能。實際上很多優(yōu)秀的開發(fā)人員,都會很自然的將現(xiàn)實世界中的問題進(jìn)行抽象,然后用計算機(jī)的語言表示出來,我們稱之為面向?qū)ο蟆5怯捎谌鄙儆H臨其境的體驗,往往會離真實的業(yè)務(wù)模型有一些距離。
我們舉一個例子來說明一下這個問題,假如我們要開發(fā)一個電子商務(wù)的網(wǎng)站,這個需求已經(jīng)非常清楚了,現(xiàn)在那么多的電子商務(wù)網(wǎng)站直接照抄一個就可以了?,F(xiàn)在我們來做一個下單的功能,來看看怎么去實現(xiàn) 。
作為一個高級程序員,我們得用面向?qū)ο蟮姆绞饺ラ_發(fā),先建類。于是我們有了用戶,訂單,訂單項的類,用戶創(chuàng)建訂單然后往訂單里面添加商品,添加訂單項的時候為了方便,我們只需要傳入產(chǎn)品ID和數(shù)量就可以了,于是Order類有一個AddItem的方法。
作為一個高級程序員,一看這圖感覺很完美,有木有? 好,下面開始實現(xiàn)AddItem方法。
Order里面是一個OrderItem的集合,而這個AddItem的方法接收的是productId,我去哪里搞個Product對象給你?我不可能在這個實體里面直接去查數(shù)據(jù)庫吧?本來是沖著這個技術(shù)點(diǎn)想咨詢一下大家,后來在小組里面討論了一下,我恍然大悟,上面的實體就是我從代碼的層面去思考想出來的,下單嘛,當(dāng)然是用戶,訂單和訂單項嘍??墒侵灰ゾW(wǎng)上買過東西都知道,用戶是不會直接往訂單里面加?xùn)|西的,而是先把商品加入購物車,然后再通過“結(jié)算”一次性就根據(jù)購物車生成了一張訂單,壓根沒有往訂單里面添加訂單項的行為。這才是真正的用戶行為(領(lǐng)域邏輯)所以后來,我們的實體變成這樣了:
所以業(yè)務(wù)是這樣的:
- 未注冊用戶也可以將商品添加到購物車中,但是不能下訂單。
- 并且購物車中的商品不能保存起來,用戶離開這個網(wǎng)站(一般是關(guān)掉瀏覽器),購物車中的商品就會消失。
- 注冊用戶購物車中的商品可以長期永久保存,通過購物車的“結(jié)算功能”,將購物車中選中的商品轉(zhuǎn)化為訂單。
- 所以購物車,應(yīng)該在用戶注冊的時候就應(yīng)該創(chuàng)建好,對應(yīng)我們上面的User實體中的CreatShoppingCart()方法。下面我們先來簡單實現(xiàn)一下注冊的代碼。
//User領(lǐng)域?qū)嶓w代碼
- namespace RepositoryAndEf.Domain
- {
- public class User : BaseEntity
- {
- public string Name { get; set; }
- public string Email { get; set; }
- public string Password { get; set; }
- public Guid ShoppingCartId { get; set; }
- public virtual ShoppingCart ShoppingCart { get; set; }
- public virtual ICollection<Order> Orders { get; set; }
- public void CreateShoppingCart()
- {
- ShoppingCart = new ShoppingCart
- {
- Id = Guid.NewGuid(),
- Customer = this,
- CustomerId = Id,
- };
- ShoppingCartId = ShoppingCart.Id;
- }
- }
- }
//領(lǐng)域?qū)?UserService.cs代碼
- namespace RepositoryAndEf.Domain
- {
- public class UserService
- {
- private IRepository<User> _userRepository;
- public UserService(IRepository<User> userRepsoitory)
- {
- _userRepository = userRepsoitory;
- }
- public virtual User Register(string email, string name, string password)
- {
- var user = new User
- {
- Id = Guid.NewGuid(),
- Email = email,
- Name = name,
- Password = password
- };
- user.CreateShoppingCart();
- _userRepository.Insert(user);
- return user;
- }
- }
- }
//應(yīng)用層 UserService.cs代碼
- namespace RepositoryAndEf.Service
- {
- public class UserService : IUserService
- {
- protected Domain.UserService DomainuUserService
- {
- get
- {
- return EngineContext.Current.Resolve<Domain.UserService>();
- }
- }
- public User Register(string email, string name, string password)
- {
- var user = DomainuUserService.Register(email, name, password);
- return user;
- }
- }
- }
上面是我們一次建模的過程,是一個將業(yè)務(wù)轉(zhuǎn)變成代碼,將現(xiàn)實世界抽象成軟件世界的過程。我們需要畫出模型不斷的與業(yè)務(wù)人員(領(lǐng)域?qū)<遥┤贤ǎ缓蟛粩嗟闹貥?gòu)去完善我們的模型,以至于這個模型能最準(zhǔn)確的反映真實的業(yè)務(wù)。這是在最開始的設(shè)計階段,是需求溝通階段就需要做的工作,并且會一直貫穿我們后面的開發(fā)甚至維護(hù)階段,沒有人可以一開始就把領(lǐng)域模型建的100%準(zhǔn)確,需求是復(fù)雜的,并且需求還是隨時變化的,所以模型也會一直發(fā)生改變。它將作為開發(fā)人員與業(yè)務(wù)人員、測試人員以及開發(fā)人員自己之間溝通的橋梁。而DDD與其它方法論的區(qū)別之處就在于,它還提供了一整套的體系來保證后續(xù)對領(lǐng)域模型的重構(gòu)不會讓系統(tǒng)變得四分五裂,比如架構(gòu)分層,倉儲,依懶注入等等,我們后面再慢慢探討。
在DDD中,領(lǐng)域模型分為三種:
- 實體
- 值對象
- 領(lǐng)域服務(wù)
我們不打算去解釋以上的概念,我相信只要你搜索一下就可以得到很全面準(zhǔn)確的答案。但是重要的是我們一定要理解3者之間的區(qū)別,什么時候是實體,什么時候是值對象,又是什么時候我們該用領(lǐng)域服務(wù)呢?我想這是剛接觸DDD的人都難免會有些糾結(jié)的地方吧,在這里就強(qiáng)調(diào)一下。
實體相對于值對象而言擁有“標(biāo)識”的概念,標(biāo)識可以讓我們持續(xù)性的跟蹤實體。標(biāo)識和數(shù)據(jù)庫里面的“主鍵”是不一樣的概念,主鍵是技術(shù)上的概念,但是標(biāo)識是業(yè)務(wù)上的概念。
在我們上面的例子中用戶ID是標(biāo)識,我們用它來持續(xù)性的跟蹤我們的用戶。訂單ID是標(biāo)識,我們用它來持續(xù)性的跟蹤訂單,同時我們的用戶和訂單都是有著不同的狀態(tài)。但是對于用戶的地址來說,我們用什么來做標(biāo)識呢?在電子商務(wù)網(wǎng)站這樣的業(yè)務(wù)里面,我們不需要去持續(xù)的跟蹤這個地址信息,它在我們的系統(tǒng)里面也不會有著像訂單從“創(chuàng)建”、“已付款”、“已發(fā)貨”、“已收貨”等這樣的狀態(tài),所以地址信息的我們系統(tǒng)中就是一個值對象。
但是我們?nèi)绻麚Q了一個系統(tǒng),比如說死慢的長城寬帶,他們把地址作為跟蹤對象。同一個地址,誰都可以去注冊,但是同一個時間只允許一個人去注冊,那么這個地址對于長城寬帶來說就去要去持續(xù)性的跟蹤,有“開戶”,“銷戶”的狀態(tài)。那么地址信息對于長城寬帶來說就是一個實體。
解決完實體和值對象,領(lǐng)域服務(wù)就好說了,一些重要的領(lǐng)域操作,既不屬于實體也不屬于值對象,那就可以把它放到服務(wù)中了。比如說我們上面的領(lǐng)域服務(wù)UserService里面的注冊操作,注冊這個操作可以說就是將這個用戶保存到我們的系統(tǒng)中。在注冊之間,這個用戶是不存在的,我們又怎么能把注冊這個操作放到User實體中去呢?所以把它放到領(lǐng)域服務(wù)中成了我們最好的選擇。
即使是這樣,哪些操作應(yīng)該放到領(lǐng)域服務(wù)中對于很多初學(xué)者來說還是一件比較難選擇的問題。也許只有慢慢的對業(yè)務(wù)越來越了解,對DDD應(yīng)用的越來越熟,我們就會少一點(diǎn)糾結(jié)。
#p#
厘清業(yè)務(wù)主次-聚合與聚合根
在上面的模型中,我們有很多關(guān)系的存在:用戶-購物車(1對1),用戶-訂單-訂單項-產(chǎn)品(1對多,1對1),購物車-購物車項-產(chǎn)品等。在DDD中,我們把這樣多個模型用關(guān)聯(lián)串起來組成一個聚合(aggregation)。
在我們的模型中,購物車-購物車項是一個聚合,訂單-訂單項是一個聚合。我們通常需要保護(hù)這些聚合的一致性,比如說我們把一個訂單刪掉了,那么這個訂單的訂單項也需要一起刪除,否則他們存在也沒有任何的意義。以前我們還會用到觸發(fā)器,但是大家都知道這個東西維護(hù)起來比較麻煩,寫起來也不方便等,所以后來大家都是在代碼中來控制。但是一直沒有一個好的約束說我們?nèi)绾稳ジ玫目刂七@些一致性,代碼一直都很散亂,直到DDD,我們有了聚合和聚合根的概念,“我們通過為每一個聚合選擇一個根,并通過根來控制所有對邊界內(nèi)的對象的訪問。外部對象只能持有根的引用;由于根控制了訪問,因此我們無法繞過它去修改內(nèi)部元素。我們后面還會說到只能為根來建立Repository,這也是為了確保我們這里面講的數(shù)據(jù)的一致性。
在我們上面的聚合中,只能通過購物車實體來操作購物車項,而不能你自己寫一個保存的方法直接就把購物車項給保存到數(shù)據(jù)庫中去了。這就是聚合和聚合根起到的作用。我們來看一下我們購物車實體的代碼:
- namespace RepositoryAndEf.Domain
- {
- public class ShoppingCart : BaseEntity
- {
- public ShoppingCart()
- {
- Items = new List<ShoppingCartItem>();
- }
- #region Properties
- public Guid CustomerId { get; set; }
- public virtual User Customer { get; set; }
- public virtual ICollection<ShoppingCartItem> Items { get; set; }
- #endregion
- #region Methods
- public void AddItem(Product product, int quantity)
- {
- // 如果該產(chǎn)品ID已經(jīng)存在于購物車中,我們直接更改數(shù)量即可
- var repetitiveCartItem = Items.FirstOrDefault(
- i => i.ProductId == product.Id);
- if (repetitiveCartItem != null)
- {
- repetitiveCartItem.Quantity += quantity;
- return;
- }
- Items.Add(new ShoppingCartItem
- {
- Product = product,
- ProductId = product.Id,
- Quantity = quantity,
- });
- }
- // 更改購物車數(shù)量
- public void ChangeProductQuantity(Guid productId, int newQuantity)
- {
- var items = Items as ICollection<ShoppingCartItem>;
- var existingCartItem = items.FirstOrDefault(
- i => i.ProductId == productId);
- if (existingCartItem == null)
- {
- throw new InvalidOperationException(
- "Cannot find the product in shopping cart");
- }
- existingCartItem.Quantity = newQuantity;
- }
- // 從購物車中移除該產(chǎn)品
- public void RemoveItem(Guid productId)
- {
- var items = Items as ICollection<ShoppingCartItem>;
- var existingCartItem = items.FirstOrDefault(
- i => i.ProductId == productId);
- if (existingCartItem == null)
- {
- throw new InvalidOperationException(
- "Cannot find the product in shopping cart");
- }
- items.Remove(existingCartItem);
- }
- #endregion
- }
- }
大家可以看到我們購物車實體的邏輯很清晰,因為我們很明確購物車擁有哪些操作。當(dāng)然還有另一種做法即把這些操作都放到用戶實體中去,因為最終其實是用戶做的這些操作。那我們的聚合就變成了用戶-購物車-購物車項,這樣也沒有什么不可以,反而更符合真實的場景。但是會導(dǎo)致我們的聚合過龐大,也就是說我必須要先有用戶實體才能進(jìn)行操作,用戶用戶可能會綁上很多的東西:購物車、訂單、地址等等。在現(xiàn)在都是ajax來操作的大型網(wǎng)站中,我們需要在服務(wù)端把這個用戶請求加載出來再執(zhí)行添加購物車的操作呢?還是可以直接加載購物車實體來操作呢?這就是一個粒度的問題,不同的問題和場景,大家可以區(qū)別來對待??傊酆鲜强梢愿鶕?jù)業(yè)務(wù)或者一些特定需求來做出調(diào)整的。比如說購物車-購物車項-產(chǎn)品,這也是一個聚合,但是由于產(chǎn)品的特殊性,我們可以把產(chǎn)品也作為一個聚合根來單獨(dú)進(jìn)行訪問。
我們來看一下應(yīng)用層ShoppingCartService的代碼:
- public class ShoppingCartService : IShoppingCartService
- {
- private IRepository<ShoppingCart> _shoppingCartRepository;
- private IRepository<Product> _productRepository;
- public ShoppingCartService(IRepository<ShoppingCart> shoppingCartRepository,
- IRepository<Product> productRepository)
- {
- _shoppingCartRepository = shoppingCartRepository;
- _productRepository = productRepository;
- }
- public ShoppingCart AddToCart(Guid cartId, Guid productId, int quantity)
- {
- var cart = _shoppingCartRepository.GetById(cartId);
- var product = _productRepository.GetById(productId);
- cart.AddItem(product, quantity);
- _shoppingCartRepository.Update(cart);
- return cart;
- }
- }
此應(yīng)用層代碼一出,大家就會發(fā)現(xiàn),這代碼太簡潔了,有木有?因為所有的邏輯、業(yè)務(wù)都被放到領(lǐng)域?qū)嶓w那里面去處理了。即使我們業(yè)務(wù)邏輯改變了,或者我們需要重構(gòu)了,它們都在領(lǐng)域?qū)嶓w那里面,改那里就好了。接下來的問題是,如何確保安全,正確的一次又一次的對領(lǐng)域?qū)嶓w進(jìn)行重構(gòu)呢?畢竟它也是各種關(guān)聯(lián),各種依懶呀?您請接著往下看我們的單元測試環(huán)節(jié)。
#p#
獨(dú)立領(lǐng)域業(yè)務(wù)層 - 高內(nèi)聚,低耦合,可測試
講到這里,請允許我從網(wǎng)上盜一張圖,當(dāng)然這張圖早就已經(jīng)是被引用過無數(shù)次了,它就是DDD中使用的分層結(jié)構(gòu)。
關(guān)于這個分層,每一層是干什么的,具體怎么玩,大家可以看一下dax的這一篇文章講解的很清楚。總之,我們的領(lǐng)域模型以及相關(guān)的類比如工廠等會被獨(dú)立成為一層來與應(yīng)用層和基礎(chǔ)設(shè)計層交互。
領(lǐng)域?qū)邮仟?dú)立的,首先它是應(yīng)用層的下層,所以肯定不會有對應(yīng)用層的依懶,但是領(lǐng)域有一些模型或者服務(wù)少不了是要與數(shù)據(jù)庫打交道的,比如說我們在注冊用戶的時候需要去驗證當(dāng)前的郵箱是不是已經(jīng)被占用了。而這一類操作都是屬于基礎(chǔ)設(shè)施層做的事情,包含像一些數(shù)據(jù)庫操作,日志,緩存等等。那么我們?nèi)绾伪苊忸I(lǐng)域?qū)訉A(chǔ)設(shè)施層的依懶呢?感謝面向?qū)ο笤O(shè)計 - 面向接口編程,只不過這里面的場景特別有代表性,它是一個非常常見的問題,于是它成為了一個模式:倉儲(Repository)。
- namespace RepositoryAndEf.Core.Data
- {
- public partial interface IRepository<T> where T : BaseEntity
- {
- T GetById(object id);
- IEnumerable<T> Get(
- Expression<Func<T, Boolean>> predicate);
- bool Insert(T entity);
- bool Update(T entity);
- bool Delete(T entity);
- }
- }
一般情況下,我們會把倉儲的接口放到領(lǐng)域?qū)?,或者也可以再建一個Core層來作個項目最下面的那一層提供一些最公共的組件部分。關(guān)于倉儲的代碼,大家在上面領(lǐng)域服務(wù)UserService中的注冊代碼中就已經(jīng)見到過了??赡苄枰⒁獾氖?,Repository用來將數(shù)據(jù)庫與其它的業(yè)務(wù)和技術(shù)分離,所以我們在領(lǐng)域?qū)又惺褂盟?,還在應(yīng)用層中使用它。
Repository讓我們專注于模型,不用去考慮持久化的問題。更為重要的一點(diǎn)是,因為它是接口,所以我們可以很方便的替代它,或者模擬一個實現(xiàn)來對我們的領(lǐng)域模型進(jìn)行單元測試。下面是我們實現(xiàn)的MockRepository的代碼:
- public class MockRepository<T>: IRepository<T> where T : BaseEntity
- {
- private List<T> _list = new List<T>();
- public T GetById(Guid id)
- {
- return _list.FirstOrDefault(e => e.Id == id);
- }
- public IEnumerable<T> Get(Expression<Func<T, bool>> predicate)
- {
- return _list.Where(predicate.Compile());
- }
- public bool Insert(T entity)
- {
- if (GetById(entity.Id) != null)
- {
- throw new InvalidCastException("The id has already existed");
- }
- _list.Add(entity);
- return true;
- }
- public bool Update(T entity)
- {
- var existingEntity = GetById(entity.Id);
- if (existingEntity == null)
- {
- throw new InvalidCastException("Cannot find the entity.");
- }
- existingEntity = entity;
- return true;
- }
- public bool Delete(T entity)
- {
- var existingEntity = GetById(entity.Id);
- if (existingEntity == null)
- {
- throw new InvalidCastException("Cannot find the entity.");
- }
- _list.Remove(entity);
- return true;
- }
下面我們給我們User領(lǐng)域?qū)嶓w的注冊方法加一個檢查Email是否存在的邏輯。
- public virtual User Register(string email, string name, string password)
- {
- if (_userRepository.Get().Any(u => u.Email == email))
- {
- throw new ArgumentException("email has already existed");
- }
- var user = new User
- {
- Id = Guid.NewGuid(),
- Email = email,
- Name = name,
- Password = password
- };
- user.CreateShoppingCart();
- _userRepository.Insert(user);
- return user;
- }
在我們真實的Repository出來之前,不管我們是打算是EF,還是NHibernate,我們現(xiàn)在只要對這個Mock的Repository來編程或者進(jìn)行單元測試就可以了。
//UserService領(lǐng)域服務(wù)在單元測試
- public class UserServiceTests
- {
- private IRepository<User> _userRepository = new MockRepository<User>();
- [Fact]
- public void RegisterUser_ExpectedParameters_Success()
- {
- var userService = new UserService(_userRepository);
- var registeredUser = userService.Register(
- "hellojesseliu@outlook.com",
- "Jesse",
- "Jesse");
- var userFromRepository = _userRepository.GetById(registeredUser.Id);
- userFromRepository.Should().NotBe(null);
- userFromRepository.Email.Should().Be("hellojesseliu@outlook.com");
- userFromRepository.Name.Should().Be("Jesse");
- userFromRepository.Password.Should().Be("Jesse");
- }
- [Fact]
- public void RegisterUser_ExistedEmail_ThrowException()
- {
- var userService = new UserService(_userRepository);
- var registeredUser = userService.Register(
- "hellojesseliu@outlook.com",
- "Jesse",
- "Jesse");
- var userFromRepository = _userRepository.GetById(registeredUser.Id);
- userFromRepository.Should().NotBe(null);
- Action action = () => userService.Register(
- "hellojesseliu@outlook.com",
- "Jesse_01",
- "Jesse");
- action.ShouldThrow<ArgumentException>();
- }
- }
我們用的XUnit.net作單元測試框架,同時用了Fluent Assertions。
結(jié)果很漂亮,有木有?有了單元測試來為我們的領(lǐng)域模型保駕護(hù)航,我們就可以安全的進(jìn)行重構(gòu)了。
干凈漂亮的代碼
經(jīng)常有人說代碼是一件藝術(shù),碼農(nóng)都是藝術(shù)家。我很喜歡這句話,如果你也認(rèn)同,那就請像對待藝術(shù)品一樣對待我們的代碼,精心的打磨它。并且你不一定要非常的有經(jīng)驗才可以干這件事情;
如果你剛?cè)胄?,那至少保證一代碼可讀性好(好的命名,代碼邏輯清晰等);
再往上一點(diǎn),你要能夠更好的組織代碼(類,函數(shù));
等到你也成為專家了,那就開始考慮一些重用性,可擴(kuò)展性,可維護(hù)性,可測試性的這些比較范的東西了;
而最后就上升到架構(gòu)層面,考慮系統(tǒng)各個組件之間通訊,分層,等等。最后你就成為碼神了。
DDD里面引入的一些思路包括分層、依懶注入、倉儲等,可以給我們一些指導(dǎo),大家從上面的代碼也可以看出這些代碼組織的很好,邏輯也不會散亂的到處都是。當(dāng)然這個項目代碼量有限,說服力是有限的,后面我們還會嘗試去加入應(yīng)用層的代碼。代碼已經(jīng)放到CodePlex上去了:http://repositoryandef.codeplex.com
歡迎大家Follow。注意代碼還沒有寫完,只是一個初級版本,我們后面會慢慢完善。這個項目會使用EF來作業(yè)ORM框架,Autofac作依懶注入容器,用Xunit作單元測試框架的同時引入了Fluent Assertions。
小結(jié)
本文主要介紹了DDD的一些基礎(chǔ)概念:
- 領(lǐng)域模型:領(lǐng)域?qū)嶓w、領(lǐng)域服務(wù)以及值對象;建模一定要從真實的領(lǐng)域業(yè)務(wù)出發(fā),多與領(lǐng)域?qū)<疫M(jìn)行溝通來完善模型。
- 聚合與聚合根:它的主要作用是用來確保各種關(guān)系下的實體的數(shù)據(jù)一致性;但是確認(rèn)聚合根這個過程,實際上也是對業(yè)務(wù)的梳理過程。
- 架構(gòu)分層: 每一層都職責(zé)清楚;依懶于接口來降低耦合。
- 封裝和測試: 所有的業(yè)務(wù)都放到領(lǐng)域?qū)樱瑫r對領(lǐng)域?qū)舆M(jìn)行單元測試來確保最核心的邏輯不會遭到破壞。
個人感覺沒有必要太強(qiáng)調(diào)Repository的概念,從領(lǐng)域?qū)嶓w的生命周期(創(chuàng)建-持久化到數(shù)據(jù)庫-銷毀-從數(shù)據(jù)庫重建)你會發(fā)現(xiàn)其實這個過程很普遍,并不是只有DDD才有的。所以我認(rèn)為Repository主要是將數(shù)據(jù)訪問功能給隔離開,避免領(lǐng)域?qū)嶓w對基礎(chǔ)設(shè)施層的依懶。那它和三層有什么區(qū)別? BLL 引用DAL不也是依懶于接口么?給我的感覺是,DDD的領(lǐng)域?qū)嶓w持久化這一塊就是三層里面的思路。這可能是在學(xué)習(xí)DDD初期的想法,因為真實的大型項目中是不會直接把領(lǐng)域?qū)嶓w給持久化的,那個叫DTO,于是Repository<>里面放的就不是我們的領(lǐng)域?qū)嶓w了,而是將領(lǐng)域?qū)嶓w轉(zhuǎn)換成對應(yīng)的DTO。
是否一定要使用DTO呢?領(lǐng)域?qū)嶓w和DTO互相轉(zhuǎn)換,最后到了表現(xiàn)層DTO還要和ViewModel轉(zhuǎn)換,會不會帶來復(fù)雜性和性能上的損失?Repository和EF還有Unit Of Work怎么來協(xié)調(diào)?抱怨寫單元測試么?怎么樣讓寫單元測試不變成只是走過場而已? 這些問題留給我們后面再解決吧。
本文出自自:http://www.cnblogs.com/jesse2013/p/the-first-glance-of-ddd.html