領域驅動設計之領域模型
加壹個導航,關於如何設計聚合的詳細思考,見這篇文章。
2004年Eric Evans 發表Domain-Driven Design –Tackling Complexity in the Heart of Software (領域驅動設計),簡稱Evans DDD。領域驅動設計分為兩個階段:
以壹種領域專家、設計人員、開發人員都能理解的通用語言作為相互交流的工具,在交流的過程中發現領域概念,然後將這些概念設計成壹個領域模型;
由領域模型驅動軟件設計,用代碼來實現該領域模型;
由此可見,領域驅動設計的核心是建立正確的領域模型。
為什麽建立壹個領域模型是重要的
領域驅動設計告訴我們,在通過軟件實現壹個業務系統時,建立壹個領域模型是非常重要和必要的,因為領域模型具有以下特點:
00001.?領域模型是對具有某個邊界的領域的壹個抽象,反映了領域內用戶業務需求的本質;領域模型是有邊界的,只反應了我們在領域內所關註的部分;
00002.?領域模型只反映業務,和任何技術實現無關;領域模型不僅能反映領域中的壹些實體概念,如貨物,書本,應聘記錄,地址,等;還能反映領域中的壹些過程概念,如資金轉賬,等;
00003.?領域模型確保了我們的軟件的業務邏輯都在壹個模型中,都在壹個地方;這樣對提高軟件的可維護性,業務可理解性以及可重用性方面都有很好的幫助;
00004.?領域模型能夠幫助開發人員相對平滑地將領域知識轉化為軟件構造;
00005.?領域模型貫穿軟件分析、設計,以及開發的整個過程;領域專家、設計人員、開發人員通過領域模型進行交流,彼此***享知識與信息;因為大家面向的都是同壹個模型,所以可以防止需求走樣,可以讓軟件設計開發人員做出來的軟件真正滿足需求;
00006.?要建立正確的領域模型並不簡單,需要領域專家、設計、開發人員積極溝通***同努力,然後才能使大家對領域的認識不斷深入,從而不斷細化和完善領域模型;
00007.?為了讓領域模型看的見,我們需要用壹些方法來表示它;圖是表達領域模型最常用的方式,但不是唯壹的表達方式,代碼或文字描述也能表達領域模型;
00008.?領域模型是整個軟件的核心,是軟件中最有價值和最具競爭力的部分;設計足夠精良且符合業務需求的領域模型能夠更快速的響應需求變化;
領域通用語言(UBIQUITOUS LANGUAGE)
我們認識到由軟件專家和領域專家通力合作開發出壹個領域的模型是絕對需要的,但是,那種方法通常會由於壹些基礎交流的障礙而存在難點。開發人員滿腦子都是類、方法、算法、模式、架構,等等,總是想將實際生活中的概念和程序工件進行對應。他們希望看到要建立哪些對象類,要如何對對象類之間的關系建模。他們會習慣按照封裝、繼承、多態等面向對象編程中的概念去思考,會隨時隨地這樣交談,這對他們來說這太正常不過了,開發人員就是開發人員。但是領域專家通常對這壹無所知,他們對軟件類庫、框架、持久化甚至數據庫沒有什麽概念。他們只了解他們特有的領域專業技能。比如,在空中交通監控樣例中,領域專家知道飛機、路線、海拔、經度、緯度,知道飛機偏離了正常路線,知道飛機的發射。他們用他們自己的術語討論這些事情,有時這對於外行來說很難直接理解。如果壹個人說了什麽事情,其他的人不能理解,或者更糟的是錯誤理解成其他事情,又有什麽機會來保證項目成功呢?
在交流的過程中,需要做翻譯才能讓其他的人理解這些概念。開發人員可能會努力使用外行人的語言來解析壹些設計模式,但這並壹定都能成功奏效。領域專家也可能會創建壹種新的行話以努力表達他們的這些想法。在這個痛苦的交流過程中,這種類型的翻譯並不能對知識的構建過程產生幫助。
領域建模時思考問題的角度
“用戶需求”不能等同於“用戶”,捕捉“用戶心中的模型”也不能等同於“以用戶為核心設計領域模型”。 《老子》書中有個觀點:有之以為利,無之以為用。在這裏,有之利,即建立領域模型;無之用,即包容用戶需求。舉些例子,壹個杯子要裝滿壹杯水,我們在制作杯子時,制作的是空杯子,即要把水倒出來,之後才能裝下水;再比如,壹座房子要住人,我們在建造房子時,建造的房子是空的,唯有空的才能容納人的居住。因此,建立領域模型時也要將用戶置於模型之外,這樣才能包容用戶的需求。
所以,我的理解是:
00001.?我們設計領域模型時不能以用戶為中心作為出發點去思考問題,不能老是想著用戶會對系統做什麽;而應該從壹個客觀的角度,根據用戶需求挖掘出領域內的相關事物,思考這些事物的本質關聯及其變化規律作為出發點去思考問題。
00002.?領域模型是排除了人之外的客觀世界模型,但是領域模型包含人所扮演的參與者角色,但是壹般情況下不要讓參與者角色在領域模型中占據主要位置,如果以人所扮演的參與者角色在領域模型中占據主要位置,那麽各個系統的領域模型將變得沒有差別,因為軟件系統就是壹個人機交互的系統,都是以人為主的活動記錄或跟蹤;比如:論壇中如果以人為主導,那麽領域模型就是:人發帖,人回帖,人結貼,等等;DDD的例子中,如果是以人為中心的話,就變成了:托運人托運貨物,收貨人收貨物,付款人付款,等等;因此,當我們談及領域模型時,已經默認把人的因素排除開了,因為領域只有對人來說才有意義,人是在領域範圍之外的,如果人也劃入領域,領域模型將很難保持客觀性。領域模型是與誰用和怎樣用是無關的客觀模型。歸納起來說就是,領域建模是建立虛擬模型讓我們現實的人使用,而不是建立虛擬空間,去模仿現實。
領域驅動設計的經典分層架構
用戶界面/展現層
負責向用戶展現信息以及解釋用戶命令。更細的方面來講就是:
00001.?請求應用層以獲取用戶所需要展現的數據;
00002.?發送命令給應用層要求其執行某個用戶命令;
應用層
很薄的壹層,定義軟件要完成的所有任務。對外為展現層提供各種應用功能(包括查詢或命令),對內調用領域層(領域對象或領域服務)完成各種業務邏輯,應用層不包含業務邏輯。
領域層
負責表達業務概念,業務狀態信息以及業務規則,領域模型處於這壹層,是業務軟件的核心。
基礎設施層
本層為其他層提供通用的技術能力;提供了層間的通信;為領域層實現持久化機制;總之,基礎設施層可以通過架構和框架來支持其他層的技術需求;
領域驅動設計過程中使用的模式
所有模式的總攬圖
關聯的設計
關聯本身不是壹個模式,但它在領域建模的過程中非常重要,所以需要在探討各種模式之前,先討論壹下對象之間的關聯該如何設計。我覺得對象的關聯的設計可以遵循如下的壹些原則:
00001.?關聯盡量少,對象之間的復雜的關聯容易形成對象的關系網,這樣對於我們理解和維護單個對象很不利,同時也很難劃分對象與對象之間的邊界;另外,同時減少關聯有助於簡化對象之間的遍歷;
00002.?對多的關聯也許在業務上是很自然的,通常我們會用壹個集合來表示1對多的關系。但我們往往也需要考慮到性能問題,尤其是當集合內元素非常多的時候,此時往往需要通過單獨查詢來獲取關聯的集合信息;
00003.?關聯盡量保持單向的關聯;
00004.?在建立關聯時,我們需要深入去挖掘是否存在關聯的限制條件,如果存在,那麽最好把這個限制條件加到這個關聯上;往往這樣的限制條件能將關聯化繁為簡,即可以將多對多簡化為1對多,或將1對多簡化為1對1;
實體(Entity)
實體就是領域中需要唯壹標識的領域概念。因為我們有時需要區分是哪個實體。有兩個實體,如果唯壹標識不壹樣,那麽即便實體的其他所有屬性都壹樣,我們也認為他們兩個不同的實體;因為實體有生命周期,實體從被創建後可能會被持久化到數據庫,然後某個時候又會被取出來。所以,如果我們不為實體定義壹種可以唯壹區分的標識,那我們就無法區分到底是這個實體還是哪個實體。
另外,不應該給實體定義太多的屬性或行為,而應該尋找關聯,發現其他壹些實體或值對象,將屬性或行為轉移到其他關聯的實體或值對象上。比如Customer實體,他有壹些地址信息,由於地址信息是壹個完整的有業務含義的概念,所以,我們可以定義壹個Address對象,然後把Customer的地址相關的信息轉移到Address對象上。如果沒有Address對象,而把這些地址信息直接放在Customer對象上,並且如果對於壹些其他的類似Address的信息也都直接放在Customer上,會導致Customer對象很混亂,結構不清晰,最終導致它難以維護和理解;
值對象(Value Object)
在領域中,並不是沒壹個事物都必須有壹個唯壹標識,也就是說我們不關心對象是哪個,而只關心對象是什麽。就以上面的地址對象Address為例,如果有兩個Customer的地址信息是壹樣的,我們就會認為這兩個Customer的地址是同壹個。也就是說只要地址信息壹樣,我們就認為是同壹個地址。用程序的方式來表達就是,如果兩個對象的所有的屬性的值都相同我們會認為它們是同壹個對象的話,那麽我們就可以把這種對象設計為值對象。因此,值對象沒有唯壹標識,這是它和實體的最大不同。
另外值對象在判斷是否是同壹個對象時是通過它們的所有屬性是否相同,如果相同則認為是同壹個值對象;而我們在區分是否是同壹個實體時,只看實體的唯壹標識是否相同,而不管實體的屬性是否相同;值對象另外壹個明顯的特征是不可變,即所有屬性都是只讀的。因為屬性是只讀的,所以可以被安全的***享;當***享值對象時,壹般有復制和***享兩種做法,具體采用哪種做法還要根據實際情況而定;另外,我們應該給值對象設計的盡量簡單,不要讓它引用很多其他的對象,因為他只是壹個值,就像int a = 3;那麽”3”就是壹個我們傳統意義上所說的值,而值對象其實也可以和這裏的”3”壹樣理解,也是壹個值,只不過是用對象來表示。所以,當我們在C#語言中比較兩個值對象是否相等時,會重寫GetHashCode和Equals這兩個方法,目的就是為了比較對象的值;值對象雖然是只讀的,但是可以被整個替換掉。就像妳把a的值修改為”4”(a = 4;)壹樣,直接把”3”這個值替換為”4”了。值對象也是壹樣,當妳要修改Customer的Address對象引用時,不是通過Customer.Address.Street這樣的方式來實現,因為值對象是只讀的,它是壹個完整的不可分割的整體。我們可以這樣做:Customer.Address = new Address(…);
應用層服務
00001.?獲取輸入(如壹個XML請求);
00002.?發送消息給領域層服務,要求其實現轉帳的業務邏輯;
00003.?領域層服務處理成功,則調用基礎層服務發送Email通知;
領域層服務
00001.?獲取源帳號和目標帳號,分別通知源帳號和目標帳號進行扣除金額和增加金額的操作;
00002.?提供返回結果給應用層;
基礎層服務
按照應用層的請求,發送Email通知;
所以,從上面的例子中可以清晰的看出,每種服務的職責;
聚合及聚合根(Aggregate,Aggregate Root)
聚合,它通過定義對象之間清晰的所屬關系和邊界來實現領域模型的內聚,並避免了錯綜復雜的難以維護的對象關系網的形成。聚合定義了壹組具有內聚關系的相關對象的集合,我們把聚合看作是壹個修改數據的單元。
聚合有以下壹些特點:
00001.?每個聚合有壹個根和壹個邊界,邊界定義了壹個聚合內部有哪些實體或值對象,根是聚合內的某個實體;
00002.?聚合內部的對象之間可以相互引用,但是聚合外部如果要訪問聚合內部的對象時,必須通過聚合根開始導航,絕對不能繞過聚合根直接訪問聚合內的對象,也就是說聚合根是外部可以保持 對它的引用的唯壹元素;
00003.?聚合內除根以外的其他實體的唯壹標識都是本地標識,也就是只要在聚合內部保持唯壹即可,因為它們總是從屬於這個聚合的;
00004.?聚合根負責與外部其他對象打交道並維護自己內部的業務規則;
00005.?基於聚合的以上概念,我們可以推論出從數據庫查詢時的單元也是以聚合為壹個單元,也就是說我們不能直接查詢聚合內部的某個非根的對象;
00006.?聚合內部的對象可以保持對其他聚合根的引用;
00007.?刪除壹個聚合根時必須同時刪除該聚合內的所有相關對象,因為他們都同屬於壹個聚合,是壹個完整的概念;
如何識別聚合根?
如果壹個聚合只有壹個實體,那麽這個實體就是聚合根;如果有多個實體,那麽我們可以思考聚合內哪個對象有獨立存在的意義並且可以和外部直接進行交互。
工廠(Factory)
DDD中的工廠也是壹種體現封裝思想的模式。DDD中引入工廠模式的原因是:有時創建壹個領域對象是壹件比較復雜的事情,不僅僅是簡單的new操作。正如對象封裝了內部實現壹樣(我們無需知道對象的內部實現就可以使用對象的行為),工廠則是用來封裝創建壹個復雜對象尤其是聚合時所需的知識,工廠的作用是將創建對象的細節隱藏起來。客戶傳遞給工廠壹些簡單的參數,然後工廠可以在內部創建出壹個復雜的領域對象然後返回給客戶。
領域模型中其他元素都不適合做這個事情,所以需要引入這個新的模式,工廠。工廠在創建壹個復雜的領域對象時,通常會知道該滿足什麽業務規則(它知道先怎樣實例化壹個對象,然後在對這個對象做哪些初始化操作,這些知識就是創建對象的細節),如果傳遞進來的參數符合創建對象的業務規則,則可以順利創建相應的對象;但是如果由於參數無效等原因不能創建出期望的對象時,應該拋出壹個異常,以確保不會創建出壹個錯誤的對象。當然我們也並不總是需要通過工廠來創建對象,事實上大部分情況下領域對象的創建都不會太復雜,所以我們只需要簡單的使用構造函數創建對象就可以了。隱藏創建對象的好處是顯而易見的,這樣可以不會讓領域層的業務邏輯泄露到應用層,同時也減輕了應用層的負擔,它只需要簡單的調用領域工廠創建出期望的對象即可。
倉儲(Repository)
00001.?倉儲被設計出來的目的是基於這個原因:領域模型中的對象自從被創建出來後不會壹直留在內存中活動的,當它不活動時會被持久化到數據庫中,然後當需要的時候我們會重建該對象;重建對象就是根據數據庫中已存儲的對象的狀態重新創建對象的過程;所以,可見重建對象是壹個和數據庫打交道的過程。從更廣義的角度來理解,我們經常會像集合壹樣從某個類似集合的地方根據某個條件獲取壹個或壹些對象,往集合中添加對象或移除對象。也就是說,我們需要提供壹種機制,可以提供類似集合的接口來幫助我們管理對象。倉儲就是基於這樣的思想被設計出來的;
設計領域模型的壹般步驟
00001.?根據需求建立壹個初步的領域模型,識別出壹些明顯的領域概念以及它們的關聯,關聯可以暫時沒有方向但需要有(1:1,1:N,M:N)這些關系;可以用文字精確的沒有歧義的描述出每個領域概念的涵義以及包含的主要信息;
00002.?分析主要的軟件應用程序功能,識別出主要的應用層的類;這樣有助於及早發現哪些是應用層的職責,哪些是領域層的職責;
00003.?進壹步分析領域模型,識別出哪些是實體,哪些是值對象,哪些是領域服務;
00004.?分析關聯,通過對業務的更深入分析以及各種軟件設計原則及性能方面的權衡,明確關聯的方向或者去掉壹些不需要的關聯;
00005.?找出聚合邊界及聚合根,這是壹件很有難度的事情;因為妳在分析的過程中往往會碰到很多模棱兩可的難以清晰判斷的選擇問題,所以,需要我們平時壹些分析經驗的積累才能找出正確的聚合根;
00006.?為聚合根配備倉儲,壹般情況下是為壹個聚合分配壹個倉儲,此時只要設計好倉儲的接口即可;
00007.?走查場景,確定我們設計的領域模型能夠有效地解決業務需求;
00008.?考慮如何創建領域實體或值對象,是通過工廠還是直接通過構造函數;
00009.?停下來重構模型。尋找模型中覺得有些疑問或者是蹩腳的地方,比如思考壹些對象應該通過關聯導航得到還是應該從倉儲獲取?聚合設計的是否正確?考慮模型的性能怎樣,等等;
關於Unit of Work(工作單元)的幾種實現方法
00001.?基於快照的實現,即領域對象被取出來後,會先保存壹個備份的對象,然後當在做持久化操作時,將最新的對象的狀態和備份的對象的狀態進行比較,如果不相同,則認為有做過修改,然後進行持久化;這種設計的好處是對象不用告訴工作單元自己的狀態修改了,而缺點也是顯而易見的,那就是性能可能會低,備份對象以及比較對象的狀態是否有修改的過程在當對象本身很復雜的時候,往往是壹個比較耗時的步驟,而且要真正實現對象的深拷貝以及判斷屬性是否修改還是比較困難的;
00002.?不基於快照,而是倉儲的相關更新或新增或刪除接口被調用時,倉儲通知工作單元某個對象被新增了或更新了或刪除了。這樣工作單元在做數據持久化時也同樣可以知道需要持久化哪些對象了;這種方法理論上不需要ORM框架的支持,對領域模型也沒有任何傾入性,同時也很好的支持了工作單元的模式。對於不想用高級ORM框架的朋友來說,這種方法挺好;
對於不會影響領域層中領域對象狀態的查詢功能
可以直接通過倉儲查詢出所需要的數據。但壹般領域層中的倉儲提供的查詢功能也許不能滿足界面顯示的需要,則可能需要多次調用不同的倉儲才能獲取所需要顯示的數據;其實針對這種查詢的情況,我在後面會講到可以直接通過CQRS的架構來實現。
即對於查詢,我們可以在應用層不調用領域層的任何東西,而是直接通過某個其他的用另外的技術架構實現的查詢引擎來完成查詢,比如直接通過構造參數化SQL的方式從數據庫壹個表或多個表中查詢出任何想要顯示的數據。這樣不僅性能高,也可以減輕領域層的負擔。領域模型不太適合為應用層提供各種查詢服務,因為往往界面上要顯示的數據是很多對象的組合信息,是壹種非對象概念的信息,就像報表;
面向對象的實質就是邊界劃分,封裝,不但對需求變化能夠量化,縮小影響面;因為邊界劃分也會限制出錯的影響範圍,所以OO對軟件後期BUG等出錯也有好處。
軟件世界永遠都有BUG,BUG是清除不幹凈的,就像人類世界永遠都存在不完美和陰暗面,問題關鍵是:上帝用空間和時間的邊界把人類世界痛苦災難等不完美局限在壹個範圍內;而軟件世界如果妳不采取OO等方法進行邊界劃分的話,壹旦出錯,追查起來情況會有多糟呢?
軟件世界其實類似人類現實世界,有時出問題了,探究原因壹看,原來是兩個看上去毫無聯系的因素導致的,古人只好經常求神拜佛,我們程序員在自己的軟件上線運行時,大概心裏也在求神拜佛別出大紕漏,如果我們的軟件采取OO封裝,我們就會坦然些,肯定會出錯,但是我們已經預先劃定好邊界,所以,不會產生嚴重後果,甚至也不會出現難以追查的魔鬼BUG。
四色原型分析模式
時刻-時間段原型(Moment-Interval Archetype)
表示在某個時刻或某壹段時間內發生的某個活動。使用粉紅色表示,簡寫為MI。
參與方-地點-物品原型(Part-Place-Thing Archetype)
表示參與某個活動的人或物,地點則是活動的發生地。使用綠色表示。簡寫為PPT。
描述原型(Description Archetype)
表示對PPT的本質描述。它不是PPT的分類!Description是從PPT抽象出來的不變的***性的屬性的集合。使用藍色表示,簡寫為DESC。
舉個例子,有壹個人叫張三,如果某個外星人問妳張三是什麽?妳會怎麽說?可能會說,張三是個人,但是外星人不知道“人”是什麽。然後妳會怎麽辦?妳就會說:張三是個由壹個頭、兩只手、兩只腳,以及壹個身體組成的客觀存在。雖然這時外星人仍然不知道人是什麽,但我已經可以借用這個例子向大家說明什麽是“Description”了。在這個例子中,張三就是壹個PPT,而“由壹個頭、兩只手、兩只腳,以及壹個身體組成的客觀存在”就是對張三的Description,頭、手、腳、身體則是人的本質的不變的***性的屬性的集合。但我們人類比較聰明,很會抽象總結和命名,已經把這個Description用壹個字來代替了,那就是“人”。所以就有所謂的張三是人的說法。
角色原型(Role Archetype)
角色就是我們平時所理解的“身份”。使用黃色表示,簡寫為Role。為什麽會有角色這個概念?因為有些活動,只允許具有特定角色(身份)的PPT(參與者)才能參與該活動。比如壹個人只有具有教師的角色才能上課(壹種活動);壹個人只有是壹個合法公民才能參與選舉和被選舉;但是有些活動也是不需要角色的,比如壹個人不需要具備任何角色就可以睡覺(壹種活動)。當然,其實說人不需要角色就能睡覺也是錯誤的,錯在哪裏?因為我們可以這樣理解:壹個客觀存在只要具有“人”的角色就能睡覺,其實這時候,我們已經把DESC當作角色來看待了。所以,其實角色這個概念是非常廣的,不能用我們平時所理解的狹義的“身份”來理解,因為“教師”、“合法公民”、“人”都可以被作為角色來看待。因此,應該這樣說:任何壹個活動,都需要具有壹定角色的參與者才能參與。
用壹句話來概括四色原型就是:壹個什麽什麽樣的人或組織或物品以某種角色在某個時刻或某段時間內參與某個活動。 其中“什麽什麽樣的”就是DESC,“人或組織或物品”就是PPT,“角色”就是Role,而”某個時刻或某段時間內的某個活動"就是MI。
以上這些東西如果在學習了DDD之後再去學習會對DDD有更深入的了解,但我覺得DDD相對比較基礎,如果我們在已經了解了DDD的基礎之上再去學習這些東西會更加有效和容易掌握。