領域驅動設計之領域模型--轉載

領域驅動設計之領域模型

加一個導航,關於如何設計聚合的詳細思考,見這篇文章。html

2004年Eric Evans 發表Domain-Driven Design –Tackling Complexity in the Heart of Software (領域驅動設計),簡稱Evans DDD。領域驅動設計分爲兩個階段:程序員

以一種領域專家、設計人員、開發人員都能理解的通用語言做爲相互交流的工具,在交流的過程當中發現領域概念,而後將這些概念設計成一個領域模型;
由領域模型驅動軟件設計,用代碼來實現該領域模型;
算法

因而可知,領域驅動設計的核心是創建正確的領域模型。數據庫

爲何創建一個領域模型是重要的

領域驅動設計告訴咱們,在經過軟件實現一個業務系統時,創建一個領域模型是很是重要和必要的,由於領域模型具備如下特色:編程

  1. 領域模型是對具備某個邊界的領域的一個抽象,反映了領域內用戶業務需求的本質;領域模型是有邊界的,只反應了咱們在領域內所關注的部分;
  2. 領域模型只反映業務,和任何技術實現無關;領域模型不只能反映領域中的一些實體概念,如貨物,書本,應聘記錄,地址,等;還能反映領域中的一些過程概念,如資金轉帳,等;
  3. 領域模型確保了咱們的軟件的業務邏輯都在一個模型中,都在一個地方;這樣對提升軟件的可維護性,業務可理解性以及可重用性方面都有很好的幫助;
  4. 領域模型可以幫助開發人員相對平滑地將領域知識轉化爲軟件構造;
  5. 領域模型貫穿軟件分析、設計,以及開發的整個過程;領域專家、設計人員、開發人員經過領域模型進行交流,彼此共享知識與信息;由於你們面向的都是同一個模型,因此能夠防止需求走樣,可讓軟件設計開發人員作出來的軟件真正知足需求;
  6. 要創建正確的領域模型並不簡單,須要領域專家、設計、開發人員積極溝通共同努力,而後才能使你們對領域的認識不斷深刻,從而不斷細化和完善領域模型;
  7. 爲了讓領域模型看的見,咱們須要用一些方法來表示它;圖是表達領域模型最經常使用的方式,但不是惟一的表達方式,代碼或文字描述也能表達領域模型;
  8. 領域模型是整個軟件的核心,是軟件中最有價值和最具競爭力的部分;設計足夠精良且符合業務需求的領域模型可以更快速的響應需求變化;

領域通用語言(UBIQUITOUS LANGUAGE)

咱們認識到由軟件專家和領域專家通力合做開發出一個領域的模型是絕對須要的,可是,那種方法一般會因爲一些基礎交流的障礙而存在難點。開發人員滿腦子都是類、方法、算法、模式、架構,等等,老是想將實際生活中的概念和程序工件進行對應。他們但願看到要創建哪些對象類,要如何對對象類之間的關係建模。他們會習慣按照封裝、繼承、多態等面向對象編程中的概念去思考,會隨時隨地這樣交談,這對他們來講這太正常不過了,開發人員就是開發人員。可是領域專家一般對這一無所知,他們對軟件類庫、框架、持久化甚至數據庫沒有什麼概念。他們只瞭解他們特有的領域專業技能。好比,在空中交通監控樣例中,領域專家知道飛機、路線、海拔、經度、緯度,知道飛機偏離了正常路線,知道飛機的發射。他們用他們本身的術語討論這些事情,有時這對於外行來講很難直接理解。若是一我的說了什麼事情,其餘的人不能理解,或者更糟的是錯誤理解成其餘事情,又有什麼機會來保證項目成功呢?設計模式

在交流的過程當中,須要作翻譯才能讓其餘的人理解這些概念。開發人員可能會努力使用外行人的語言來解析一些設計模式,但這並必定都能成功奏效。領域專家也可能會建立一種新的行話以努力表達他們的這些想法。在這個痛苦的交流過程當中,這種類型的翻譯並不能對知識的構建過程產生幫助。安全

領域驅動設計的一個核心的原則是使用一種基於模型的語言。由於模型是軟件知足領域的共同點,它很適合做爲這種通用語言的構造基礎。使用模型做爲語言的核心骨架,要求團隊在進行全部的交流是都使用一致的語言,在代碼中也是這樣。在共享知識和推敲模型時,團隊會使用演講、文字和圖形。這兒須要確保團隊使用的語言在全部的交流形式中看上去都是一致的,這種語言被稱爲「通用語言(Ubiquitous Language)」。通用語言應該在建模過程當中普遍嘗試以推進軟件專家和領域專家之間的溝通,從而發現要在模型中使用的主要的領域概念。架構

