你瞭解過領域驅動設計嗎?如何運用領域驅動設計來進行業務建模?

領域驅動設計與業務建模

好的軟體,來自於好的軟體設計。軟體設計是一門藝術,就像繪畫、寫作等其他藝術形式一樣,它不能透過定理和公式以一種精確科學的方式被教授和學習。雖然透過軟體建立的過程,可以發現和獲取到有用的規律和技巧,但是也許永遠無法提供一個準確的方法,以滿足從現實世界對映到程式碼模型的需要。如今,完成軟體設計的方法多種多樣,其中領域驅動設計(Domain DrivenDesign,DDD)正是透過對業務領域建模,完成業務知識與程式碼的對映,從而降低軟體開發的複雜性。在大型軟體中,DDD可以有效降低構建軟體的複雜性。

本節將介紹 DDD的基本概念,以及運用DDD來進行業務建模。

你瞭解過領域驅動設計嗎?如何運用領域驅動設計來進行業務建模?

什麼是通用語言

開發人員滿腦子都是類、方法、演算法、模式,總是想將實際生活中的概念和程式元件做對應。他們希望看到要建立哪些物件類,要如何對物件類之間的關係進行建模。他們會按照繼承、多型、面向物件的程式設計等方式去思考及交流,這對開發人員來說太正常不過了。但是領域專家通常對這一無所知。他們對軟體類庫、框架、持久化,甚至資料庫沒有什麼概念。他們只瞭解特有的專業業務技能。如果領域專家和技術人員之間進行討論,領域專家使用自己的行話,技術團隊成員在設計中也用自己的語言討論領域,那麼兩者將永遠也無法達成共識。

在設計過程中,開發人員傾向於使用自己熟悉的“方言”,但是沒有一種方言能成為通用的語言,因為它們都不能滿足所有人員的溝通需要。在討論模型和定義模型時,領域專家和開發人員確實需要講同一種語言。

領域驅動設計的一個核心原則是使用一種基於模型的語言。因為模型是軟體滿足領域的共同點,它很適合作為這種通用語言的構造基礎,這種語言稱為“通用語言(Ubiquitous Language)”。通用語言連線起設計中的所有的部分,是建立設計團隊良好工作的前提。

但這種語言的形成可不是一蹴而就的,它需要領域專家和開發人員坐在一起,不斷討論業務模組,從而慢慢演變成大家都可以理解的通用語言。通用語言的表達方式可以多種多樣,並無固定格式,可以是圖、UML、文件或程式碼。

總之,開發人員應該要理解通用語言的重要性,要建立起模型和語言之間的密切關聯。也必須要認識到,對語言的變更會造成對模型的變更,而模型的變更也意味著軟體也要跟著變更。

領域驅動設計的核心概念

下面介紹關於領域驅動設計中的一些核心概念。

1。模型驅動設計(Model Driven Design )

通用語言應該在建模過程中需要進行廣泛的嘗試,從而推動軟體專家和領域專家之間的溝通,以及發現要在模型中使用的主要的領域概念。建模過程的主要目的是建立一個優良的模型,而後將模型實現成程式碼。這是軟體開發過程中同等重要的兩個階段。

但從模型到程式碼這個過程的轉換並不簡單,一個看上去正確的模型並不代表模型能被直接轉換成程式碼。分析模型是業務領域分析的結果,其產生的模型並不會考慮軟體需要如何實現。這樣的一個模型可用來理解領域,因為它建立了特定級別的知識,而且模型看上去會很正確。問題是分析的時候不能預見模型中存在的某些缺陷及領域中所有的複雜關係。分析人員可能深人到了模型中某些元件的細節,但卻未深入到其他部分。非常重要的細節直到設計和實現過程才可能被發現。主要的模型雖然能夠如實反映領域知識,卻可能會導致物件持久化的一系列問題,或者導致不可接受的效能行為。而此時,開發人員會被迫做出自己的決定,做出設計變更以解決實際問題,而這個問題在模型建立時是沒有考慮到的。主要的結果是,他們建立了一個偏離模型的設計,讓模型和實現二者越來越不相關。所以選擇一個能夠被輕易和準確轉換成程式碼的模型變得很重要。那麼應該如何動手處理從模型到程式碼的轉換呢?

