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