將領域模型轉換爲代碼實現的最佳實踐

擁有一個看上去正確的模型不表明模型能被直接轉換成代碼,也或者它的實現可能會違背某些咱們所不建議的軟件設計原則。咱們該如何實現從模型到代碼的轉換,並讓代碼具備可擴展性、可維護性,高性能等指標呢?另外,如實反映領域的模型可能會致使對象持久化的一系列問題,或者致使不可接受的性能問題。那麼咱們應該怎麼作呢?框架

咱們應該緊密關聯領域建模和設計,緊密將領域模型和軟件編碼實現捆綁在一塊兒,模型在構建時就考慮到軟件和設計。開發人員會被加入到建模的過程當中來。主要的想法是選擇一個可以恰當在軟件中表現的模型,這樣設計過程會很順暢而且基於模型。代碼和其下的模型緊密關聯會讓代碼更有意義並與模型更相關。有了開發人員的參與就會有反饋。它能保證模型被實現成軟件。若是其中某處有錯誤,會在早期就被標識出來,問題也會容易修正。寫代碼的人會很好地瞭解模型,會感受本身有責任保持它的完整性。他們會意識到對代碼的一個變動其實就隱含着對模型的變動,另外,若是哪裏的代碼不能表現原始模型的話,他們會重構代碼。若是分析人員從實現過程當中分離出去,他會再也不關心開發過程當中引入的侷限性。最終結果是模型再也不實用。任何技術人員想對模型作出貢獻必須花費一些時間來接觸代碼,不管他在項目中擔負的是什麼主要角色。任何一個負責修改代碼的人都必須學會用代碼表現模型。每位開發人員都必須參與到必定級別的領域討論中並和領域專家聯絡。異步

領域建模時思考問題的角度

「用戶需求」不能等同於「用戶」,捕捉「用戶心中的模型」也不能等同於「以用戶爲核心設計領域模型」。 《老子》書中有個觀點:有之覺得利,無之覺得用。在這裏,有之利,即創建領域模型;無之用,即包容用戶需求。舉些例子,一個杯子要裝滿一杯水,咱們在製做杯子時,製做的是空杯子,即要把水倒出來,以後才能裝下水;再好比,一座房子要住人,咱們在建造房子時,建造的房子是空的,惟有空的才能容納人的居住。所以,創建領域模型時也要將用戶置於模型以外,這樣才能包容用戶的需求。

因此,個人理解是:

  1. 咱們設計領域模型時不能以用戶爲中心做爲出發點去思考問題,不能總是想着用戶會對系統作什麼;而應該從一個客觀的角度,根據用戶需求挖掘出領域內的相關事物,思考這些事物的本質關聯及其變化規律做爲出發點去思考問題。
  2. 領域模型是排除了人以外的客觀世界模型,可是領域模型包含人所扮演的參與者角色,可是通常狀況下不要讓參與者角色在領域模型中佔據主要位置,若是以人所扮演的參與者角色在領域模型中佔據主要位置,那麼各個系統的領域模型將變得沒有差異,由於軟件系統就是一我的機交互的系統,都是以人爲主的活動記錄或跟蹤;好比:論壇中若是以人爲主導,那麼領域模型就是:人發帖,人回帖,人結貼,等等;DDD的例子中,若是是以人爲中心的話,就變成了:託運人託運貨物,收貨人收貨物,付款人付款,等等;所以,當咱們談及領域模型時,已經默認把人的因素排除開了,由於領域只有對人來講纔有意義,人是在領域範圍以外的,若是人也劃入領域,領域模型將很難保持客觀性。領域模型是與誰用和怎樣用是無關的客觀模型。概括起來講就是,領域建模是創建虛擬模型讓咱們現實的人使用,而不是創建虛擬空間,去模仿現實。

以Eric Evans(DDD之父)在他的書中的一個貨物運輸系統爲例子簡單說明一下。在通過一些用戶需求討論以後,在用戶需求相對明朗以後,Eric這樣描述領域模型:

  1. 一個Cargo(貨物)涉及多個Customer(客戶,如託運人、收貨人、付款人),每一個Customer承擔不一樣的角色;
  2. Cargo的運送目標已指定,即Cargo有一個運送目標;
  3. 由一系列知足Specification(規格)的Carrier Movement(運輸動做)來完成運輸目標;