所以,最好的方案是,模型在構建時就考慮到軟體的設計,而開發人員要參與到整個建模的過程中來。這樣,就能夠選擇一個能恰當在軟體中表現的模型,設計過程會很順暢並且始終是忠於模型的。程式碼和其下的模型緊密關聯會讓程式碼更有意義。

任何技術人員想對模型做出貢獻,必須花費一些時間來接觸程式碼,無論他在專案中擔負的是什麼樣的角色。任何一個負責修改程式碼的人都必須學會用程式碼表現模型。每個開發人員都必須參與到一定級別的領域討論中,並和領域專家進行溝通。那些按不同方式貢獻的人必須自覺地與接觸程式碼的人使用通用語言,動態交換模型思想。因為對程式碼的一個變更就可能成為對模型的變更。

2。分層架構(Layered Architecture )

將應用劃分成分離的層並建立層間的交換規則很重要。如果程式碼沒有被清晰隔離到某層中,它會馬上變得混亂,變得非常難以管理和變更。在某處對程式碼的一個簡單修改會對其他地方的程式碼造成不可估量的結果。領域層應該關注核心的領域問題,它不應該涉及基礎設施類的活動。使用者介面既不跟業務邏輯緊密捆綁,也不包含通常屬於基礎設施層的任務。在很多情況下應用層是必要的,它會成為業務邏輯之上的管理者,用來監督和協調應用的整個活動。

一個典型的DDD分層架構如圖6-10所示。

你瞭解過領域驅動設計嗎?如何運用領域驅動設計來進行業務建模?

其中:

UI層:負責介面展示或使用者介面;

應用層:負責業務流程;

領域層:負責領域邏輯;

基礎設施層:負責提供基礎設施支援。

越往上層,變動越頻繁;越往下層,變動就會越少,越穩定。

3。實體(Entity )

實體是指帶有識別符號的物件,它的識別符號在歷經軟體的各種狀態後仍能保持一致。

如果有一個存放了天氣資訊(如溫度)的類,很容易產生同一個類的不同例項,這兩個例項都包含了同樣的值,這兩個物件是完全相等的,可以用其中一個與另一個交換,但它們擁有不同的引用,而且不是實體。

開發人員可能會建立一個Person類,這個類會帶有一系列的屬性,如名字、出生日期、出生地等。這些屬性中有哪個可以作為Person的識別符號嗎?名字不可以作為識別符號,因為可能有很多人擁有同一個名字。如果只考慮兩個人的名字,就不能使用同一個名字來區分他們兩個,也不能使用出生日期作為識別符號,因為會有很多人在同一天出生。同樣也不能用出生地作為識別符號。一個物件必須與其他的物件區分開來,即使是它們擁有相同的屬性。錯誤的識別符號可能會導致資料混亂。

因此,在軟體中實現實體意味著建立識別符號。對一個Person類而言,其識別符號可能是屬性的組合:名字、出生日期、出生地、父母名字、當前地址等。在中國,身份證號碼也會用來建立識別符號。

通常識別符號或者是物件的一個屬性(或屬性的組合),一個專門為儲存和表現識別符號而建立的屬性,抑或是一種行為。

有很多不同的方式來為每一個物件建立一個唯一的識別符號:可能由一個模型來自動產生ID,在軟體中內部使用,不會讓它對使用者可見;它可能是資料庫表的一個主鍵,會被保證在資料庫中是唯一的。只要物件從資料庫中被檢索,它的ID就會被檢索出來並在記憶體中被重建;ID也可能由使用者建立,如身份證號碼,每個人都會擁有一個唯一的字串ID,這個字串在中國範圍內是通用的。

另一種解決方案是使用物件的屬性來建立識別符號,當這個屬性不足以代表識別符號時,另一個屬性就會被加人以幫助確定每一個物件。

