對領(lǐng)域驅(qū)動設(shè)計的理解與實踐(對領(lǐng)域驅(qū)動設(shè)計的理解與實踐怎么寫)
領(lǐng)域驅(qū)動設(shè)計(domain-Driven-Design)是一種針對大型復雜系統(tǒng)的領(lǐng)域建模與分析方法論。
2003 年,Eric Evans 發(fā)布《Domain-Driven Design: Tackling Complexity in the Heart of Software》(領(lǐng)域驅(qū)動設(shè)計:軟件核心復雜性應(yīng)對之道),其中定義了DDD。
DDD改變了傳統(tǒng)軟件開發(fā)針對數(shù)據(jù)庫進行的建模方法;DDD先對業(yè)務(wù)領(lǐng)域進行分析,建立領(lǐng)域模型,根據(jù)領(lǐng)域模型驅(qū)動代碼設(shè)計。合理運用面向?qū)ο蟮母邇?nèi)聚低耦合設(shè)計要素,降低整個系統(tǒng)的業(yè)務(wù)復雜性,并使得系統(tǒng)具有更好的擴展性,更好的應(yīng)對多變的業(yè)務(wù)需求。
領(lǐng)域 Domain
一個領(lǐng)域就是一個問題域,只要是同一個領(lǐng)域,那問題域就相同。
只要確定了系統(tǒng)所屬的領(lǐng)域,那么這個系統(tǒng)的核心業(yè)務(wù),即要解決的問題以及問題的邊界就基本確定了。
舉例
陌生人社交領(lǐng)域,包含有聊天,用戶推薦,朋友圈等核心環(huán)節(jié)。 只要是這個領(lǐng)域,一般都會有這些相同的核心業(yè)務(wù),因為他們要解決問題的本質(zhì)是一樣的,就是交友。
每一個領(lǐng)域,都有一個對應(yīng)的領(lǐng)域模型,領(lǐng)域模型能夠很好的幫我們解決復雜的業(yè)務(wù)問題。
驅(qū)動設(shè)計
在DDD中,以領(lǐng)域(domain)為邊界,分析領(lǐng)域的核心問題,再設(shè)計對應(yīng)的領(lǐng)域模型,最后通過領(lǐng)域模型驅(qū)動代碼設(shè)計的實現(xiàn)。這樣設(shè)計的系統(tǒng)才有合理的分層與解耦,對以后業(yè)務(wù)的迭代開發(fā),代碼的維護才更加容易。
然而很多互聯(lián)網(wǎng)公司,為了追求快速的上線,都是模型都沒有想清楚就開始寫代碼,這就導致了后續(xù)代碼維護困難,無法擴展。修改bug同時又引入新的bug,反反復復,進入惡性循環(huán)。
當然,這跟梳理清楚領(lǐng)域模型需要一定時間,這與初創(chuàng)型的互聯(lián)網(wǎng)公司需求快速上線有點相悖,但是,這點時間的投入是非常值得的。因為可以避免了系統(tǒng)上線后不久又得重構(gòu)的問題。
概念總結(jié)
- 領(lǐng)域就是問題域
- 模型驅(qū)動的思想:通過建立領(lǐng)域模型來解決領(lǐng)域中的核心問題
- 領(lǐng)域建模的目標:針對我們在領(lǐng)域中核心問題,而不是整個領(lǐng)域中的所有問題
- 領(lǐng)域模型設(shè)計:設(shè)計時應(yīng)考慮一定的抽象性、通用性,以及復用價值
- 代碼實現(xiàn):通過領(lǐng)域模型驅(qū)動代碼的實現(xiàn),確保代碼讓領(lǐng)域模型落地,代碼最終能解決問題
為什么需要DDD
系統(tǒng)復雜性
耦合
隨著產(chǎn)品不斷的迭代,業(yè)務(wù)邏輯變得越來越復雜,系統(tǒng)也越來越龐大。模塊彼此互相關(guān)聯(lián)、耦合。導致增加或修改一個功能變得異常艱難,同時功能間的界限也變得模糊,職責不再清晰。這個時候就需要進行重構(gòu),拆分。
雖然架構(gòu)本身是隨著業(yè)務(wù)進行不斷演進的;但是,如果架構(gòu)初始設(shè)計不體現(xiàn)出業(yè)務(wù)的模型,那么新需求就無法體現(xiàn)在現(xiàn)有架構(gòu)上,導致不斷腐化,不斷重構(gòu)。
內(nèi)聚
貧血模型 Anemic Domain Object
domain object僅用作數(shù)據(jù)載體,而沒有行為和動作的領(lǐng)域?qū)ο蟆?/p>
指領(lǐng)域?qū)ο罄镏挥術(shù)et和set方法,沒有相關(guān)領(lǐng)域?qū)ο蟮臉I(yè)務(wù)邏輯。業(yè)務(wù)邏輯放在業(yè)務(wù)層。
充血模型 Rich Domain Object
將業(yè)務(wù)邏輯和對象存儲放在domain object里面,業(yè)務(wù)層只是簡單進行小部分業(yè)務(wù)的封裝及其他domain的編排。
面向?qū)ο笤O(shè)計,符合單一職責設(shè)計。
貧血 vs 充血
貧血模型的domain object很輕量,這導致業(yè)務(wù)層的復雜,domain object相關(guān)的業(yè)務(wù)邏輯散布在各個業(yè)務(wù)層,造成業(yè)務(wù)邏輯的冗余以及原本domain object的定義就變得相對模糊,這就是貧血癥引起的失憶癥。
而采用領(lǐng)域開發(fā)的方式,將數(shù)據(jù)和行為封裝在一起,與業(yè)務(wù)對象相映射;領(lǐng)域?qū)ο舐氊熐逦瑢⑾嚓P(guān)業(yè)務(wù)聚合到領(lǐng)域?qū)ο髢?nèi)部。
微服務(wù)
DDD 的本質(zhì)是一種軟件設(shè)計方法論,而微服務(wù)架構(gòu)是具體的實現(xiàn)方式。微服務(wù)架構(gòu)并沒有定義對復雜系統(tǒng)進行分解的具體方法論,而 DDD 正好就是解決方案。
微服務(wù)架構(gòu)強調(diào)從業(yè)務(wù)維度來分治系統(tǒng)的復雜度,而DDD也是同樣的著重業(yè)務(wù)視角。
DDD能帶來什么
- 建立通用語言: 圍繞領(lǐng)域模型建立的一種語言,團隊所有成員都使用這種語言進行溝通和活動
- 驅(qū)動代碼設(shè)計:領(lǐng)域建立模型,模型指導設(shè)計,設(shè)計產(chǎn)出代碼
- 解決核心問題:模型的設(shè)計中心就是核心域,就是解決核心的問題
DDD建模
戰(zhàn)略設(shè)計
戰(zhàn)略設(shè)計就是從宏觀角度對領(lǐng)域進行建模。劃分出業(yè)務(wù)的邊界,組織架構(gòu),系統(tǒng)架構(gòu)。
DDD中,對系統(tǒng)的劃分是基于領(lǐng)域的,也是基于業(yè)務(wù)的。
通用語言Ubiquitous Language
通用語言是指確定統(tǒng)一的領(lǐng)域術(shù)語,提高開發(fā)人員與領(lǐng)域?qū)<抑g的溝通效率。
一旦確定了統(tǒng)一語言,無論是與領(lǐng)域?qū)<业挠懻?,還是最終的實現(xiàn)代碼,都可以通過使用相同的術(shù)語,清晰準確地定義領(lǐng)域知識。
當確認整個團隊統(tǒng)一的語言后,就可以開始進行領(lǐng)域建模。
領(lǐng)域和子域
領(lǐng)域Domain
一個領(lǐng)域本質(zhì)上可以理解為就是一個問題域。只要我們確定了系統(tǒng)所屬的領(lǐng)域,那這個系統(tǒng)的核心業(yè)務(wù),即要解決的關(guān)鍵問題、問題的范圍邊界就基本確定了。
舉例
- 社交領(lǐng)域:關(guān)鍵問題是用戶推薦,聊天
- 電商領(lǐng)域:關(guān)鍵問題是購物,訂單,物流
子域Subdomain
如果一個領(lǐng)域過于復雜,涉及到的領(lǐng)域概念、業(yè)務(wù)規(guī)則、交互流程太多,導致沒辦法直接針對這個大的領(lǐng)域進行領(lǐng)域建模。這時就需要將領(lǐng)域進行拆分,本質(zhì)上就是把大問題拆分為小問題,把一個大的領(lǐng)域劃分為了多個小的領(lǐng)域(子域),那最關(guān)鍵的就是要理清每個子域的邊界
子域可以根據(jù)自身重要性和功能屬性劃分為三類子域:
- 核心域:公司核心產(chǎn)品和業(yè)務(wù)的領(lǐng)域
- 支撐子域:不包含決定產(chǎn)品和公司核心競爭力的功能,也不包含通用功能的子域
- 通用子域:被多個子域使用的通用功能子域
每個領(lǐng)域的劃分都不一樣。對相同領(lǐng)域公司而言,其核心,支撐,通用的子域也可能有不一樣的地方,但大體上基本都是一樣的。
舉例
社交領(lǐng)域的劃分
- 核心域:用戶推薦,聊天
- 支撐子域:客服,反垃圾
- 通用子域:消息推送
限界上下文Bounded Context
限界上下文
限界指劃分邊界,上下文對應(yīng)一個聚合,限界上下文可以理解為業(yè)務(wù)的邊界。
一個子域?qū)?yīng)一個或多個限界上下文。如果對應(yīng)多個上下文,則可以考慮子域是否要再進行細粒度的拆分。
限界上下文的目的是為了更加明確領(lǐng)域模型的職責和范圍
劃分限界上下文
三個原則:
- 概念相同,含義不同(通用語言):如果一個模型在一個上下文里面有歧義,那有歧義的地方就是邊界所在,應(yīng)該把它們拆到不同的限界上下文中。
- 外部系統(tǒng):有時候系統(tǒng)需要同外部系統(tǒng)交互,這時可以把與外部系統(tǒng)交互的那部分拆分出去以實現(xiàn)更好的擴展性。這樣一旦外部系統(tǒng)發(fā)生了變化,就不會影響到我們的核心業(yè)務(wù)邏輯。
- 組織擴展:盡量不要兩個團隊一起在一個限界上下文里面開發(fā),因為這樣可能會存在溝通不順暢、集成困難等問題。
組織架構(gòu)
康威定律
任何組織在設(shè)計一套系統(tǒng)時,所交付的設(shè)計方案在結(jié)構(gòu)上都與該組織的溝通結(jié)構(gòu)保持一致。
團隊結(jié)構(gòu)就是組織結(jié)構(gòu),限界上下文就是系統(tǒng)的業(yè)務(wù)結(jié)構(gòu)。所以,團隊結(jié)構(gòu)應(yīng)該盡量和限界上下文保持一致。
舉例
社交領(lǐng)域中,訂單子域?qū)?yīng)訂單上下文
上下文映射
從宏觀上看每個上下文之間的關(guān)系,可以更好理解各個上下文之間的依賴關(guān)系。
梳理清楚上下文之間的關(guān)系是為了:
- 任務(wù)更好拆分,一個開發(fā)人員可以全身心的投入到相關(guān)的一個單獨的上下文中
- 溝通更加順暢,一個上下文可以明確自己對其他上下文的依賴關(guān)系,從而使得團隊內(nèi)開發(fā)直接更好的對接
- 每個團隊在它的上下文中能夠更加明確自己領(lǐng)域內(nèi)的概念,因為上下文是領(lǐng)域的解系統(tǒng)
舉例
聊天上下文依賴消息推送,推廣上下文也依賴消息推送
戰(zhàn)術(shù)建模
戰(zhàn)術(shù)建模是從微觀角度對上下文進行建模。
梳理清楚聚合根,實體,值對象,領(lǐng)域服務(wù),領(lǐng)域事件,資源庫等。
實體Entity
當一個對象可以由標識進行區(qū)分時,這種對象稱為實體
和數(shù)據(jù)庫中的實體是不同的,這里的實體是從業(yè)務(wù)角度進行劃分的。
實體:
- 具有唯一標識
- 持久化
- 可變
舉例
社交中的用戶即為實體,可以通過用戶唯一的id進行區(qū)分。
值對象value object
當一個對象用于對事物進行描述而沒有唯一標識時,它被稱作值對象。
在實踐中,需要保證值對象創(chuàng)建后就不能被修改,即不允許外部再修改其屬性。
例如:年齡,聊天表情符號( :stuck_out_tongue:: 吐舌 (U 1F61B))
習慣了使用數(shù)據(jù)庫的數(shù)據(jù)建模后,很容易將所有對象看作實體
聚合根Aggregate Root
聚合是一組相關(guān)對象的集合,作為一個整體被外界訪問,聚合根是這個聚合的根節(jié)點。
聚合由根實體,值對象和實體組成。(聚合根里面有多少個實體,由領(lǐng)域建模決定)
外部對象需要訪問聚合內(nèi)的實體時,只能通過聚合根進行訪問,而不能直接訪問
舉例
一個訂單是一個聚合根,訂單購買的商品是實體,收貨地址是值對象。
領(lǐng)域服務(wù)Domain Service
領(lǐng)域服務(wù)
一些既不是實體,也不是值對象的范疇的領(lǐng)域行為或操作,可以放到領(lǐng)域服務(wù)中。用來處理業(yè)務(wù)邏輯,協(xié)調(diào)領(lǐng)域?qū)ο髞硗瓿上嚓P(guān)業(yè)務(wù)。
例如,有些業(yè)務(wù)邏輯不適合放到領(lǐng)域?qū)ο笾?,或?qū)嶓w之間的業(yè)務(wù)協(xié)調(diào),這些業(yè)務(wù)邏輯都可以放到領(lǐng)域服務(wù)中。
特征
- 與領(lǐng)域相關(guān)的操作如執(zhí)行一個業(yè)務(wù)操作過程,但它又并不適合放入實體與值對象中
- 操作是無狀態(tài)的
- 對領(lǐng)域?qū)ο筮M行轉(zhuǎn)換,或以多個領(lǐng)域?qū)ο笞鳛檩斎脒M行計算,結(jié)果產(chǎn)生一個值對象
當采用微服務(wù)架構(gòu)風格,一切領(lǐng)域邏輯的對外暴露均需要通過領(lǐng)域服務(wù)來進行。
如原本由聚合根暴露的業(yè)務(wù)邏輯也需要依托于領(lǐng)域服務(wù)。
舉例
必須通過訂單領(lǐng)域服務(wù)來創(chuàng)建和訪問訂單
領(lǐng)域事件
領(lǐng)域事件是對領(lǐng)域內(nèi)發(fā)生的活動進行的建模。捕獲一些有價值的領(lǐng)域活動事件。
作用
- 解耦:可以通過發(fā)布訂閱模式,發(fā)布領(lǐng)域事件
- 一致性:通過領(lǐng)域事件來達到最終一致性
- 事件溯源
舉例
發(fā)送聊天消息,這屬于一個領(lǐng)域事件;撤回消息,也屬于一個領(lǐng)域事件。
推送服務(wù)訂閱消息事件,然后將消息推送給用戶端。這樣就解耦了消息服務(wù)與推送服務(wù)之間的強依賴關(guān)系。
資源庫Repository
資源庫用于保存和獲取聚合對象。
領(lǐng)域模型 vs 數(shù)據(jù)模型
資源庫介于領(lǐng)域模型(業(yè)務(wù)模型)和數(shù)據(jù)模型(數(shù)據(jù)庫)之間,主要用于聚合對象的持久化和檢索。
資源庫隔離了領(lǐng)域模型和數(shù)據(jù)模型,以便上層只需要關(guān)注于領(lǐng)域模型而不需要考慮如何進行持久化。
分層架構(gòu)
把一系列相同的對象進行分類放在同一層,然后根據(jù)他們之間的依賴關(guān)系再確定上下層次關(guān)系。
在實際決策時,我們需要知道各層的職責、意義以及相應(yīng)的場景;
落實到代碼層面時,我們還需要知道各層所包含的具體內(nèi)容、各層的一些常見的具體策略/模式、層次之間的交互/依賴關(guān)系。
DDD經(jīng)典分層架構(gòu)
- 用戶接口層(interfaces):處理顯示和用戶請求,以及一些基本的參數(shù)檢查,不包括業(yè)務(wù)邏輯
- 應(yīng)用層(application):主要協(xié)調(diào)領(lǐng)域?qū)ο蟮牟僮?;處理持久化事?wù)、發(fā)送消息、安全認證等
- 領(lǐng)域?qū)樱╠omain):處理核心業(yè)務(wù)邏輯,不包括技術(shù)實現(xiàn)細節(jié)。領(lǐng)域?qū)邮菢I(yè)務(wù)軟件的核心
- 基礎(chǔ)設(shè)施層(infrastructure):處理純技術(shù)細節(jié),為其他層提供技術(shù)支撐,也可用于封裝調(diào)用的外部系統(tǒng)細節(jié)。例如:持久化的實現(xiàn),消息中間件的實現(xiàn),工具類,rpc等
個人理解:這種分層,既可以在一個單體應(yīng)用中,也可以是微服務(wù)的形式。DDD分層并不一定要按微服務(wù)的服務(wù)粒度進行分層。
如果一個業(yè)務(wù)邏輯非常簡單的子域,則可以將幾層都放進一個單體應(yīng)用中,在應(yīng)用中進行分層。如果業(yè)務(wù)較為復雜,則可以按服務(wù)進行拆分,每層都有自己對應(yīng)的服務(wù)。
其他架構(gòu)
- 對稱性架構(gòu)
- 洋蔥架構(gòu)
- 整潔架構(gòu)
- CQRS架構(gòu)
DDD工程實踐
以一個簡化的社交領(lǐng)域的例子來實踐DDD。
核心概念
- 用戶(User): 一個賬戶,并以用戶id識別
- 關(guān)系(Relationship):用戶之間的關(guān)系
- 動態(tài)(Feed): 用戶發(fā)布文字,圖片,視頻,評論等內(nèi)容
- 會話(Conversation):用戶之間的聊天會話
領(lǐng)域設(shè)計
戰(zhàn)略建模
領(lǐng)域就是社交領(lǐng)域,核心問題和絕大部分社交系統(tǒng)一樣。
子域
- 核心域:聊天,動態(tài)
- 支撐子域:反作弊,推廣
- 通用子域:用戶,關(guān)系,消息推送
上下文
- 消息上下文
- 會話上下文
- 動態(tài)上下文
- 推送上下文
- 用戶上下文
戰(zhàn)術(shù)建模
以會話上下文為例子來進行戰(zhàn)術(shù)建模
會話上下文
- 會話:聚合根用戶:實體用戶:實體消息列表:實體發(fā)送人:實體接收人:實體消息內(nèi)容:值對象
消息在會話上下文屬于實體,在消息上下文屬于聚合根。
結(jié)構(gòu)
以會話子域為例
架構(gòu)分層
- interfaces 接口層RESTfulRPC
- application 應(yīng)用層Conversationmessage
- domain_service 領(lǐng)域服務(wù)層modelConversationMessagerepositoryConversationMessage
- infrastructure 基礎(chǔ)設(shè)施層storeConversationRepositoryMessageRepositorymessageSendMessageutils
領(lǐng)域
package domain// 聚合根type Conversation struct { ID int User1 User User2 User Messages list.List}// 實體type Message struct { ID int From User // 實體 To User Body Content // 值對象}
用戶接口層
type ChatInferface struct { // 應(yīng)用層 app app.ChatApplication}func (c *ChatInferface) Route() { c.route("POST", "/api/message", c.SendMessage) c.route("PATCH", "/api/message", c.RecallMessage)}// POST /api/messagefunc (c *ChatInferface) SendMessage(ctx *Context) { if !c.validateRequest(ctx) { return } message := c.parseMessage(ctx) app.SendMessage(message)}func (c *ChatInferface) RecallMessage(ctx *Context) { if !c.validateRequest(ctx) { return } messageID := c.parseMessage(ctx) app.RecallMessage(messageID)}
應(yīng)用層
type ChatApplication struct { user service.UserService chat service.ChatService // 這里領(lǐng)域事件由應(yīng)用層發(fā)布 // publisher EventPublisher lbs LBSFacade}func (c *ChatApplication) SendMessage(msg *Message) { if !c.user.CheckUser(msg.UserID) { return } c.chat.SendMessage(msg)}
領(lǐng)域服務(wù)層
type ChatService struct { // 領(lǐng)域事件 publisher MessageEventPublisher repo MessageRepository}func (c *ChatService SendMessage(msg *Message) { // 業(yè)務(wù)邏輯 ... // 領(lǐng)域資源持久化 c.repo.Save(msg) // 發(fā)布領(lǐng)域事件 c.publisher.Publish(msg)}
基礎(chǔ)設(shè)施層
package infrastructuretype MessageRepository struct { db MessageDatabase cache MessageCache}func (m *MessageRepository) Save(msg *Message) { db.Save(m.ToPO(msg))}func (m MessageRepository) Get(msgID int) *Message { msg := m.cache.Get(msgID) if msg != nil { return m.FromPO(msg) } return m.FromPO(m.db.Get(msgID))}
總結(jié)
在設(shè)計和實現(xiàn)一個系統(tǒng)的時候,這個系統(tǒng)所要處理問題的領(lǐng)域?qū)<液烷_發(fā)人員以一套統(tǒng)一語言進行協(xié)作,共同完成該領(lǐng)域模型的構(gòu)建,在這個過程中,業(yè)務(wù)架構(gòu)和系統(tǒng)架構(gòu)等問題都得到了解決,之后將領(lǐng)域模型中關(guān)于系統(tǒng)架構(gòu)的主體映射為實現(xiàn)代碼,完成系統(tǒng)的實現(xiàn)落地。