從上面的描述咱們能夠看出,他徹底沒有從用戶的角度去描述領域模型,而是以領域內的相關事物爲出發點,考慮這些事物的本質關聯及其變化規律的。上述這段描述徹底以貨物爲中心,把客戶當作是貨物在某個場景中可能會涉及到的關聯角色,如貨物會涉及到託運人、收貨人、付款人;貨物有一個肯定的目標,貨物會通過一系列列的運輸動做到達目的地;其實,我以爲以用戶爲中心來思考領域模型的思惟只是停留在需求的表面,而沒有挖掘出真正的需求的本質;咱們在作領域建模時須要努力挖掘用戶需求的本質,這樣才能真正實現用戶需求;

關於用戶、參與者這兩個概念的區分,能夠看一下下面的例子:

試想兩我的共同玩足球遊戲,操做者(用戶)是驅動者,它驅使足球比賽領域中,各個「人」(參與者)的活動。這裏立下一個假設,假設操做者A操做某一隊員a,而隊員a擁有着某人B的信息,那麼有如下說法,a是B的鏡像,a是領域參與者,A是驅動者。

領域驅動設計的經典分層架構

用戶界面/展示層

負責向用戶展示信息以及解釋用戶命令。更細的方面來說就是:

  1. 請求應用層以獲取用戶所須要展示的數據;
  2. 發送命令給應用層要求其執行某個用戶命令;

應用層

很薄的一層,定義軟件要完成的全部任務。對外爲展示層提供各類應用功能(包括查詢或命令),對內調用領域層(領域對象或領域服務)完成各類業務邏輯,應用層不包含業務邏輯。

領域層

負責表達業務概念,業務狀態信息以及業務規則,領域模型處於這一層,是業務軟件的核心。

基礎設施層

本層爲其餘層提供通用的技術能力;提供了層間的通訊;爲領域層實現持久化機制;總之,基礎設施層能夠經過架構和框架來支持其餘層的技術需求;

領域驅動設計過程當中使用的模式

全部模式的總攬圖

關聯的設計

關聯自己不是一個模式,但它在領域建模的過程當中很是重要,因此須要在探討各類模式以前,先討論一下對象之間的關聯該如何設計。我以爲對象的關聯的設計能夠遵循以下的一些原則:

  1. 關聯儘可能少,對象之間的複雜的關聯容易造成對象的關係網,這樣對於咱們理解和維護單個對象很不利,同時也很難劃分對象與對象之間的邊界;另外,同時減小關聯有助於簡化對象之間的遍歷;
  2. 對多的關聯也許在業務上是很天然的,一般咱們會用一個集合來表示1對多的關係。但咱們每每也須要考慮到性能問題,尤爲是當集合內元素很是多的時候,此時每每須要經過單獨查詢來獲取關聯的集合信息;
  3. 關聯儘可能保持單向的關聯;
  4. 在創建關聯時,咱們須要深刻去挖掘是否存在關聯的限制條件,若是存在,那麼最好把這個限制條件加到這個關聯上;每每這樣的限制條件能將關聯化繁爲簡,便可以將多對多簡化爲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(…);

領域服務(Domain Service)

領域中的一些概念不太適合建模爲對象,即歸類到實體對象或值對象,由於它們本質上就是一些操做,一些動做,而不是事物。這些操做或動做每每會涉及到多個領域對象,而且須要協調這些領域對象共同完成這個操做或動做。若是強行將這些操做職責分配給任何一個對象,則被分配的對象就是承擔一些不應承擔的職責,從而會致使對象的職責不明確很混亂。可是基於類的面嚮對象語言規定任何屬性或行爲都必須放在對象裏面。因此咱們須要尋找一種新的模式來表示這種跨多個對象的操做,DDD認爲服務是一個很天然的範式用來對應這種跨多個對象的操做,因此就有了領域服務這個模式。和領域對象不一樣,領域服務是以動詞開頭來命名的,好比資金轉賬服務能夠命名爲MoneyTransferService。固然,你也能夠把服務理解爲一個對象,但這和通常意義上的對象有些區別。由於通常的領域對象都是有狀態和行爲的,而領域服務沒有狀態只有行爲。須要強調的是領域服務是無狀態的,它存在的意義就是協調領域對象共完成某個操做,全部的狀態仍是都保存在相應的領域對象中。我以爲模型(實體)與服務(場景)是對領域的一種劃分,模型關注領域的個體行爲,場景關注領域的羣體行爲,模型關注領域的靜態結構,場景關注領域的動態功能。這也符合了現實中出現的各類現象,有動有靜,有獨立有協做。