實體是領域模型中非常重要的物件,並且它們應該在建模過程開始時就被考慮。

4。值物件(Value Object)

實體是可以被跟蹤的,但跟蹤和建立識別符號需要一定的成本。開發人員不但需要保證每一個實體都有唯一標識,而且跟蹤標識也並非易事。需要花費很多精力來決定由什麼來構成一個識別符號,因為一個錯誤的決定可能會讓物件擁有相同的標識,而這顯然不是人們所期望的。將所有的物件視為實體也會帶來隱含的效能問題,因為需要對每個物件產生一個例項。

有時,人們對某個物件是什麼不感興趣,只關心它擁有的屬性。用來描述領域的特殊方面,且沒有識別符號的一個物件,稱為值物件。

區分實體物件和值物件非常必要。沒有識別符號,值物件就可以被輕易地建立或丟棄。在沒有其他物件引用時,垃圾回收會處理這個物件。這極大地簡化了設計,同時對於效能也是非常大的提升。

值物件由一個構造器建立,並且在它們的生命週期內永遠不會被修改。當希望一個物件擁有不同的值時,就會簡單地去建立另一個物件。這會對設計產生重要的結果。如果值物件保持不變,並且不具有識別符號,那麼它就可以被共享了。

所以,如果值物件是可共享的,那麼它們應該是不可變的。

5。服務( Service )

當開發人員分析領域並試圖定義構成模型的主要物件時,就會發現有些方面的領域是很難對映成物件的。

物件通常要考慮的是擁有屬性,物件會管理它的內部狀態並暴露行為。在開發通用語言時,領域中的主要概念被引入到語言中,語言中的名詞很容易被對映成物件。語言中對應那些名詞的動詞變成那些物件的行為。但是有些領域中的動作,它們是一些動詞,看上去卻不屬於任何物件。它們代表了領域中的一個重要的行為,所以不能忽略它們或簡單地把它們合併到某個實體或值物件中去。

給一個物件增加這樣的行為會破壞這個物件,讓它看上去擁有了本不該屬於它的功能。但是,要使用一種面嚮物件語言,就必須用到一個物件才行。它不能只擁有一個單獨的功能,而不附屬於任何物件。通常這種行為類的功能會跨越若干個物件,或許是不同的類。例如,為了從一個賬戶向另一個賬戶轉錢,這個功能應該放到轉出的賬戶還是在接收的賬戶中?感覺放在這兩個賬戶中的哪一個也不對。當這樣的行為從領域中被識別出來時,最佳實踐是將它宣告成一個服務。這樣的物件不再擁有內建的狀態了,它的作用是為了簡化所提供的領域功能。

一個服務應該不是對通常屬於領域物件操作的替代。開發人員不應該為每一個需要的操作建立一個服務。但是當一個操作凸顯為一個領域中的重要概念時,就需要為它建立一個服務了。以下是服務的三個特徵。

服務執行的操作涉及一個領域概念,這個領域概念通常不屬於一個實體或值物件。

被執行的操作涉及領域中的其他物件。

操作是無狀態的。

當領域中的一個重要的過程或變化不屬於一個實體或值物件的自然職責時,向模型中增加一個操作,作為一個單獨的介面將其宣告為一個服務。根據領域模型的語言定義一個介面,確保操作的名稱是通用語言的一部分。最後,應該讓服務變得無狀態。

6。模組(Module )

對一個大型的複雜專案而言,模型會趨向于越來越大。當模型最終大到作為整體也很難討論時,理解不同部件之間的關係和互動將變得很困難。基於此原因,非常有必要將模型以模組方式進行組織。模組被用來作為組織相關概念和任務,以便降低軟體複雜性的一種非常簡單有效的方法。

使用模組另一方面也可以提高程式碼質量。好的軟體程式碼應該具有高內聚性和低耦合度。雖然內聚開始於類和方法級別,但它其實也可以應用於模組級別。強烈推薦將高關聯度的類分組到一個模組,以提供儘可能大的內聚。

