初探領(lǐng)域驅(qū)動(dòng)設(shè)計(jì):為復(fù)雜業(yè)務(wù)而生
概述
領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)也就是3D(Domain-Driven Design)已經(jīng)有了10年的歷史,我相信很多人或多或少都聽(tīng)說(shuō)過(guò)這個(gè)名詞,但是有多少人真正懂得如何去運(yùn)用它,或者把它運(yùn)用好呢?于是有人說(shuō),DDD和TDD這些玩意是一些形而上的東西,只是一茶余飯后的談資,又或是放到簡(jiǎn)歷上提升逼格而已。前面這句話(huà)我寫(xiě)完之后猶豫了,猶豫要不要把它刪掉,因?yàn)樗屛铱雌饋?lái)像個(gè)噴子,我確實(shí)感到不解,為什么別人10年前創(chuàng)造總結(jié)出來(lái)的東西,我們?cè)?0年之后對(duì)它的理解還處于這么低的一個(gè)層次。開(kāi)篇就說(shuō)遠(yuǎn)了,我也是最近才開(kāi)始認(rèn)真學(xué)習(xí)領(lǐng)域驅(qū)動(dòng)設(shè)計(jì),并且得到了園子里面netfocus,劉標(biāo)才和田園里的蟋蟀的幫助,在此再次表示感謝。希望能和大家一起把DDD普及下去。
我們之前有一個(gè)關(guān)于領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的討論,另外dax.net也有一個(gè)關(guān)于領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的系列寫(xiě)得不錯(cuò),有興趣的同學(xué)可以看看。本文會(huì)以一個(gè)初學(xué)者的角度來(lái)講解DDD,讓我們一切從零開(kāi)始,我相信你跟我一樣也會(huì)愛(ài)上它的。
本篇主要討論一下為什么我們要用DDD,它能夠?yàn)槲覀儙?lái)什么?
一點(diǎn)廢話(huà) ,我們需要好的設(shè)計(jì)么?
當(dāng)我們學(xué)習(xí)一些設(shè)計(jì)模式或者框架的時(shí)候,總有人會(huì)站出來(lái)和你說(shuō)“這些都沒(méi)有用,只要能實(shí)現(xiàn)功能就行了。” 在這里并非針對(duì)某個(gè)人,實(shí)際上我認(rèn)為他們說(shuō)的是對(duì)的,在資源有限的情況下,我們?yōu)榱送瓿身?xiàng)目的交付,這是我們最好的選擇。但是別忘了,欠下的債總是要還的,以實(shí)現(xiàn)功能為導(dǎo)向的項(xiàng)目務(wù)必會(huì)造成維護(hù)性的大大降低,如果只是一個(gè)臨時(shí)隨便用用的東西倒是可以一試,但如果是要長(zhǎng)期進(jìn)行更新的產(chǎn)品,那后期就會(huì)拖該產(chǎn)品的后腿。
我們團(tuán)隊(duì)現(xiàn)在維護(hù)著一個(gè)有著20多年歷史的產(chǎn)品,該產(chǎn)品是一個(gè)酒店、餐飲行業(yè)的POS系統(tǒng),在美國(guó)和亞太地區(qū)都占有著比較大的市場(chǎng)份額。該產(chǎn)品從C,C++,VB6一路更新,直到現(xiàn)在的C#,但是很可惜不是整體替換,而是局部的,所以現(xiàn)在項(xiàng)目里面這4種代碼全都有。可能你會(huì)覺(jué)得這玩的是混搭,是潮流,但事實(shí)是,一旦產(chǎn)品上線(xiàn)之后,會(huì)有很多的新功能,老bug等在那里,再加上“重市場(chǎng)輕技術(shù)”的高層在那里制訂戰(zhàn)略,你壓根就沒(méi)有時(shí)間或者沒(méi)有多少時(shí)間去重構(gòu)。日積月累,等著你的就是每一次改代碼都如履薄冰,一不小心就因?yàn)楦囊粋€(gè)bug而整出好幾個(gè)新bug出來(lái),前不久我們?yōu)榱诵掳姹镜陌l(fā)布就停下所有開(kāi)發(fā)的任務(wù),大家集體花了1個(gè)月的時(shí)間去做回歸測(cè)試了。因?yàn)榍捌诎l(fā)布新版本之后bug太多,所以這次老大們都不敢輕易發(fā)布了。:)
這是我們血的教訓(xùn),如果你前期只顧開(kāi)發(fā)功能,最后就會(huì)讓你很難再開(kāi)發(fā)新功能。所以真誠(chéng)的希望大家不要再片面的說(shuō)“只要實(shí)現(xiàn)功能就可以了!”,軟件開(kāi)發(fā)的領(lǐng)域這么大,我們沒(méi)有必要把自己局限在某一個(gè)框框里面。對(duì)于大型系統(tǒng)來(lái)說(shuō),我們要學(xué)習(xí)的地方還有很多:
- 組織良好、可閱讀性高的代碼可以讓其它開(kāi)發(fā)人員很容易的開(kāi)始去修改代碼。
- 低耦合,高內(nèi)聚 - 適合運(yùn)用設(shè)計(jì)模式以及原則來(lái)設(shè)計(jì)一些好的框架可以降低修改代碼引發(fā)新bug的風(fēng)險(xiǎn)。
- 良好的單元測(cè)試以及集成測(cè)試可以及時(shí)的幫助我們檢測(cè)新增或修改的代碼是否會(huì)破壞原有的邏輯。
- 自動(dòng)化測(cè)試絕對(duì)是省時(shí)省力的好幫手,也是項(xiàng)目質(zhì)量的保證。
- 持續(xù)集成可以幫助我們更快速安全的進(jìn)行迭代。
上面說(shuō)了這么多也沒(méi)有提到DDD,那么為什么它能夠在構(gòu)建復(fù)雜系統(tǒng)的時(shí)候有優(yōu)勢(shì)呢?我們可以從以下幾個(gè)點(diǎn)去思考:
- 從設(shè)計(jì)階段出發(fā),站在業(yè)務(wù)的角度思考問(wèn)題
- 厘清業(yè)務(wù)主次
- 獨(dú)立領(lǐng)域業(yè)務(wù)層,打通開(kāi)發(fā)和測(cè)試階段
- 干凈的代碼
從設(shè)計(jì)階段開(kāi)始,站在業(yè)務(wù)的角度思考問(wèn)題
除了DDD,現(xiàn)在還流行另外一個(gè)詞匯TDD。但是不知道大家有沒(méi)有注意到DDD(Domain-Driven Design)中的D代表著設(shè)計(jì),而TDD(Test-Driven Development)中的D代表著開(kāi)發(fā),你有沒(méi)有曾幾何時(shí)把領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)說(shuō)成領(lǐng)域驅(qū)動(dòng)開(kāi)發(fā)呢?當(dāng)然我們確實(shí)是可以根據(jù)領(lǐng)域驅(qū)動(dòng)來(lái)開(kāi)發(fā),但是DDD被設(shè)計(jì)出來(lái)的完美初衷卻是設(shè)計(jì)。TDD強(qiáng)調(diào)的已經(jīng)是開(kāi)發(fā)了,要求開(kāi)發(fā)人員先寫(xiě)單元測(cè)試然后再通過(guò)不斷的迭代重構(gòu)讓單元測(cè)試通過(guò),以此來(lái)實(shí)現(xiàn)功能。這樣做的好處是強(qiáng)迫讓開(kāi)發(fā)人員清楚正確的理解需求,要知道這年頭沒(méi)有正確理解需求就開(kāi)始寫(xiě)代碼的程序員大有人在,并且我不認(rèn)為需求就是業(yè)務(wù),需求已經(jīng)是將本來(lái)的業(yè)務(wù)理解之后,轉(zhuǎn)化為了通過(guò)計(jì)算機(jī)可以實(shí)現(xiàn)的一些功能定義,通常是業(yè)務(wù)分析師或者項(xiàng)目經(jīng)理會(huì)去完成這個(gè)工作。而DDD中的D(領(lǐng)域)更像是本來(lái)的業(yè)務(wù),所以在領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的時(shí)候,開(kāi)發(fā)人員或者架構(gòu)師直接與領(lǐng)域?qū)<遥ɑ蛘哒f(shuō)客戶(hù))進(jìn)行溝通來(lái)建模,這些業(yè)務(wù)模型也是以后開(kāi)發(fā)人員進(jìn)行設(shè)計(jì)和實(shí)現(xiàn)的依據(jù)。
領(lǐng)域模型被當(dāng)作開(kāi)發(fā)人員之間,開(kāi)發(fā)人員與領(lǐng)域?qū)<抑g溝通的橋梁,這樣可以閉免開(kāi)發(fā)人員用錯(cuò)誤的方式去實(shí)現(xiàn)功能。實(shí)際上很多優(yōu)秀的開(kāi)發(fā)人員,都會(huì)很自然的將現(xiàn)實(shí)世界中的問(wèn)題進(jìn)行抽象,然后用計(jì)算機(jī)的語(yǔ)言表示出來(lái),我們稱(chēng)之為面向?qū)ο蟆5怯捎谌鄙儆H臨其境的體驗(yàn),往往會(huì)離真實(shí)的業(yè)務(wù)模型有一些距離。
我們舉一個(gè)例子來(lái)說(shuō)明一下這個(gè)問(wèn)題,假如我們要開(kāi)發(fā)一個(gè)電子商務(wù)的網(wǎng)站,這個(gè)需求已經(jīng)非常清楚了,現(xiàn)在那么多的電子商務(wù)網(wǎng)站直接照抄一個(gè)就可以了。現(xiàn)在我們來(lái)做一個(gè)下單的功能,來(lái)看看怎么去實(shí)現(xiàn) 。
作為一個(gè)高級(jí)程序員,我們得用面向?qū)ο蟮姆绞饺ラ_(kāi)發(fā),先建類(lèi)。于是我們有了用戶(hù),訂單,訂單項(xiàng)的類(lèi),用戶(hù)創(chuàng)建訂單然后往訂單里面添加商品,添加訂單項(xiàng)的時(shí)候?yàn)榱朔奖悖覀冎恍枰獋魅氘a(chǎn)品ID和數(shù)量就可以了,于是Order類(lèi)有一個(gè)AddItem的方法。
作為一個(gè)高級(jí)程序員,一看這圖感覺(jué)很完美,有木有? 好,下面開(kāi)始實(shí)現(xiàn)AddItem方法。
Order里面是一個(gè)OrderItem的集合,而這個(gè)AddItem的方法接收的是productId,我去哪里搞個(gè)Product對(duì)象給你?我不可能在這個(gè)實(shí)體里面直接去查數(shù)據(jù)庫(kù)吧?本來(lái)是沖著這個(gè)技術(shù)點(diǎn)想咨詢(xún)一下大家,后來(lái)在小組里面討論了一下,我恍然大悟,上面的實(shí)體就是我從代碼的層面去思考想出來(lái)的,下單嘛,當(dāng)然是用戶(hù),訂單和訂單項(xiàng)嘍。可是只要去網(wǎng)上買(mǎi)過(guò)東西都知道,用戶(hù)是不會(huì)直接往訂單里面加?xùn)|西的,而是先把商品加入購(gòu)物車(chē),然后再通過(guò)“結(jié)算”一次性就根據(jù)購(gòu)物車(chē)生成了一張訂單,壓根沒(méi)有往訂單里面添加訂單項(xiàng)的行為。這才是真正的用戶(hù)行為(領(lǐng)域邏輯)所以后來(lái),我們的實(shí)體變成這樣了:
所以業(yè)務(wù)是這樣的:
- 未注冊(cè)用戶(hù)也可以將商品添加到購(gòu)物車(chē)中,但是不能下訂單。
- 并且購(gòu)物車(chē)中的商品不能保存起來(lái),用戶(hù)離開(kāi)這個(gè)網(wǎng)站(一般是關(guān)掉瀏覽器),購(gòu)物車(chē)中的商品就會(huì)消失。
- 注冊(cè)用戶(hù)購(gòu)物車(chē)中的商品可以長(zhǎng)期永久保存,通過(guò)購(gòu)物車(chē)的“結(jié)算功能”,將購(gòu)物車(chē)中選中的商品轉(zhuǎn)化為訂單。
- 所以購(gòu)物車(chē),應(yīng)該在用戶(hù)注冊(cè)的時(shí)候就應(yīng)該創(chuàng)建好,對(duì)應(yīng)我們上面的User實(shí)體中的CreatShoppingCart()方法。下面我們先來(lái)簡(jiǎn)單實(shí)現(xiàn)一下注冊(cè)的代碼。
//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;
- }
- }
- }
上面是我們一次建模的過(guò)程,是一個(gè)將業(yè)務(wù)轉(zhuǎn)變成代碼,將現(xiàn)實(shí)世界抽象成軟件世界的過(guò)程。我們需要畫(huà)出模型不斷的與業(yè)務(wù)人員(領(lǐng)域?qū)<遥┤贤ǎ缓蟛粩嗟闹貥?gòu)去完善我們的模型,以至于這個(gè)模型能最準(zhǔn)確的反映真實(shí)的業(yè)務(wù)。這是在最開(kāi)始的設(shè)計(jì)階段,是需求溝通階段就需要做的工作,并且會(huì)一直貫穿我們后面的開(kāi)發(fā)甚至維護(hù)階段,沒(méi)有人可以一開(kāi)始就把領(lǐng)域模型建的100%準(zhǔn)確,需求是復(fù)雜的,并且需求還是隨時(shí)變化的,所以模型也會(huì)一直發(fā)生改變。它將作為開(kāi)發(fā)人員與業(yè)務(wù)人員、測(cè)試人員以及開(kāi)發(fā)人員自己之間溝通的橋梁。而DDD與其它方法論的區(qū)別之處就在于,它還提供了一整套的體系來(lái)保證后續(xù)對(duì)領(lǐng)域模型的重構(gòu)不會(huì)讓系統(tǒng)變得四分五裂,比如架構(gòu)分層,倉(cāng)儲(chǔ),依懶注入等等,我們后面再慢慢探討。
在DDD中,領(lǐng)域模型分為三種:
- 實(shí)體
- 值對(duì)象
- 領(lǐng)域服務(wù)
區(qū)分實(shí)體、值對(duì)象和領(lǐng)域服務(wù)
我們不打算去解釋以上的概念,我相信只要你搜索一下就可以得到很全面準(zhǔn)確的答案。但是重要的是我們一定要理解3者之間的區(qū)別,什么時(shí)候是實(shí)體,什么時(shí)候是值對(duì)象,又是什么時(shí)候我們?cè)撚妙I(lǐng)域服務(wù)呢?我想這是剛接觸DDD的人都難免會(huì)有些糾結(jié)的地方吧,在這里就強(qiáng)調(diào)一下。
實(shí)體相對(duì)于值對(duì)象而言擁有“標(biāo)識(shí)”的概念,標(biāo)識(shí)可以讓我們持續(xù)性的跟蹤實(shí)體。標(biāo)識(shí)和數(shù)據(jù)庫(kù)里面的“主鍵”是不一樣的概念,主鍵是技術(shù)上的概念,但是標(biāo)識(shí)是業(yè)務(wù)上的概念。
在我們上面的例子中用戶(hù)ID是標(biāo)識(shí),我們用它來(lái)持續(xù)性的跟蹤我們的用戶(hù)。訂單ID是標(biāo)識(shí),我們用它來(lái)持續(xù)性的跟蹤訂單,同時(shí)我們的用戶(hù)和訂單都是有著不同的狀態(tài)。但是對(duì)于用戶(hù)的地址來(lái)說(shuō),我們用什么來(lái)做標(biāo)識(shí)呢?在電子商務(wù)網(wǎng)站這樣的業(yè)務(wù)里面,我們不需要去持續(xù)的跟蹤這個(gè)地址信息,它在我們的系統(tǒng)里面也不會(huì)有著像訂單從“創(chuàng)建”、“已付款”、“已發(fā)貨”、“已收貨”等這樣的狀態(tài),所以地址信息的我們系統(tǒng)中就是一個(gè)值對(duì)象。
但是我們?nèi)绻麚Q了一個(gè)系統(tǒng),比如說(shuō)死慢的長(zhǎng)城寬帶,他們把地址作為跟蹤對(duì)象。同一個(gè)地址,誰(shuí)都可以去注冊(cè),但是同一個(gè)時(shí)間只允許一個(gè)人去注冊(cè),那么這個(gè)地址對(duì)于長(zhǎng)城寬帶來(lái)說(shuō)就去要去持續(xù)性的跟蹤,有“開(kāi)戶(hù)”,“銷(xiāo)戶(hù)”的狀態(tài)。那么地址信息對(duì)于長(zhǎng)城寬帶來(lái)說(shuō)就是一個(gè)實(shí)體。
解決完實(shí)體和值對(duì)象,領(lǐng)域服務(wù)就好說(shuō)了,一些重要的領(lǐng)域操作,既不屬于實(shí)體也不屬于值對(duì)象,那就可以把它放到服務(wù)中了。比如說(shuō)我們上面的領(lǐng)域服務(wù)UserService里面的注冊(cè)操作,注冊(cè)這個(gè)操作可以說(shuō)就是將這個(gè)用戶(hù)保存到我們的系統(tǒng)中。在注冊(cè)之間,這個(gè)用戶(hù)是不存在的,我們又怎么能把注冊(cè)這個(gè)操作放到User實(shí)體中去呢?所以把它放到領(lǐng)域服務(wù)中成了我們最好的選擇。
即使是這樣,哪些操作應(yīng)該放到領(lǐng)域服務(wù)中對(duì)于很多初學(xué)者來(lái)說(shuō)還是一件比較難選擇的問(wèn)題。也許只有慢慢的對(duì)業(yè)務(wù)越來(lái)越了解,對(duì)DDD應(yīng)用的越來(lái)越熟,我們就會(huì)少一點(diǎn)糾結(jié)。
#p#
厘清業(yè)務(wù)主次-聚合與聚合根
在上面的模型中,我們有很多關(guān)系的存在:用戶(hù)-購(gòu)物車(chē)(1對(duì)1),用戶(hù)-訂單-訂單項(xiàng)-產(chǎn)品(1對(duì)多,1對(duì)1),購(gòu)物車(chē)-購(gòu)物車(chē)項(xiàng)-產(chǎn)品等。在DDD中,我們把這樣多個(gè)模型用關(guān)聯(lián)串起來(lái)組成一個(gè)聚合(aggregation)。
在我們的模型中,購(gòu)物車(chē)-購(gòu)物車(chē)項(xiàng)是一個(gè)聚合,訂單-訂單項(xiàng)是一個(gè)聚合。我們通常需要保護(hù)這些聚合的一致性,比如說(shuō)我們把一個(gè)訂單刪掉了,那么這個(gè)訂單的訂單項(xiàng)也需要一起刪除,否則他們存在也沒(méi)有任何的意義。以前我們還會(huì)用到觸發(fā)器,但是大家都知道這個(gè)東西維護(hù)起來(lái)比較麻煩,寫(xiě)起來(lái)也不方便等,所以后來(lái)大家都是在代碼中來(lái)控制。但是一直沒(méi)有一個(gè)好的約束說(shuō)我們?nèi)绾稳ジ玫目刂七@些一致性,代碼一直都很散亂,直到DDD,我們有了聚合和聚合根的概念,“我們通過(guò)為每一個(gè)聚合選擇一個(gè)根,并通過(guò)根來(lái)控制所有對(duì)邊界內(nèi)的對(duì)象的訪問(wèn)。外部對(duì)象只能持有根的引用;由于根控制了訪問(wèn),因此我們無(wú)法繞過(guò)它去修改內(nèi)部元素。我們后面還會(huì)說(shuō)到只能為根來(lái)建立Repository,這也是為了確保我們這里面講的數(shù)據(jù)的一致性。
在我們上面的聚合中,只能通過(guò)購(gòu)物車(chē)實(shí)體來(lái)操作購(gòu)物車(chē)項(xiàng),而不能你自己寫(xiě)一個(gè)保存的方法直接就把購(gòu)物車(chē)項(xiàng)給保存到數(shù)據(jù)庫(kù)中去了。這就是聚合和聚合根起到的作用。我們來(lái)看一下我們購(gòu)物車(chē)實(shí)體的代碼:
- 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)存在于購(gòu)物車(chē)中,我們直接更改數(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,
- });
- }
- // 更改購(gòu)物車(chē)數(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;
- }
- // 從購(gòu)物車(chē)中移除該產(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
- }
- }
大家可以看到我們購(gòu)物車(chē)實(shí)體的邏輯很清晰,因?yàn)槲覀兒苊鞔_購(gòu)物車(chē)擁有哪些操作。當(dāng)然還有另一種做法即把這些操作都放到用戶(hù)實(shí)體中去,因?yàn)樽罱K其實(shí)是用戶(hù)做的這些操作。那我們的聚合就變成了用戶(hù)-購(gòu)物車(chē)-購(gòu)物車(chē)項(xiàng),這樣也沒(méi)有什么不可以,反而更符合真實(shí)的場(chǎng)景。但是會(huì)導(dǎo)致我們的聚合過(guò)龐大,也就是說(shuō)我必須要先有用戶(hù)實(shí)體才能進(jìn)行操作,用戶(hù)用戶(hù)可能會(huì)綁上很多的東西:購(gòu)物車(chē)、訂單、地址等等。在現(xiàn)在都是ajax來(lái)操作的大型網(wǎng)站中,我們需要在服務(wù)端把這個(gè)用戶(hù)請(qǐng)求加載出來(lái)再執(zhí)行添加購(gòu)物車(chē)的操作呢?還是可以直接加載購(gòu)物車(chē)實(shí)體來(lái)操作呢?這就是一個(gè)粒度的問(wèn)題,不同的問(wèn)題和場(chǎng)景,大家可以區(qū)別來(lái)對(duì)待。總之聚合是可以根據(jù)業(yè)務(wù)或者一些特定需求來(lái)做出調(diào)整的。比如說(shuō)購(gòu)物車(chē)-購(gòu)物車(chē)項(xiàng)-產(chǎn)品,這也是一個(gè)聚合,但是由于產(chǎn)品的特殊性,我們可以把產(chǎn)品也作為一個(gè)聚合根來(lái)單獨(dú)進(jìn)行訪問(wèn)。
我們來(lái)看一下應(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)用層代碼一出,大家就會(huì)發(fā)現(xiàn),這代碼太簡(jiǎn)潔了,有木有?因?yàn)樗械倪壿嫛I(yè)務(wù)都被放到領(lǐng)域?qū)嶓w那里面去處理了。即使我們業(yè)務(wù)邏輯改變了,或者我們需要重構(gòu)了,它們都在領(lǐng)域?qū)嶓w那里面,改那里就好了。接下來(lái)的問(wèn)題是,如何確保安全,正確的一次又一次的對(duì)領(lǐng)域?qū)嶓w進(jìn)行重構(gòu)呢?畢竟它也是各種關(guān)聯(lián),各種依懶呀?您請(qǐng)接著往下看我們的單元測(cè)試環(huán)節(jié)。
#p#
獨(dú)立領(lǐng)域業(yè)務(wù)層 - 高內(nèi)聚,低耦合,可測(cè)試
講到這里,請(qǐng)?jiān)试S我從網(wǎng)上盜一張圖,當(dāng)然這張圖早就已經(jīng)是被引用過(guò)無(wú)數(shù)次了,它就是DDD中使用的分層結(jié)構(gòu)。
關(guān)于這個(gè)分層,每一層是干什么的,具體怎么玩,大家可以看一下dax的這一篇文章講解的很清楚。總之,我們的領(lǐng)域模型以及相關(guān)的類(lèi)比如工廠等會(huì)被獨(dú)立成為一層來(lái)與應(yīng)用層和基礎(chǔ)設(shè)計(jì)層交互。
領(lǐng)域?qū)邮仟?dú)立的,首先它是應(yīng)用層的下層,所以肯定不會(huì)有對(duì)應(yīng)用層的依懶,但是領(lǐng)域有一些模型或者服務(wù)少不了是要與數(shù)據(jù)庫(kù)打交道的,比如說(shuō)我們?cè)谧?cè)用戶(hù)的時(shí)候需要去驗(yàn)證當(dāng)前的郵箱是不是已經(jīng)被占用了。而這一類(lèi)操作都是屬于基礎(chǔ)設(shè)施層做的事情,包含像一些數(shù)據(jù)庫(kù)操作,日志,緩存等等。那么我們?nèi)绾伪苊忸I(lǐng)域?qū)訉?duì)基礎(chǔ)設(shè)施層的依懶呢?感謝面向?qū)ο笤O(shè)計(jì) - 面向接口編程,只不過(guò)這里面的場(chǎng)景特別有代表性,它是一個(gè)非常常見(jiàn)的問(wèn)題,于是它成為了一個(gè)模式:倉(cāng)儲(chǔ)(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);
- }
- }
一般情況下,我們會(huì)把倉(cāng)儲(chǔ)的接口放到領(lǐng)域?qū)樱蛘咭部梢栽俳ㄒ粋€(gè)Core層來(lái)作個(gè)項(xiàng)目最下面的那一層提供一些最公共的組件部分。關(guān)于倉(cāng)儲(chǔ)的代碼,大家在上面領(lǐng)域服務(wù)UserService中的注冊(cè)代碼中就已經(jīng)見(jiàn)到過(guò)了。可能需要注意的是,Repository用來(lái)將數(shù)據(jù)庫(kù)與其它的業(yè)務(wù)和技術(shù)分離,所以我們?cè)陬I(lǐng)域?qū)又惺褂盟€在應(yīng)用層中使用它。
Repository讓我們專(zhuān)注于模型,不用去考慮持久化的問(wèn)題。更為重要的一點(diǎn)是,因?yàn)樗墙涌冢晕覀兛梢院芊奖愕奶娲蛘吣M一個(gè)實(shí)現(xiàn)來(lái)對(duì)我們的領(lǐng)域模型進(jìn)行單元測(cè)試。下面是我們實(shí)現(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的注冊(cè)方法加一個(gè)檢查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;
- }
在我們真實(shí)的Repository出來(lái)之前,不管我們是打算是EF,還是NHibernate,我們現(xiàn)在只要對(duì)這個(gè)Mock的Repository來(lái)編程或者進(jìn)行單元測(cè)試就可以了。
//UserService領(lǐng)域服務(wù)在單元測(cè)試
- 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作單元測(cè)試框架,同時(shí)用了Fluent Assertions。
結(jié)果很漂亮,有木有?有了單元測(cè)試來(lái)為我們的領(lǐng)域模型保駕護(hù)航,我們就可以安全的進(jìn)行重構(gòu)了。
干凈漂亮的代碼
經(jīng)常有人說(shuō)代碼是一件藝術(shù),碼農(nóng)都是藝術(shù)家。我很喜歡這句話(huà),如果你也認(rèn)同,那就請(qǐng)像對(duì)待藝術(shù)品一樣對(duì)待我們的代碼,精心的打磨它。并且你不一定要非常的有經(jīng)驗(yàn)才可以干這件事情;
如果你剛?cè)胄校侵辽俦WC一代碼可讀性好(好的命名,代碼邏輯清晰等);
再往上一點(diǎn),你要能夠更好的組織代碼(類(lèi),函數(shù));
等到你也成為專(zhuān)家了,那就開(kāi)始考慮一些重用性,可擴(kuò)展性,可維護(hù)性,可測(cè)試性的這些比較范的東西了;
而最后就上升到架構(gòu)層面,考慮系統(tǒng)各個(gè)組件之間通訊,分層,等等。最后你就成為碼神了。
DDD里面引入的一些思路包括分層、依懶注入、倉(cāng)儲(chǔ)等,可以給我們一些指導(dǎo),大家從上面的代碼也可以看出這些代碼組織的很好,邏輯也不會(huì)散亂的到處都是。當(dāng)然這個(gè)項(xiàng)目代碼量有限,說(shuō)服力是有限的,后面我們還會(huì)嘗試去加入應(yīng)用層的代碼。代碼已經(jīng)放到CodePlex上去了:http://repositoryandef.codeplex.com
歡迎大家Follow。注意代碼還沒(méi)有寫(xiě)完,只是一個(gè)初級(jí)版本,我們后面會(huì)慢慢完善。這個(gè)項(xiàng)目會(huì)使用EF來(lái)作業(yè)ORM框架,Autofac作依懶注入容器,用Xunit作單元測(cè)試框架的同時(shí)引入了Fluent Assertions。
小結(jié)
本文主要介紹了DDD的一些基礎(chǔ)概念:
- 領(lǐng)域模型:領(lǐng)域?qū)嶓w、領(lǐng)域服務(wù)以及值對(duì)象;建模一定要從真實(shí)的領(lǐng)域業(yè)務(wù)出發(fā),多與領(lǐng)域?qū)<疫M(jìn)行溝通來(lái)完善模型。
- 聚合與聚合根:它的主要作用是用來(lái)確保各種關(guān)系下的實(shí)體的數(shù)據(jù)一致性;但是確認(rèn)聚合根這個(gè)過(guò)程,實(shí)際上也是對(duì)業(yè)務(wù)的梳理過(guò)程。
- 架構(gòu)分層: 每一層都職責(zé)清楚;依懶于接口來(lái)降低耦合。
- 封裝和測(cè)試: 所有的業(yè)務(wù)都放到領(lǐng)域?qū)樱瑫r(shí)對(duì)領(lǐng)域?qū)舆M(jìn)行單元測(cè)試來(lái)確保最核心的邏輯不會(huì)遭到破壞。
個(gè)人感覺(jué)沒(méi)有必要太強(qiáng)調(diào)Repository的概念,從領(lǐng)域?qū)嶓w的生命周期(創(chuàng)建-持久化到數(shù)據(jù)庫(kù)-銷(xiāo)毀-從數(shù)據(jù)庫(kù)重建)你會(huì)發(fā)現(xiàn)其實(shí)這個(gè)過(guò)程很普遍,并不是只有DDD才有的。所以我認(rèn)為Repository主要是將數(shù)據(jù)訪問(wèn)功能給隔離開(kāi),避免領(lǐng)域?qū)嶓w對(duì)基礎(chǔ)設(shè)施層的依懶。那它和三層有什么區(qū)別? BLL 引用DAL不也是依懶于接口么?給我的感覺(jué)是,DDD的領(lǐng)域?qū)嶓w持久化這一塊就是三層里面的思路。這可能是在學(xué)習(xí)DDD初期的想法,因?yàn)檎鎸?shí)的大型項(xiàng)目中是不會(huì)直接把領(lǐng)域?qū)嶓w給持久化的,那個(gè)叫DTO,于是Repository<>里面放的就不是我們的領(lǐng)域?qū)嶓w了,而是將領(lǐng)域?qū)嶓w轉(zhuǎn)換成對(duì)應(yīng)的DTO。
是否一定要使用DTO呢?領(lǐng)域?qū)嶓w和DTO互相轉(zhuǎn)換,最后到了表現(xiàn)層DTO還要和ViewModel轉(zhuǎn)換,會(huì)不會(huì)帶來(lái)復(fù)雜性和性能上的損失?Repository和EF還有Unit Of Work怎么來(lái)協(xié)調(diào)?抱怨寫(xiě)單元測(cè)試么?怎么樣讓寫(xiě)單元測(cè)試不變成只是走過(guò)場(chǎng)而已? 這些問(wèn)題留給我們后面再解決吧。
本文出自自:http://www.cnblogs.com/jesse2013/p/the-first-glance-of-ddd.html