領域服務還有一個很重要的功能就是能夠避免領域邏輯泄露到應用層。由於若是沒有領域服務,那麼應用層會直接調用領域對象完成本該是屬於領域服務該作的操做,這樣一來,領域層可能會把一部分領域知識泄露到應用層。由於應用層須要瞭解每一個領域對象的業務功能,具備哪些信息,以及它可能會與哪些其餘領域對象交互,怎麼交互等一系列領域知識。所以,引入領域服務能夠有效的防治領域層的邏輯泄露到應用層。對於應用層來講,從可理解的角度來說,經過調用領域服務提供的簡單易懂但意義明確的接口確定也要比直接操縱領域對象容易的多。這裏彷佛也看到了領域服務具備Façade的功能,呵呵。

說到領域服務,還須要提一下軟件中通常有三種服務:應用層服務、領域服務、基礎服務。

應用層服務

  1. 獲取輸入(如一個XML請求);
  2. 發送消息給領域層服務,要求其實現轉賬的業務邏輯;
  3. 領域層服務處理成功,則調用基礎層服務發送Email通知;

領域層服務

  1. 獲取源賬號和目標賬號,分別通知源賬號和目標賬號進行扣除金額和增長金額的操做;
  2. 提供返回結果給應用層;

基礎層服務

按照應用層的請求,發送Email通知;

因此,從上面的例子中能夠清晰的看出,每種服務的職責;

聚合及聚合根(Aggregate,Aggregate Root)

聚合,它經過定義對象之間清晰的所屬關係和邊界來實現領域模型的內聚,並避免了錯綜複雜的難以維護的對象關係網的造成。聚合定義了一組具備內聚關係的相關對象的集合,咱們把聚合看做是一個修改數據的單元。

聚合有如下一些特色:

  1. 每一個聚合有一個根和一個邊界,邊界定義了一個聚合內部有哪些實體或值對象,根是聚合內的某個實體;
  2. 聚合內部的對象之間能夠相互引用,可是聚合外部若是要訪問聚合內部的對象時,必須經過聚合根開始導航,絕對不能繞過聚合根直接訪問聚合內的對象,也就是說聚合根是外部能夠保持 對它的引用的惟一元素;
  3. 聚合內除根之外的其餘實體的惟一標識都是本地標識,也就是隻要在聚合內部保持惟一便可,由於它們老是從屬於這個聚合的;
  4. 聚合根負責與外部其餘對象打交道並維護本身內部的業務規則;
  5. 基於聚合的以上概念,咱們能夠推論出從數據庫查詢時的單元也是以聚合爲一個單元,也就是說咱們不能直接查詢聚合內部的某個非根的對象;
  6. 聚合內部的對象能夠保持對其餘聚合根的引用;
  7. 刪除一個聚合根時必須同時刪除該聚合內的全部相關對象,由於他們都同屬於一個聚合,是一個完整的概念;

關於如何識別聚合以及聚合根的問題:

我以爲咱們能夠先從業務的角度深刻思考,而後慢慢分析出有哪些對象是:

  1. 有獨立存在的意義,即它是不依賴於其餘對象的存在它纔有意義的;
  2. 能夠被獨立訪問的,仍是必須經過某個其餘對象導航獲得的;

如何識別聚合?

我以爲這個須要從業務的角度深刻分析哪些對象它們的關係是內聚的,即咱們會把他們當作是一個總體來考慮的;而後這些對象咱們就能夠把它們放在一個聚合內。所謂關係是內聚的,是指這些對象之間必須保持一個固定規則,固定規則是指在數據變化時必須保持不變的一致性規則。當咱們在修改一個聚合時,咱們必須在事務級別確保整個聚合內的全部對象知足這個固定規則。做爲一條建議,聚合儘可能不要太大,不然即使可以作到在事務級別保持聚合的業務規則完整性,也可能會帶來必定的性能問題。有分析報告顯示,一般在大部分領域模型中,有70%的聚合一般只有一個實體,即聚合根,該實體內部沒有包含其餘實體,只包含一些值對象;另外30%的聚合中,基本上也只包含兩到三個實體。這意味着大部分的聚合都只是一個實體,該實體同時也是聚合根。

如何識別聚合根?

若是一個聚合只有一個實體,那麼這個實體就是聚合根;若是有多個實體,那麼咱們能夠思考聚合內哪一個對象有獨立存在的意義而且能夠和外部直接進行交互。

工廠(Factory)