有多重內聚的方式。最常用到的是通訊性內聚(Communicational Cohesion)和功能性內聚(Functional Cohesion)。在模組中的部件操作相同的資料時,可以得到通訊性內聚。把它們分到一組很有意義,因為它們之間存在很強的關聯性。在模組中的部件協同工作以完成定義好的任務時,可以得到功能性內聚。功能性內聚一般被認為是最佳的內聚型別。

給定的模組名稱會成為通用語言的組成部分。模組和它們的名稱應該能夠反映對領域的深層理解。

7。聚合(Aggregate )

聚合是一種用來定義物件所有權和邊界的領域模式。

聚合是針對資料變化可以考慮成一個單元的一組關聯的物件。聚合使用邊界將內部和外部的物件劃分開來。每個聚合都有一個根。這個根是一個實體,並且它是外部可以訪問的唯一的物件。根物件可以持有對任意聚合物件的引用,其他的物件可以互相持有彼此的引用,但一個外部物件只能持有對根物件的引用。如果邊界內還有其他的實體,那些實體的識別符號是本地化的,只在聚合內有意義。

將實體和值物件聚集在聚合之中,並且定義各個聚合之間的邊界。為每個聚合選擇一個實體作為根,並且透過根來控制所有對邊界內的物件的訪問。允許外部物件僅持有對根的引用。對內部成員的臨時引用可以被傳遞出來,但是僅能用於單個操作之中。因為由根物件來進行訪問控制,將無法盲目地對內部物件進行變更。這種安排使強化聚合內物件的不變數變得可行,並且對聚合而言,它在任何狀態變更中都是作為一個整體。

8。資源庫(Repository )

在模型驅動設計中,物件從被建立開始,直到被刪除或被歸檔結束,是有一個生命週期的。一個建構函式或工廠可應用於處理物件的建立。建立物件的整體作用是為了使用它們。在一個面向物件的語言中,必須保持對一個物件的引用以便能夠使用它。為了獲得這樣的引用,客戶程式必須建立一個物件,或者透過導航已有的關聯關係從另一個物件中獲得它。例如,為了從一個聚合中獲得一個值物件,客戶程式需要向聚合的根傳送請求。問題是現在客戶程式必須先擁有一個對根的引用。

對大型的應用而言,這會變成一個問題,因為必須保證客戶始終對需要的物件保持一個引用,或者是對關注的物件保持引用。在設計中使用這樣的規則,將強制要求物件持有一系列它們可能其實並不需要保持的一系列的引用。這增加了物件間的耦合性,建立了一系列本不需要的關聯。

客戶程式需要有一個獲取已存在領域物件引用的實際方式。如果基礎設施讓這變得簡單,客戶程式的開發人員可能會增加更多的可導航的關聯,從而進一步使模型混亂。從另一方面講,他們可能會使用查詢從資料庫中獲取所需的資料,或者拿到幾個特定的物件,而不是透過聚合的根來遞迴。

領域邏輯分散到查詢和客戶程式碼中,實體和值物件變得更像是資料容器。應用到眾多資料庫訪問的基礎設施的技術複雜性會迅速蔓延在客戶程式碼中,開發人員不再關注領域層,所做的工作與模型也沒有任何關係了。最終的結果是放棄了對領域的關注,在設計上做了妥協。

使用資源庫的目的是封裝所有獲取物件引用所需的邏輯。領域物件無須處理基礎設施,便可以得到領域中對其他物件所需的引用。這種從資源庫中獲取引用的方式,可以讓模型重獲它應有的清晰和焦點。

資源庫會儲存對某些物件的引用。當一個物件被創建出來時,它可以被儲存到資源庫中,然後在以後使用時就可以從資源庫中檢索到。如果客戶程式從資源庫中請求一個物件,而資源庫中並沒有該物件時,就會從儲存介質中獲取它。換種說法是,資源庫作為一個全域性的可訪問物件的儲存點而存在。

