Repository 仓储,你的归宿究竟在哪?(一)-仓储的概念
寫在前面
寫這篇博文的靈感來自《如何開始DDD(完)》,很感謝young.han兄這幾天的堅持,陸陸續(xù)續(xù)寫了幾篇有關(guān)于領(lǐng)域驅(qū)動設(shè)計的博文,讓園中再次刮了一陣“DDD探討風(fēng)”,我現(xiàn)在不像前段時間那樣“瘋狂”了,寫博文需要靈感,就像這篇一樣。那篇博文除去其他的一些問題探討,留給我印象最深的就是:領(lǐng)域服務(wù)中使用倉儲,下面摘自文中我的一段評論:
文中Luminji兄這樣回復(fù)我:“領(lǐng)域服務(wù)不用倉儲,那我們怎么單元測試領(lǐng)域服務(wù)?僅此一點(diǎn),就說明領(lǐng)域服務(wù)必用倉儲。反之,倒是上層,如控制器這里不應(yīng)該用倉儲。”其實(shí)原本大家的焦點(diǎn)不應(yīng)該放在倉儲上面的,而應(yīng)該放在領(lǐng)域驅(qū)動設(shè)計的核心-領(lǐng)域模型上,為此我還曾寫了幾篇關(guān)于領(lǐng)域模型設(shè)計的博文,但是一個完整的應(yīng)用程序不只是包含領(lǐng)域模型,還有其他的東西需要進(jìn)行探討,雖然它不像領(lǐng)域模型那么重要,但同樣必不可少。
Luminji兄的評論,讓我意識到需要把領(lǐng)域驅(qū)動設(shè)計中的其他概念明確探討下了,如果對一些概念模糊不清,或者不能很好的明確其職責(zé),這樣就很容易導(dǎo)致我們在領(lǐng)域驅(qū)動設(shè)計的過程中陷入一些困境,就像我之前所掉進(jìn)的坑-《設(shè)計窘境:來自 Repository 的一絲線索,Domain Model 再重新設(shè)計》。
以下內(nèi)容只是個人對倉儲概念及其問題進(jìn)行探討,并非是結(jié)論總結(jié),僅供各位仁兄參考。
《實(shí)現(xiàn)領(lǐng)域驅(qū)動設(shè)計》
在進(jìn)行正文探討之前,我先啰嗦幾句。
《實(shí)現(xiàn)領(lǐng)域驅(qū)動設(shè)計》這本書,我在之前覺得沒必要閱讀,因?yàn)楫?dāng)時認(rèn)為學(xué)習(xí)領(lǐng)域驅(qū)動設(shè)計,只要精讀下 Eric Evans 的經(jīng)典著作《領(lǐng)域驅(qū)動設(shè)計-軟件核心復(fù)雜性應(yīng)對之道》就可以了,但是DDD是需要進(jìn)行實(shí)踐的,Eric Evans 只是提出領(lǐng)域驅(qū)動設(shè)計這個概念,有關(guān)于其實(shí)現(xiàn),書中并沒有花很大的精力去講解,而《實(shí)現(xiàn)領(lǐng)域驅(qū)動設(shè)計》這本書正是彌補(bǔ)了這一點(diǎn)。
這兩本書的閱讀順序,當(dāng)然是先閱讀《領(lǐng)域驅(qū)動設(shè)計》,然后再閱讀《實(shí)現(xiàn)領(lǐng)域驅(qū)動設(shè)計》,如果你是第一次讀第一本書,它會顛覆你對軟件設(shè)計的一些看法,然后讓你不能自拔的“愛上它”,不知道你有沒有,反正我是這樣,然后你在做一些應(yīng)用程序設(shè)計的時候,會嘗試使用領(lǐng)域驅(qū)動設(shè)計,雖然有些步履蹣跚,但是走出第一步是很重要的。關(guān)于閱讀第二本書,我的建議是,在閱讀之前,先根據(jù)第一本書中的指導(dǎo),自己嘗試去實(shí)踐領(lǐng)域驅(qū)動設(shè)計,最好是做一些實(shí)際業(yè)務(wù)場景的應(yīng)用,在這個過程中,完全按照自己對領(lǐng)域驅(qū)動設(shè)計的想法去實(shí)現(xiàn),雖然可能會掉進(jìn)一些深坑,但是我覺得只有這樣你才會理解的更加深刻。至于為什么自己實(shí)踐過領(lǐng)域驅(qū)動設(shè)計再去閱讀第二本書?因?yàn)閷?shí)踐過后閱讀的話,你會與作者產(chǎn)生一些共鳴,這是很奇妙的感覺,就像譯者-騰云這樣所說:
《實(shí)現(xiàn)領(lǐng)域驅(qū)動設(shè)計》這本書,我現(xiàn)在也只讀了第十二章-資源庫(譯者把 Repository 翻譯為資源庫,和倉儲是一個意思,我更喜歡倉儲這個名詞,后面就用它來表示 Repository 了),閱讀倉儲這一章的時候,我是帶著問題進(jìn)行閱讀的,也就是倉儲的職責(zé)是什么?它的歸宿究竟在哪?但是很可惜,我在這一章節(jié)中并沒有找到我要尋找的答案,因?yàn)樽髡咧饕v解的是倉儲的實(shí)現(xiàn),但是我發(fā)現(xiàn)了其他一些有意思的東西,下面希望和各位仁兄分享下(或許有點(diǎn)偏離主題了,但是我覺得應(yīng)該會蠻有意義的)。
倉儲(Repository) VS 數(shù)據(jù)訪問對象(DAO)
有關(guān)于倉儲的概念,我不止在一篇博文中進(jìn)行說明,但是這邊既然和數(shù)據(jù)訪問對象進(jìn)行比較的話,還是要聲明一下,下面來自《領(lǐng)域驅(qū)動設(shè)計》書中的定義:
Repository(倉儲):協(xié)調(diào)領(lǐng)域和數(shù)據(jù)映射層,利用類似與集合的接口來訪問領(lǐng)域?qū)ο蟆?/p>
也可以像 dudu 這樣進(jìn)行直白的理解:Repository 是一個獨(dú)立的層,介于領(lǐng)域?qū)优c數(shù)據(jù)映射層(數(shù)據(jù)訪問層)之間。它的存在讓領(lǐng)域?qū)痈杏X不到數(shù)據(jù)訪問層的存在,它提供一個類似集合的接口提供給領(lǐng)域?qū)舆M(jìn)行領(lǐng)域?qū)ο蟮脑L問。Repository 是倉庫管理員,領(lǐng)域?qū)有枰裁礀|西只需告訴倉庫管理員,由倉庫管理員把東西拿給它,并不需要知道東西實(shí)際放在哪。
倉儲是領(lǐng)域驅(qū)動設(shè)計中產(chǎn)生的概念,也就是說,如果你的應(yīng)用程序不是基于領(lǐng)域驅(qū)動設(shè)計的,那在設(shè)計中使用倉儲是不是有點(diǎn)不倫不類呢?首先,就像 Eric Evans 所定義中明確的那樣:協(xié)調(diào)領(lǐng)域和數(shù)據(jù)映射層,兩個關(guān)鍵字領(lǐng)域和數(shù)據(jù)映射層,這里面的領(lǐng)域是指領(lǐng)域模型(實(shí)體和值對象),這是橋的一頭,另一頭就是數(shù)據(jù)映射層,也就是我們常說的 ORM 工具,在 .NET 領(lǐng)域也就是我們常用的 EntityFramework,很多人認(rèn)為 EntityFramework 就包含倉儲,好像之前有人發(fā)表過博文闡述過這個問題,但是你看下倉儲的定義,就會發(fā)現(xiàn)這不是一個概念的問題。除了這兩個關(guān)鍵詞,還有一個動詞就是協(xié)調(diào),倉儲協(xié)調(diào)的是什么?怎么協(xié)調(diào)的?這個概念需要明確下,橋的一頭-領(lǐng)域模型(主要是實(shí)體對象),這個就不多說了,橋的另一頭-ORM(對象關(guān)系映射),因?yàn)槲覀兇蟛糠智闆r下使用的是關(guān)系型數(shù)據(jù)庫,如何對數(shù)據(jù)進(jìn)行管理?當(dāng)然 DAO 是一種(這邊先不多說),還有就是使用 ORM,它可以讓你很方便的進(jìn)行數(shù)據(jù)和對象映射轉(zhuǎn)換,如果你的項(xiàng)目是基于事務(wù)腳本模式設(shè)計的,那就沒必要使用 ORM 工具了,因?yàn)槭褂煤唵蔚?SQL 更合適,說了這么多,好像都沒說到重點(diǎn),其實(shí)倉儲協(xié)調(diào)的是 ORM 中的“O”,也就是對象的概念,它是在數(shù)據(jù)映射層之上的,是一種概念,而不是一種實(shí)現(xiàn),這個概念很重要。
有時候,倉儲和數(shù)據(jù)訪問對象會當(dāng)作同義詞來看待,因?yàn)樗麄兌继峁┝藢Τ志没瘷C(jī)制的抽象,在 DAO 中比較好理解,倉儲中的持久化機(jī)制主要體現(xiàn)在 ORM 中,但是這并不屬于倉儲,更不屬于 DAO,所以有時候我們認(rèn)為所有的持久化抽象稱為 DAO,并不是很準(zhǔn)確,我們需要確定的是這種模式是否得到了真正的實(shí)現(xiàn)。
倉儲和 DAO 是不同的,一個 DAO 主要從數(shù)據(jù)庫表的角度來看待問題,并且提供 CRUD 操作,這種模式適用于事務(wù)腳本程序中,這是因?yàn)?#xff0c;這些與 DAO 相關(guān)的模式通常只是對數(shù)據(jù)庫表的一層封裝。而另一方面,倉儲和數(shù)據(jù)影射器(ORM)則更加偏向于對象,因此通常被用于領(lǐng)域模型中。
還有一點(diǎn)內(nèi)容就是存儲過程的探討,在《實(shí)現(xiàn)領(lǐng)域驅(qū)動設(shè)計》書中,作者也提到了,他不建議我們在基于領(lǐng)域驅(qū)動設(shè)計的應(yīng)用中去使用存儲過程,因?yàn)槲覀兊慕F(tuán)隊并不能很好的理解存儲過程所使用的語言,此外,通常來說他們也看不到存儲過程的實(shí)現(xiàn),而這些都是有餑于 DDD 目標(biāo)的,但是有時候使用存儲過程是為了程序性能,這是一個取舍的問題,就像我們使用 ORM 一樣,我們需要對這個概念進(jìn)行明確清楚,以防止我們在領(lǐng)域驅(qū)動設(shè)計的過程中參雜一些其他的東西。
有關(guān)倉儲和數(shù)據(jù)訪問對象的探討,最后的結(jié)論是,通常來說,你可以將倉儲當(dāng)作 DAO 來看待,但是請注意一點(diǎn),在設(shè)計倉儲時,我們應(yīng)該采用面向集合的方式,而不是面向數(shù)據(jù)訪問的方式。這有助于你將自己的領(lǐng)域當(dāng)作模型來看待,而不是 CRUD 操作。
以下幾段話來自netfocus兄:
關(guān)于倉儲(Repository),你必須知道的幾個概念。
1,倉儲的兩種設(shè)計方式:面向集合和面向持久化
面向集合和面向持久化,這兩種類型的倉儲設(shè)計方式,在《實(shí)現(xiàn)領(lǐng)域驅(qū)動設(shè)計》中有很詳細(xì)的講解,作者還附帶了幾個具體的實(shí)現(xiàn),比如 Hibernate 實(shí)現(xiàn)、TopLink 實(shí)現(xiàn)等等,這個必須贊一個,感興趣的朋友,可以進(jìn)行閱讀下。這面我簡單說明下,這兩種設(shè)計方式的不同之處,舉個最直白的例子。
面向集合方式:
this.UserRepository.Add(user);面向持久化方式:
this.UserRepository.Save(user);可能很多朋友看到這,會不以為然,需要明確一點(diǎn),在領(lǐng)域驅(qū)動設(shè)計中,不論是變量或是方法的命名規(guī)則都非常重要,因?yàn)槠浯a就是代表著一種通用語言,你要讓人家可以看懂。在面向集合方式中,新對象的添加使用的是 Add,而在面向持久化方式中,不論是新對象的添加或是修改,都是使用的 Save,如果是基于 Unit Of Work(工作單元),會有 Commit。
2,不允許同一聚合實(shí)例多次添加到倉儲中
關(guān)于這一點(diǎn)其實(shí)很多人都知道,因?yàn)榫酆洗嬖谖ㄒ恍?#xff0c;倉儲是管理它的集合,所以不可能在集合中存在多個同一聚合。另外在面向集合方式實(shí)現(xiàn)中,當(dāng)從倉儲中獲取一個對象并對其進(jìn)行修改時,我們并不需要“重新保存”該對象到倉儲中,因?yàn)榧暇S護(hù)了對該對象的引用,而修改將直接作用在該對象上。
3,倉儲實(shí)現(xiàn)方法返回類型建議為 void
我們在定義倉儲接口的時候,一般會這樣定義:
bool Add(TAggregateRoot aggregateRoot);比如添加聚合實(shí)例的方法返回值為 bool 類型,但是有時候返回 true 并不一定代表著該聚合實(shí)例成功添加到倉儲中了,因此,對于倉儲來說,返回 void 可能會是更好的方式。那如何判斷該聚合實(shí)例成功添加到倉儲中了呢?我們一般會在倉儲實(shí)現(xiàn)中進(jìn)行異常捕獲,這一點(diǎn)內(nèi)容,在書中有講解,我們可以自定義異常信息,友好的拋出一個異常。
4,對聚合實(shí)例的批量操作,最好不要使用 addAll() 和 removeAll() 方法
有時候我們在單個事務(wù)中,對多個聚合實(shí)例進(jìn)行添加或刪除的時候,為了方便,我們會使用 addAll() 和 removeAll() 方法,但是,我們使用這種方式,并不能對單個聚合實(shí)例操作進(jìn)行監(jiān)控,建議方式是循環(huán)調(diào)用 add() 和 remove() 方法。
5,聚合中刪除聚合實(shí)例的正確表達(dá)是什么?
有時候,在應(yīng)用程序設(shè)計中,對實(shí)例對象的生命周期管理就代表著其業(yè)務(wù)邏輯的體現(xiàn),我們一般在設(shè)計中刪除對象使用的是 delete,具體表現(xiàn)是從數(shù)據(jù)庫中直接將數(shù)據(jù)刪除掉,這是在事務(wù)腳本中的實(shí)現(xiàn)方式,在領(lǐng)域驅(qū)動設(shè)計中,其實(shí)是不存在對象刪除這一說法的,正確的表達(dá)應(yīng)該是,將聚合實(shí)例標(biāo)記為失活的(disabled),不可用的(unusable),也就是說在倉儲所涵蓋的內(nèi)容里面,最好不要出現(xiàn) delete,至于數(shù)據(jù)庫具體持久化中的 delete,這個就不在倉儲的概念之中了。
6,倉儲在各層中的位置存放
在書中,作者是這樣表述的:我們將倉儲接口定義放在了與聚合相同的包中(書中所有的示例都是用 java 實(shí)現(xiàn)的),而將倉儲中的實(shí)現(xiàn)類放在了 impl 子包中,這種方式被大量的 java 項(xiàng)目所采用,然而,在協(xié)作上下文中,團(tuán)隊成員們,將實(shí)現(xiàn)類放在了基礎(chǔ)設(shè)施層中。
這一點(diǎn)我是和作者持相同觀點(diǎn),比如下面的解決方案:
7,倉儲中的級聯(lián)刪除所引出的問題
關(guān)于這個問題,其實(shí)我也不是很理解,下面引自作者的一段話(P375):
有人可能會依賴于ORM所提供的生命周期事件來完成對象的級聯(lián)刪除。我刻意地沒有使用這種方式,因?yàn)槲覐?qiáng)烈反對由聚合來管理持久化,同時我強(qiáng)烈地提倡只使用資源庫來處理持久化。當(dāng)然,有關(guān)這兩者的爭論非常激烈,并且還在繼續(xù)。因此,在選擇時,你需要多方權(quán)衡。但是請記住,DDD專家是不會首先考慮使用聚合來管理持久化的。
根據(jù)我的猜測,大概是這樣的意思,主要是倉儲的持久化管理,一種是使用 ORM 攻擊所提供的持久化機(jī)制,這種方式就使得倉儲依賴于這些技術(shù)的實(shí)現(xiàn),但是可以為我們在實(shí)現(xiàn)倉儲的時候省去很多事,比如我們使用 EntityFramework,你會發(fā)現(xiàn)我們在實(shí)現(xiàn)倉儲的時候,變得異常簡單。還有一種方式就是作者提到的,建議讓倉儲自身去實(shí)現(xiàn)持久化機(jī)制,但是這種方式實(shí)現(xiàn)起來比較復(fù)雜,我也沒具體的找到其實(shí)現(xiàn)方法,這邊就不多說。
8,Unit Of Work(工作單元)的使用
只需要記住一點(diǎn):當(dāng) Unit Of Work 中的 commit() 方法執(zhí)行時,所有發(fā)生在對象上的修改都將提交到數(shù)據(jù)庫中。
9,count() or size()?
我們有時候計算聚合實(shí)例的總數(shù),一般會將實(shí)現(xiàn)方法命名為 count(),但是因?yàn)閭}儲應(yīng)該盡可能的模擬一個集合,因此建議接口定義如下:
int Size();命名規(guī)則是我們在軟件開發(fā)過程中,最容易忽略的一點(diǎn),可能在一般的開發(fā)過程中不注意會沒事,但是在領(lǐng)域驅(qū)動設(shè)計中,就像之前所表述的那樣,代碼代表著一種語言,不光是自己能看懂,還要讓需求人員可以看懂,至少可以從名字上知道其代表的意思,這一點(diǎn)很重要。
10,聚合根下的子聚合正確方式
有時,如果我們要獲取聚合根下的某些子聚合,我們不用先從資源庫中獲取到聚合根,然后再從聚合根中獲取這些子聚合,而是可以直接從資源庫中返回。在有些情況下,這種做法是有好處的。比如,某個聚合根擁有一個很大的實(shí)體類型集合,而你需要根據(jù)某種查詢條件返回該集合中的一部分實(shí)體。當(dāng)然,只有在聚合根中提供了對該實(shí)體集合的導(dǎo)航時,我們才能這么做,否則,我們便違背了聚合的設(shè)計原則。我建議不要因?yàn)榭蛻舳说姆奖愣峁┻@種訪問方式。更多的時候,采用這種方式是由于性能上的考慮,比如從聚合根中訪問子聚合將帶來性能瓶頸的時候。此時的查找方法和其他查找方法具有相同的基本特征,只是它直接返回聚合根下的子聚合,而不是聚合根本身。無論如何,請慎重使用這種方式。
以上是書中作者的觀點(diǎn)描述,其實(shí)最終也沒有表述出一個正確的方式,只是說直接訪問子聚合,作者不建議這樣做,但是有時候?yàn)榱艘恍┬阅軉栴},我們又不得不權(quán)衡利弊一下。除了這個問題之外,還有一個就是倉儲執(zhí)行完查詢后,有時候會返回多個聚合的查詢結(jié)果對象,這個我們一般會將查詢結(jié)果放在一個值對象中。
11,CQRS 模式引入
對于 CQRS 模式,我沒有深入研究過,更沒有實(shí)踐應(yīng)用過,我的想法是先去把經(jīng)典DDD理解透,然后再去嘗試其他東西,畢竟路要一步一步走,CQRS 模式是對 DDD 的一種很好補(bǔ)充,也就是說它的產(chǎn)生是有一定的理由的,對于領(lǐng)域驅(qū)動設(shè)計初學(xué)者,我個人不建議,一開始就使用 CQRS 模式。
當(dāng)我們使用用例優(yōu)化查詢時,有時候我們必須創(chuàng)建多個查詢方法,什么意思?就是跨聚合查詢,這可能意味著你的聚合邊界劃分的有問題,如果你確定你的聚合邊界劃分沒有問題,那你應(yīng)該考慮使用 CQRS 模式了,它的應(yīng)用場景就是這樣,凡事都有產(chǎn)生的原因,如果你的應(yīng)用程序沒有很復(fù)雜的查詢操作,我個人覺得,完全沒必要使用 CQRS 模式,有時候不要為了實(shí)現(xiàn)而實(shí)現(xiàn)。
12,共享倉儲
對于這個概念,我沒有深入研究過,作者也只是提出了一個思考,這邊也不多說,思考如下:
為不同的聚合類型提供單獨(dú)的資源庫究竟給我們帶來了什么好處?在聚合子類較少的情況下,為它們使用單獨(dú)的資源庫可能是最好的方式。但是,隨著聚合子類數(shù)目的增加,而同時它們又具有完全的可互換性時,使用一個共享的資源庫便更合適了。
寫在最后
本來想一篇博文寫完了事,但是看了下內(nèi)容,寫了還蠻多的,其實(shí)都還沒說到重點(diǎn)上,只是大致講述了倉儲的概念,為防止大家看得累,那分為上下篇來進(jìn)行講解。
下篇主要對:倉儲,你的歸宿究竟在哪?這個問題進(jìn)行探討,內(nèi)容主要包含其職責(zé)及調(diào)用場景的可行性探討,具體用代碼來驗(yàn)證。
這一篇內(nèi)容就到這里,歡迎大家拍磚討論。
轉(zhuǎn)載于:https://www.cnblogs.com/xishuai/p/ddd_repository.html
總結(jié)
以上是生活随笔為你收集整理的Repository 仓储,你的归宿究竟在哪?(一)-仓储的概念的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 内核态(Kernel Mode)与用户态
- 下一篇: MVC3学习:利用mvc3+ajax实现