DDD中的工廠也是一種體現封裝思想的模式。DDD中引入工廠模式的緣由是:有時建立一個領域對象是一件比較複雜的事情,不只僅是簡單的new操做。正如對象封裝了內部實現同樣(咱們無需知道對象的內部實現就可使用對象的行爲),工廠則是用來封裝建立一個複雜對象尤爲是聚合時所需的知識,工廠的做用是將建立對象的細節隱藏起來。客戶傳遞給工廠一些簡單的參數,而後工廠能夠在內部建立出一個複雜的領域對象而後返回給客戶。領域模型中其餘元素都不適合作這個事情,因此須要引入這個新的模式,工廠。工廠在建立一個複雜的領域對象時,一般會知道該知足什麼業務規則(它知道先怎樣實例化一個對象,而後在對這個對象作哪些初始化操做,這些知識就是建立對象的細節),若是傳遞進來的參數符合建立對象的業務規則,則能夠順利建立相應的對象;可是若是因爲參數無效等緣由不能建立出指望的對象時,應該拋出一個異常,以確保不會建立出一個錯誤的對象。固然咱們也並不老是須要經過工廠來建立對象,事實上大部分狀況下領域對象的建立都不會太複雜,因此咱們只須要簡單的使用構造函數建立對象就能夠了。隱藏建立對象的好處是顯而易見的,這樣能夠不會讓領域層的業務邏輯泄露到應用層,同時也減輕了應用層的負擔,它只須要簡單的調用領域工廠建立出指望的對象便可。

倉儲(Repository)

  1. 倉儲被設計出來的目的是基於這個緣由:領域模型中的對象自從被建立出來後不會一直留在內存中活動的,當它不活動時會被持久化到數據庫中,而後當須要的時候咱們會重建該對象;重建對象就是根據數據庫中已存儲的對象的狀態從新建立對象的過程;因此,可見重建對象是一個和數據庫打交道的過程。從更廣義的角度來理解,咱們常常會像集合同樣從某個相似集合的地方根據某個條件獲取一個或一些對象,往集合中添加對象或移除對象。也就是說,咱們須要提供一種機制,能夠提供相似集合的接口來幫助咱們管理對象。倉儲就是基於這樣的思想被設計出來的;
  2. 倉儲裏面存放的對象必定是聚合,緣由是以前提到的領域模型中是以聚合的概念去劃分邊界的;聚合是咱們更新對象的一個邊界,事實上咱們把整個聚合當作是一個總體概念,要麼一塊兒被取出來,要麼一塊兒被刪除。咱們永遠不會單獨對某個聚合內的子對象進行單獨查詢或作更新操做。所以,咱們只對聚合設計倉儲。
  3. 倉儲還有一個重要的特徵就是分爲倉儲定義部分和倉儲實現部分,在領域模型中咱們定義倉儲的接口,而在基礎設施層實現具體的倉儲。這樣作的緣由是:因爲倉儲背後的實現都是在和數據庫打交道,可是咱們又不但願客戶(如應用層)把重點放在如何從數據庫獲取數據的問題上,由於這樣作會致使客戶(應用層)代碼很混亂,極可能會所以而忽略了領域模型的存在。因此咱們須要提供一個簡單明瞭的接口,供客戶使用,確保客戶能以最簡單的方式獲取領域對象,從而可讓它專心的不會被什麼數據訪問代碼打擾的狀況下協調領域對象完成業務邏輯。這種經過接口來隔離封裝變化的作法其實很常見。因爲客戶面對的是抽象的接口並非具體的實現,因此咱們能夠隨時替換倉儲的真實實現,這頗有助於咱們作單元測試。
  4. 儘管倉儲能夠像集合同樣在內存中管理對象,可是倉儲通常不負責事務處理。通常事務處理會交給一個叫「工做單元(Unit Of Work)」的東西。關於工做單元的詳細信息我在下面的討論中會講到。
  5. 另外,倉儲在設計查詢接口時,可能還會用到規格模式(Specification Pattern),我見過的最厲害的規格模式應該就是LINQ以及DLINQ查詢了。通常咱們會根據項目中查詢的靈活度要求來選擇適合的倉儲查詢接口設計。一般狀況下只須要定義簡單明瞭的具備固定查詢參數的查詢接口就能夠了。只有是在查詢條件是動態指定的狀況下才可能須要用到Specification等模式。