不同型別的物件可以使用不同的儲存位置。最終結果是,領域模型同需要儲存的物件及它們的引用之間實現瞭解耦。領域模型可以訪問潛在的任何持久化基礎設施。

雖然看上去資源庫的實現可能會非常類似於基礎設施,但資源庫的介面是純粹的領域模型。

利用DDD 來進行微服務的業務建模

1。利用限界上下文來拆分微服務

DDD對微服務來說,一個重要的指導就是服務拆分。在DDD中,限界上下文(Bounded Con-text)主要用於確定業務流程的邊界,同樣也適用於微服務之間邊界的劃定。在一個好的限界上下文中,每一個微服務都應該只表示一個領域概念,無歧義且唯一。一個限界上下文並不一定包含在一個子域中,一個子域也可以包含多個上下文。對於一個領域中的限界上下文不是孤立存在的,而是透過多個限界上下文的協作完成業務的。

在設計API的時候,要拋棄以往以CURD操作為中心的設計,而應使用DDD策略,設計更加符合業務需求的介面。這樣,這些操作就會具有良好的定義。不管對於服務提供方還是客戶端來說,這樣的體驗都更好。服務提供方不再需要根據更新欄位來推測業務操作的意圖,業務操作清晰明瞭,這樣的程式碼更簡單,也更容易維護。而對於客戶端來說,它們能執行或不能執行哪些操作也是一目瞭然的。如果API需要具有良好的文件化,那麼可以結合使用Swagger工具,就可以很清楚地瞭解到API都具有哪些約束。

圖6-11展示了對於一個天氣預報系統而言,所需要劃分的限界上下文。

你瞭解過領域驅動設計嗎?如何運用領域驅動設計來進行業務建模?

整個系統可以分為天氣資料採集限界上下文、天氣資料API限界上下文、城市資料API限界上下文、天氣預報限界上下文。其中限界上下文又可以劃分為不同的元件,其中:

天氣資料採集限界上下文包含資料採集元件、資料儲存元件。資料採集元件是通用的用於採集天氣資料的元件。資料儲存元件是用於儲存天氣資料的元件;

天氣資料API限界上下文包含了天氣資料查詢元件。天氣資料查詢元件提供了天氣資料查詢的介面;

城市資料API限界上下文包含了城市資料查詢元件。城市資料查詢元件提供了城市資料查詢的介面;

天氣預報限界上下文包含了資料展示元件。資料展示元件用於將資料模型展示為使用者能夠理解的UI介面。

。使用領域事件進行服務間解耦

領域事件(Domain Events )是DDD中的一個概念,用於捕獲建模領域中所發生過的事情。那麼,什麼是領域事件?

例如,在使用者註冊過程中,有這麼一個業務要求,即“當用戶註冊成功之後,傳送一封確認郵件給客戶”。那麼,此時的“使用者註冊成功”便是一個領域事件。領域事件對業務的價值在於,有助於形成完整的業務閉環,即一個領域事件將導致進一步的業務操作。正如例子中的“使用者註冊成功”事件,會觸發一個傳送確認郵件給客戶的操作。

在微服務架構裡面,“使用者註冊”和“傳送郵件”可能是分佈於不同的微服務中,透過事件,將兩個服務的業務給串聯起來了。

簡而言之,透過引入領域事件,我們的軟體帶來如下好處。

幫助使用者深入理解領域模型:因為只有理解了領域模型,才能更好地設計領域事件。

解耦微服務:這也是最終的目的。事件就是為了更好地處理服務間的依賴。

領域事件的實現,往往依賴於訊息中介軟體系統。在本文的最後也介紹了一種“分散式訊息匯流排”的方式,來實現服務間的事件處理。

本篇文章給大家講解的內容是領域驅動設計與業務建模

下篇文章給大家講解天氣預報系統的微服務架構設計與實現;

覺得文章不錯的朋友可以轉發此文關注小編;

感謝大家的支援!!

承接上文

TAG: 物件模型領域一個識別符號