目錄html
在上一篇文章中,咱們已經瞭解過領域驅動設計中一個很核心的對象-聚合。在現實場景中,咱們每每須要將聚合持久化到某個地方,或者是從某個地方建立出聚合。此時就會使得領域對象與咱們的基礎架構產生緊密的耦合,那麼咱們應該怎麼隔絕這一層耦合關係,使它們自身的職責界限更加清晰呢?是的,這就要用到咱們今天要講的內容 - 存儲庫。在不少地方,咱們喜歡叫它爲倉儲,特別是在現有的AspNetCore應用中,大量的應用都在引入Repository這種東西。那麼究竟什麼是存儲庫呢?咱們如今的使用方式是正確的嗎?它在領域驅動設計中又扮演着怎樣的角色呢?本文將從不一樣的角度來帶你們從新認識一下「存儲庫」這個概念,而且給出相應的代碼片斷(本教程的代碼片斷都使用的是C#,後期的實戰項目也是基於 DotNet Core 平臺)。git
「少囉嗦,直接看東西」。是的,在本次的文章中,竟然!竟然!竟然! 附帶了Github的代碼。本次代碼實際上是演示工做單元的實現,可是它確實又結合了存儲庫的一些內容,因此就在這裏提供給你們參考。github
這是一個工做單元的超簡易版本,您能夠在github中看到它的描述和簡介,這裏我就再也不重複了。下一次的文章會對工做單元的實現進行解析和優化,可能它就不屬於 《如何運用領域驅動設計》 系列的正傳系列了(算個番外吧 ( ̄▽ ̄)")。因此爲了您不錯過這一部分,能夠點擊博客園右上角的關注,有了動態以後就可以第一時間收到啦!c#
哦,對了!在Github代碼中,您可能會看到一個叫作MiCake(米蛋糕)的東西,它是咱們一步一步實現的DDD組件,它會讓您的 aspnet core 應用更輕鬆的融合DDD的思想,而且它包含了咱們該系列博文中所提到的全部戰略組件,以及它們之間的約束和處理。設計模式
是的,說存儲庫模式您可能還不能一下想到這是個什麼東西,可是一說到倉儲,您可能就會有一種豁然開朗的感受:「哦!就是這個東西呀!」。回顧一下,您現有的AspNet Core項目,是否已經引入了一個叫作Repository的對象,而且它爲您提供了與數據基礎架構交互的方法。架構
彷彿從某一天開始,以往咱們使用的BLL,DLL這種東西就逐漸開始消失了,替換它們的是一個叫作Repository的東西。特別是從傳統的AspNet演化爲AspNetCore的階段,大量的應用都開始使用倉儲了,即便您在使用相似於EF這樣的ORM框架。mvc
關於存儲厙模式存在很是多的誤解和混淆,許多人認爲它是多餘的儀式以及沒必要要的抽象,它隱藏了底層持久化框架的能力。特別是當您正在使用相似於Entity FrameWork Core這樣的ORM框架的時候,您是否發現明明EFCore直接就能夠實現的東西,爲何我又在它的基礎上套了一層,並且這一層中我並無執行任何邏輯,只是簡單的調用DbContext(EF中的數據上下文)這種東西。那爲何我不能直接調用DbContext呢?是的,這樣的疑問相信不止不少同窗都遇到了。因此在微軟EF Core 3.x的官方教程中,提到了這樣的一句話:框架
該內容位於 ASP.NET Core 官方教程 - 數據訪問 - 高級教程 中。分佈式
那麼咱們真的不須要存儲庫這種東西嗎?答案是否認的,至少在實踐領域驅動設計的應用中。還記得在上一篇文章 如何運用領域驅動設計 - 聚合 中,咱們不止一次的提到了倉儲這個概念,由於它是爲聚合而服務的,而隨着領域的深刻,使得領域模型愈來愈複雜的時候,存儲庫將慢慢變成模型的擴展,它將描述您每個用例檢索聚合的意圖。
思考一下,您現有的應用中是否包含了一個全能的ORM框架(好比EF),那您引入倉儲的緣由是什麼呢?
好吧,此次的開篇太長了,終於回到了正題:什麼是存儲庫? 原著《領域驅動設計:軟件核心複雜性應對之道》 中對存儲庫的有關解釋:
爲每種須要全局訪問的對象類型建立一個對象,這個對象就至關於該類型的全部對象在內存中的一個集合的「替身」。經過一個衆所周知的接口來提供訪問。提供添加和刪除對象的方法,用這些方法來封裝在數據存儲中實際插入或刪除數據的操做。提供根據具體標準來挑選對象的方法,並返回屬性值知足查詢標準的對象或對象集合(所返回的對象是徹底實例化的),從而將實際的存儲和查詢技術封裝起來。只爲那些確實須要直接訪問的Aggregate提供Repository。讓客戶始終聚焦於型,而將全部對象存儲和訪問操做交給Repository來完成。
國際慣例,讓咱們來看看這一段話大體講了什麼。Repository提供了一個增刪改查的操做,它抽象了數據訪問的部分。是的,這個理解是很正確的,由於這是存儲庫很重要的特性。因此有不少同窗就開始瘋狂的使用存儲庫了,在項目中大量的引入Repository,而嵌套於ORM之上。
可是!!!!! 咱們忽略了上面的其它幾點:「確實須要直接訪問的Aggregate提供Repository」 ,「提供根據具體標準來挑選對象」 。 注意,這很重要,下文將一一爲你們解釋。
這一點是很是關鍵的,存儲庫是爲聚合而服務的。有關於聚合的部分,能夠查看上一篇文章 如何運用領域驅動設計 - 聚合。爲何呢它必定要爲聚合服務? 它不能爲實體服務嗎? 由於聚合是一個總體,在上一文中咱們已經說過了,當凝練出一個聚合根的時候,就證實外界只能經過聚合根來訪問聚合內的實體,因此咱們沒有理由在任何一個地方須要穿透聚合根去訪問實體,這是錯誤而且沒有意義的。那麼很天然的就能夠衍生出:咱們何時須要使用存儲庫單獨來提取實體呢?好像確實沒有。不過有的同窗會說了,我在作**報表的時候,我就確實須要只訪問某個實體呀?那麼請思考兩個點:一、該實體是否須要提高爲聚合根。 二、若是是普遍查詢的報表,可能並不須要經過倉儲來獲取對象,須要專門的查詢框架來完成。
所以,咱們創建出來的倉儲的接口多是這個樣子的:
public interface IRepository<TAggregateRoot> where TAggregateRoot : class, IAggregateRoot { }
此處使用了C#的接口泛型約束,將倉儲的服務者約束爲了一個聚合根。該代碼在上文介紹的 MiCake 中您也能夠看到。
到目前爲止,咱們已經知道一個存儲庫至少應該包含根據ID來對聚合的增刪改查方法,可能有一些時候咱們只須要查,不須要刪。可是就一個通用的存儲庫來講,它能具備這些方法是毫無疑問的。因此咱們的倉儲接口能夠增長一些通用方法:
public interface IRepository<TAggregateRoot> where TAggregateRoot : class, IAggregateRoot { TAggregateRoot Find(TKey Id); void Add(TAggregateRoot aggregateRoot); void Update(TAggregateRoot aggregateRoot); void Delete(TAggregateRoot aggregateRoot); }
雖然存儲庫提供了基礎的提取方法,可是在許多場景下,咱們可能更須要根據某種條件來從數據庫中讀取對應的模型並將其轉換爲領域聚合對象。好比在以前的一篇文章 如何運用領域驅動設計 - 領域服務 中就有一個地方出現了使用存儲庫的狀況:咱們須要根據當前的位置來查找附近的飯店:
var nearbyRestaurants = restaurantRepository.GetNearbyRestaurant(currentAddress);
採用了相似於這樣的寫法。該存儲庫對外提供了一個GetNearbyRestaurant的方法出來,外界的應用服務就能夠經過該方法來獲取對應的結果。
這是一個很好的方法簽名,咱們經過傳入一個當前位置就可以獲取到附近的飯店。經過閱讀存儲庫提供出來的方法就能理解領域中的檢索意圖,從側面也反應了領域的某些用例。
可是,如今有部分的同窗熱愛另一種寫法:經過Lambda做爲方法參數,傳遞給下層的ORM框架來進行查詢。該方法簽名相似於這樣:
IQueryable<TEntity> FindMatch(params Expression<Func<TEntity, object>>[] propertySelectors);
這樣作的好處是全部的存儲庫均可以複用這個接口,之後全部的查詢均可以經過使用該方來來完成,而不須要再去單獨寫各類Find方法。經過返回一個IQueryable對象,甚至能夠將業務查詢邏輯直接放到應用層,這樣想怎麼操做就怎麼操做。
請注意!!!這很是的危險!!!! 您可能會問了:「我平時所接觸的框架或者倉儲不都是這樣寫的嗎?能夠實現我任何的業務查詢,爽歪歪。」 可是這樣寫正在逐漸喪失存儲庫原有的做用。回到開篇提到的一個問題:假如使用了EF這樣的ORM框架,爲何還須要嵌套一層倉儲呢? 而如今,您可能正在這樣作,開放且靈活的約定,再加上延遲的IQueryable對象,讓倉儲層徹底喪失了原有的做用,它反而成了負擔,爲何不直接使用DbContext對象呢? 爲了倉儲而使用倉儲,爲了看上去像DDD而DDD,那不是本身騙本身嗎?
因此請儘可能避免在您的存儲庫中去寫這種靈活而沒有任何明確檢索意圖的方法接口,它可能確實會使您減小代碼書寫量,但隨着項目的複雜和領域對象的逐漸增多,它會使您的應用層愈來愈迷惑。因此存儲庫中所提供的應該是具備明確約定的方法。
這裏我摘抄了 領域驅動設計模式、原理與實踐 中的一段話,我以爲它的描述很是好:
存儲庫不是一個對象。它是一個程序邊界以及一個明確的約定,在其上命名方法時它須要的工做量與領域模型中的對象所需的工做量同樣多。你的存儲庫約定應該是特定的以及可以揭示意圖並對領域專傢俱備意義。
具備領域意圖的東西咱們都應該領域層,而相似於數據庫的訪問實現這類基礎架構應該放在基礎設施層。因此能夠看出咱們抽象出來的倉儲接口是應該放在領域層的,而倉儲的實現能夠放在基礎設施層 。這個問題有不少小夥伴可能迷惑了好久,我上次看到一位同窗將倉儲接口放在了應用層,由於它認爲和領域無關,認爲倉儲只是一個提供增刪改查的東西。而這也是由於忽略了倉儲也是領域行爲的一部分的結果。
在前面講值對象的文章中,有一位園友問了我一個問題,有一點是:相似於CreateDate,CreateUser這種審計信息,咱們許多時候都會依附在領域對象身上,那麼是否是應該經過領域服務來作處理呢?
其實否則,它們雖然對咱們有參考意義,其實並無在捕獲領域需求時捕獲出來。每每這類審計信息都是咱們按照以往的開發經驗所提煉出來的,因此它們對領域對象的影響很小。那麼咱們又很須要去操做它們,好比持久化一個聚合根的時候,爲它附帶上建立時間,這樣便於咱們去追蹤它的一些記錄。而此時,就能夠依賴咱們的存儲庫來完成了,當聚合根在領域服務或者領域用例中已經完成了操做時,將它傳遞給存儲庫持久化以前就可讓存儲庫爲它加上審計信息。
存儲庫有時還能夠擁有對集合彙總的功能,好比上面咱們提到了飯店的一個倉儲,可能咱們在系統中想獲得我係統中到底有多少個飯店,或者在某個區域有多少個飯店。這種彙總的功能您也能夠交給存儲庫來完成,這也完美的符合「存儲庫」中「庫」的含義。但仍是請注意,這些彙總的方法依然得擁有一個明確的約定格式,不要由於是彙總就將存儲庫寫的開放而過於靈活。
有時候您可能須要造成一個報表,該報表它包含了各個領域對象的彙總狀況。在此時,該彙總的職責可能並不屬於存儲庫了,它須要您使用另外的方式來完成,該內容能夠看下面的小節。
在持久化的過程當中,如今的主流方式咱們都會依賴於相似於EF Core這樣的ORM框架來完成。當咱們須要將領域對象轉換爲數據庫的數據對象(能夠理解爲表吧)時,可能有時候就須要代表什麼是主鍵,什麼具備約束等狀況。若是您正在使用EF Core,對於 Data annotations 您可能再熟悉不過了,它提供了經過特性來標記的寫法完成映射關係:
public class CustomerWithoutNullableReferenceTypes { public int Id { get; set; } [Required] // Data annotations needed to configure as required public string FirstName { get; set; } [Required] public string LastName { get; set; } // Data annotations needed to configure as required public string MiddleName { get; set; } // Optional by convention }
該代碼摘自 EF Core 教程 - 必需和可選屬性
這種寫法很誘人,由於只須要簡單的在屬性上增長一個特性就完成了配置。可是!!!這些特性對領域對象實際上是沒有必要的,它可能還會干擾您的閱讀。由於咱們在構建領域對象的時候不該該考慮數據持久層面的問題,而構建出來的領域對象也應該保持乾淨。
在EFCore中,爲咱們提供了Fluent API的方式來配置模型,該方式能夠很好的讓領域對象保持乾淨。假如您沒有使用EFCore,另外的ORM框架也必定會爲您提供相似於這樣的配置方法。
不少場景咱們可能須要提供一個豐富的界面,或者一個完整的報表。好比在一個界面上顯示了某個聚合中的一個實體的信息,又或者在報表中提供了各個實體和值對象的彙總和特定信息。在這個狀況下,倉儲可能就顯得有點隆重了,我必需要經過A、B、C……倉儲獲取全部聚合A,B,C,而後再來處理彙總信息。要麼就是將存儲庫的規則打破,直接查詢利用EF Core查詢出IQueryable集合對象,而後一頓輸出猛如虎來達到效果。
記住不要爲了使用DDD而讓您的開發變得複雜而不順手,在這個時候咱們甚至能夠不使用存儲庫,咱們能夠利用另外的框架來直接查詢數據庫,也或者是使用ADO.NET運用原生Sql來達到查詢的效果。還有一種方法是將查詢單獨劃分爲應用系統的一個分支,將修改(命令)單獨劃分爲另一個分支來操做領域對象。這是DDD的另一種模式,可能您已經聽過它的英文簡寫了:CQRS。該模式的內容會在後期的文章中爲你們介紹,MiCake後期也會增長對CQRS的支持。
在持久化的過程當中,咱們必須保證一個聚合的全部的部分一同保持成功,或者一個用例的多個聚合同時保存成功(在分佈式中可能只能追求最終一致性)。因此咱們必須得保證存儲庫是有事務的,而事務的管理是由工做單元來提供的。這也是爲何存儲庫每次都和工做單元這一律念一同出現。下面引用了微軟AspNet中的一張圖,方便您理解工做單元(UnitOfWork):
該圖片選取自 微軟 AspNet 教程 - 實現存儲庫和工做單元模式
本章附帶了關於工做單元和倉儲接口的演示代碼,關於工做單元的部分會在下篇文章爲你們介紹。
關於持久化的問題已是一個老生常談的話題了,在一篇關於值對象的博文中就已經說明了這個問題。如何將領域對象如何經過ORM來持久化到數據庫?在回答這個問題以前,咱們得先理解一下什麼是領域模型和數據模型:領域模型是問題域的抽象,富含行爲和語言;數據模式是一種包含指定時間領域模型狀態的存儲結構,ORM能夠將特定的對象(C#的類)映射到數據模型。數據模型和領域模型無關,存儲庫的做用就是保持這兩個模型的獨立而且不讓它們變得模糊不清。
也就是說咱們在設計領域模型時應該僅僅關心領域中的對象,千萬不要讓框架(好比ORM)來驅動你的設計。關於這一點給了我一點靈感:既然咱們只關心領域對象,那在持久化的時候能不能單獨創建一個持久化對象專門供ORM去映射到數據庫,而倉儲負責了聚合建立和保存的過程,在這個過程當中讓倉儲自動去完成領域對象到持久化對象的轉換就好了。關於這個實現方法,準備在下下一塊兒番外系列中爲你們介紹,可能MiCake也會默認支持該方法來完成領域對象的持久化任務。固然,由於是番外的系列,因此爲了您不錯過這一部分,能夠點擊博客園右上角的關注。( 好吧,我又把上面的話不要臉的又複製了一遍 (ง •_•)ง)
本次咱們介紹了有關領域驅動設計中「存儲庫」的內容,咱們知道了什麼是存儲庫,以及如何去使用一個存儲庫。因爲存儲庫屬於一個很基礎的概念,因此在該章節中咱們沒有使用旅行記帳的案例來爲你們介紹。而更多的是但願你們可以理解使用存儲庫的場景和規範,畢竟如今存儲庫模式是很經常使用的一個模式,若是隻知其然而不知其因此然的去使用存儲庫模式,不只體驗不到它的益處,反而會讓代碼變得愈來愈複雜。
最後,提早祝你們元旦快樂。 (o゚v゚)ノ