設計領域模型的通常步驟

  1. 根據需求創建一個初步的領域模型,識別出一些明顯的領域概念以及它們的關聯,關聯能夠暫時沒有方向但須要有(1:1,1:N,M:N)這些關係;能夠用文字精確的沒有歧義的描述出每一個領域概念的涵義以及包含的主要信息;
  2. 分析主要的軟件應用程序功能,識別出主要的應用層的類;這樣有助於及早發現哪些是應用層的職責,哪些是領域層的職責;
  3. 進一步分析領域模型,識別出哪些是實體,哪些是值對象,哪些是領域服務;
  4. 分析關聯,經過對業務的更深刻分析以及各類軟件設計原則及性能方面的權衡,明確關聯的方向或者去掉一些不須要的關聯;
  5. 找出聚合邊界及聚合根,這是一件頗有難度的事情;由於你在分析的過程當中每每會碰到不少模棱兩可的難以清晰判斷的選擇問題,因此,須要咱們平時一些分析經驗的積累才能找出正確的聚合根;
  6. 爲聚合根配備倉儲,通常狀況下是爲一個聚合分配一個倉儲,此時只要設計好倉儲的接口便可;
  7. 走查場景,肯定咱們設計的領域模型可以有效地解決業務需求;
  8. 考慮如何建立領域實體或值對象,是經過工廠仍是直接經過構造函數;
  9. 停下來重構模型。尋找模型中以爲有些疑問或者是蹩腳的地方,好比思考一些對象應該經過關聯導航獲得仍是應該從倉儲獲取?聚合設計的是否正確?考慮模型的性能怎樣,等等;

領域建模是一個不斷重構,持續完善模型的過程,你們會在討論中將變化的部分反映到模型中,從而是模型不斷細化並朝正確的方向走。領域建模是領域專家、設計人員、開發人員之間溝通交流的過程,是你們工做和思考問題的基礎。

在分層架構中其餘層如何與領域層交互

從經典的領域驅動設計分層架構中能夠看出,領域層的上層是應用層,下層是基礎設施層。那麼領域層是如何與其它層交互的呢?

對於會影響領域層中領域對象狀態的應用層功能

通常應用層會先啓動一個工做單元,而後:

  1. 對於修改領域對象的狀況,經過倉儲獲取領域對象,調用領域對象的相關業務方法以完成業務邏輯處理;
  2. 對於新增領域對象的狀況,經過構造函數或工廠建立出領域對象,若是須要還能夠繼續對該新建立的領域對象作一些操做,而後把該新建立的領域對象添加到倉儲中;
  3. 對於刪除領域對象的狀況,能夠先把領域對象從倉儲中取出來,而後將其從倉儲中刪除,也能夠直接傳遞一個要刪除的領域對象的惟一標識給倉儲通知其移除該惟一標識對應領域對象;
  4. 若是一個業務邏輯涉及到多個領域對象,則調用領域層中的相關領域服務完成操做;

注意,以上所說的全部領域對象都是隻聚合根,另外在應用層須要獲取倉儲接口以及領域服務接口時,均可以經過IOC容器獲取。最後通知工做單元提交事務從而將全部相關的領域對象的狀態以事務的方式持久化到數據庫;

關於Unit of Work(工做單元)的幾種實現方法

  1. 基於快照的實現,即領域對象被取出來後,會先保存一個備份的對象,而後當在作持久化操做時,將最新的對象的狀態和備份的對象的狀態進行比較,若是不相同,則認爲有作過修改,而後進行持久化;這種設計的好處是對象不用告訴工做單元本身的狀態修改了,而缺點也是顯而易見的,那就是性能可能會低,備份對象以及比較對象的狀態是否有修改的過程在當對象自己很複雜的時候,每每是一個比較耗時的步驟,並且要真正實現對象的深拷貝以及判斷屬性是否修改仍是比較困難的;
  2. 不基於快照,而是倉儲的相關更新或新增或刪除接口被調用時,倉儲通知工做單元某個對象被新增了或更新了或刪除了。這樣工做單元在作數據持久化時也一樣能夠知道須要持久化哪些對象了;這種方法理論上不須要ORM框架的支持,對領域模型也沒有任何傾入性,同時也很好的支持了工做單元的模式。對於不想用高級ORM框架的朋友來講,這種方法挺好;
  3. 不基於快照,也不用倉儲告訴工做單元數據更改了。而是採用AOP的思想,採用透明代理的方式進行一個攔截。在NHibernate中,咱們的屬性一般要被聲明爲virtual的,一個緣由就是NHibernate會生成一個透明代理,用於攔截對象的屬性被修改時,自動通知工做單元對象的狀態被更新了。這樣工做單元也一樣知道須要持久化哪些對象了。這種方法對領域模型的傾入性不大,而且能很好的支持工做單元模式,若是用NHibernate做爲ORM,這種方法用的比較多;
  4. 通常是微軟用的方法,那就是讓領域對象實現.NET框架中的INotifiyPropertyChanged接口,而後在每一個屬性的set方法的最後一行調用OnPropertyChanged的方法從而顯示地通知別人本身的狀態修改了。這種方法相對來講對領域模型的傾入性最強。

對於不會影響領域層中領域對象狀態的查詢功能

能夠直接經過倉儲查詢出所須要的數據。但通常領域層中的倉儲提供的查詢功能也許不能知足界面顯示的須要,則可能須要屢次調用不一樣的倉儲才能獲取所須要顯示的數據;其實針對這種查詢的狀況,我在後面會講到能夠直接經過CQRS的架構來實現。即對於查詢,咱們能夠在應用層不調用領域層的任何東西,而是直接經過某個其餘的用另外的技術架構實現的查詢引擎來完成查詢,好比直接經過構造參數化SQL的方式從數據庫一個表或多個表中查詢出任何想要顯示的數據。這樣不只性能高,也能夠減輕領域層的負擔。領域模型不太適合爲應用層提供各類查詢服務,由於每每界面上要顯示的數據是不少對象的組合信息,是一種非對象概念的信息,就像報表;

爲何面向對象比面向過程更能適應業務變化

對象將需求用類一個個隔開,就像用儲物箱把東西一個個封裝起來同樣,需求變了,分幾種狀況,最嚴重的是大變,那麼每一個儲物箱都要打開改,這種方法就不見得有好處;可是這種狀況發生機率比較小,大部分需求變化都是侷限在一兩個儲物箱中,那麼咱們只要打開這兩個儲物箱修改就能夠,不會影響其餘儲物櫃了。

而面向過程是把全部東西都放在一個大儲物箱中,修改某個部分之後,會引發其餘部分不穩定,一個BUG修復,引起新的無數BUG,最後程序員陷入焦頭爛額,如日本東京電力公司員工處理核危機同樣,心力交瘁啊。

因此,咱們不能粗粒度看需求變,認爲需求變了,就是大範圍變,萬事萬物都有邊界,老子說,無慾觀其繳,什麼事物都要觀察其邊界,雖然需求能夠用「需求」這個名詞表達,談到需求變了,不都意味着最大邊界範圍的變化,這樣看問題容易走極端。

其實就是就地畫圈圈——邊界。咱們小時候寫做文分老三段也是一樣道理,各自職責明確,劃分邊界明確,經過過渡句實現承上啓下——接口。爲何組織須要分不一樣部門,一樣是邊界思惟。畫圈圈容易,但如何畫才難,因此OO中思惟很是重要。

需求變化所引發的變化是有邊界,若果變化的邊界等於整個領域,那麼已是徹底不一樣的項目了。要掌握邊界,是須要大量的領域知識的。不然,走進銀行連業務職責都分不清的,如何畫圈圈呢?

面向過程是無邊界一詞的(就算有也只是最大的邊界),它沒有要求各自獨立,它能夠橫跨邊界進行調用,這就是容易引發BUG的緣由,引發BUG不必定是技術錯誤,更多的是邏輯錯誤。分別封裝就是畫圈圈了,全部邊界都以接口實現。不用改或者小改接口,都不會牽一髮動全身。若果面向過程當中考慮邊界,那麼也就已經上升到OO思惟,即便用的不是對象語言,但對象已經隱含其中。說白了,面向對象與面向過程最大區別就是:分解。邊界的分解。從需求到最後實現都貫穿。

面向對象的實質就是邊界劃分,封裝,不但對需求變化可以量化,縮小影響面;由於邊界劃分也會限制出錯的影響範圍,因此OO對軟件後期BUG等出錯也有好處。

軟件世界永遠都有BUG,BUG是清除不乾淨的,就像人類世界永遠都存在不完美和陰暗面,問題關鍵是:上帝用空間和時間的邊界把人類世界痛苦災難等不完美侷限在一個範圍內;而軟件世界若是你不採起OO等方法進行邊界劃分的話,一旦出錯,追查起來狀況會有多糟呢?

軟件世界其實相似人類現實世界,有時出問題了,探究緣由一看,原來是兩個看上去毫無聯繫的因素致使的,古人只好常常求神拜佛,咱們程序員在本身的軟件上線運行時,大概內心也在求神拜佛別出大紕漏,若是咱們的軟件採起OO封裝,咱們就會坦然些,確定會出錯,可是咱們已經預先劃定好邊界,因此,不會產生嚴重後果,甚至也不會出現難以追查的魔鬼BUG。

領域驅動設計的其餘一些主題

上面只是涉及到DDD中最基本的內容,DDD中還有不少其餘重要的內容在上面沒有提到,如:

  1. 模型上下文、上下文映射、上下文共享;
  2. 如何將分析模式和設計模式運用到DDD中;
  3. 一些關於柔性設計的技巧;
  4. 若是保持模型完整性,以及持續集成方面的知識;
  5. 如何精煉模型,識別核心模型以及通用子領域;

這些主題都很重要,由於篇幅有限以及我目前掌握的知識也有限,而且爲了突出這篇文章的重點,因此不對他們作詳細介紹了,你們有興趣的能夠本身閱讀一下。

一些相關的擴展閱讀

CQRS架構

核心思想是將應用程序的查詢部分和命令部分徹底分離,這兩部分能夠用徹底不一樣的模型和技術去實現。好比命令部分能夠經過領域驅動設計來實現;查詢部分能夠直接用最快的非面向對象的方式去實現,好比用SQL。這樣的思想有不少好處:

  1. 實現命令部分的領域模型不用常常爲了領域對象可能會被如何查詢而作一些折中處理;
  2. 因爲命令和查詢是徹底分離的,因此這兩部分能夠用不一樣的技術架構實現,包括數據庫設計均可以分開設計,每一部分能夠充分發揮其長處;
  3. 高性能,命令端由於沒有返回值,能夠像消息隊列同樣接受命令,放在隊列中,慢慢處理;處理完後,能夠經過異步的方式通知查詢端,這樣查詢端能夠作數據同步的處理;

Event Sourcing(事件溯源)

對於DDD中的聚合,不保存聚合的當前狀態,而是保存對象上所發生的每一個事件。當要重建一個聚合對象時,能夠經過回溯這些事件(即讓這些事件從新發生)來讓對象恢復到某個特定的狀態;由於有時一個聚合可能會發生不少事件,因此若是每次要在重建對象時都從頭回溯事件,會致使性能低下,因此咱們會在必定時候爲聚合建立一個快照。這樣,咱們就能夠基於某個快照開始建立聚合對象了。

DCI架構

DCI架構強調,軟件應該真實的模擬現實生活中對象的交互方式,代碼應該準確樸實的反映用戶的心智模型。在DCI中有:數據模型、角色模型、以及上下文這三個概念。數據模型表示程序的結構,目前咱們所理解的DDD中的領域模型能夠很好的表示數據模型;角色模型表示數據如何交互,一個角色定義了某個「身份」所具備的交互行爲;上下文對應業務場景,用於實現業務用例,注意是業務用例而不是系統用例,業務用例只與業務相關;軟件運行時,根據用戶的操做,系統建立相應的場景,並把相關的數據對象做爲場景參與者傳遞給場景,而後場景知道該爲每一個對象賦予什麼角色,當對象被賦予某個角色後就真正成爲有交互能力的對象,而後與其餘對象進行交互;這個過程與現實生活中咱們所理解的對象是一致的;

DCI的這種思想與DDD中的領域服務所作的事情是同樣的,但實現的角度有些不一樣。DDD中的領域服務被建立的出發點是當一些職責不太適合放在任何一個領域對象上時,這個職責每每對應領域中的某個活動或轉換過程,此時咱們應該考慮將其放在一個服務中。好比資金轉賬的例子,咱們應該提供一個資金轉賬的服務,用來對應領域中的資金轉賬這個領域概念。可是領域服務內部作的事情是協調多個領域對象完成一件事情。所以,在DDD中的領域服務在協調領域對象作事情時,領域對象每每是處於一個被動的地位,領域服務通知每一個對象要求其作本身能作的事情,這樣就好了。這個過程當中咱們彷佛看不到對象之間交互的意思,由於整個過程都是由領域服務以面向過程的思惟去實現了。而DCI則通用引入角色,賦予角色以交互能力,而後讓角色之間進行交互,從而可讓咱們看到對象與對象之間交互的過程。但前提是,對象之間確實是在交互。由於現實生活中並非全部的對象在作交互,好比有A、B、C三個對象,A通知B作事情,A通知C作事情,此時能夠認爲A和B,A和C之間是在交互,可是B和C之間沒有交互。因此咱們須要分清這種狀況。資金轉賬的例子,A至關於轉賬服務,B至關於賬號1,C至關於賬號2。所以,資金轉賬這個業務場景,用領域服務比較天然。有人認爲DCI能夠替換DDD中的領域服務,我持懷疑態度。

四色原型分析模式

時刻-時間段原型(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的基礎之上再去學習這些東西會更加有效和容易掌握。

但願本文對你們有所幫助。

相關文章
相關標籤/搜索