GitChat課程《領域驅動設計--戰略篇》筆記,課程作者張逸
一.理解限界上下文
1.限界上下文的定義
- 限界上下文:Bounded Context
1)上下文(Context)表現業務流程的場景片段
2)整個業務流程由諸多具有時序的活動組成,隨著流程的進行,不同的活動有不同的角色參與,并導致上下文切換
3)上下文(Context)其實是動態的業務流程被邊界(Bounded)靜態切分的產物 - 以咨詢師從成都到深圳為客戶提供服務的場景為例理解限界上下文
1)相同的人物在不同的上下文參與不同的活動,履行不同的職責
2)整個業務流程由諸多分散且目標不同的活動(Actions)組成,這些活動在同一個上下文中為同一目標提供服務
- 理解限界上下文的關鍵點
1)知識:不同的限界上下文需要不同的領域知識,這實際上就是業務相關性
2)角色:參與到這個上下文的對象扮演何種角色,各種角色如何協作
3)邊界 - 根據業務相關性、耦合強弱程度、分離的關注點對業務的活動進行歸類,找到不同類別之間存在的邊界,這就是限界上下文的含義
2.限界上下文的價值
- 對不同邊界的控制力
1)領域邏輯層面:限界上下文確定了領域模型的業務邊界,維護了模型的完整性與一致性,從而降低系統的業務復雜度
2)團隊合作層面:限界上下文確定了開發團隊的工作邊界,建立了團隊之間的合作模式,避免團隊之間的溝通混亂,從而降低系統開發的管理復雜度
3)技術實現層面:限界上下文確定了系統架構的應用邊界,保證了系統層和上下文領域層各自的一致性,建立了上下文之間的集成方式,從而降低系統的技術復雜度 - 限界上下文是滿足下述四個特點的自治單元
1)最小完備:自治單元履行的職責是完整的,同時避免添加不必要的職責
2)自我履行:由自治單元自身決定要做什么,對于不屬于自身的行為應轉交給其他上下文
3)穩定空間:減少外界變化對限界上下文內部的影響
4)獨立進化:減少限界上下文變化對外界的影響
3.限界上下文的控制力
- 限界上下文分離了業務邊界
如在電商系統中,產品實體Product在不同的限界上下文有不同的含義,關注的屬性與行為也不盡相同
1)在采購上下文,需要關注產品的進價、最小起訂量與供貨周期
2)在市場上下文,則關心產品的品質、售價,以及用于促銷的精美?片和銷售類型
3)在倉儲上下文,倉庫?作?員更關心產品的位置,重量與體積,是否易碎品以及訂購產品的數量
4)在推薦上下文,系統關注的是產品的類別、銷量、收藏數、正面評價數、負面評價數。
理想的設計方案是讓每一個限界上下文擁有自己的領域模型Product
每個限界上下文都有Product
- 限界上下文明確了工作邊界
1)2PTs(Two-Pizza Teams)規則:讓團隊保持在兩個披薩能讓成員吃飽的規模(7-10人)
2)康威定律(Conway's Law):任何組織在設計一套系統時,所交付的設計方案在結構上都與該組織的溝通結構保持一致
3)DDD按照軟件的特性(Feature)而非組件(Component)來組織軟件開發團隊
4)組件團隊:數據庫+前端+后端,更容易發揮每個人的技能特長,但導致團隊成員缺乏對業務的了解,任何修改都要橫跨多個組件團隊,溝通成本較高
5)特性團隊:端對端的開發垂直細分領域的跨職能團隊,將需求分析、架構設計、開發測試等多個角色糅合在一起,專注于領域邏輯
特性團隊和組件團隊結合組成的開發團隊
團隊應該和限界上下文重合
- 限界上下文封裝了應用邊界
劃分限界上下文不只從業務邊界確立,還要考慮控制技術復雜度,從而保證系統質量,例如
1)高并發:外賣系統的訂單業務與門店、支付等領域存在業務相關性,但外賣訂單在高峰期存在高并發壓力,將訂單業務作為一個單獨的限界上下文,可以從物理架構上保證獨立性,在資源分配上做到高優先級地擴展
2)功能重用:?個?向企業雇員的國際報稅系統,報稅業務、旅游業務與 Visa 業務都需要賬戶功能的?撐。系統對?戶的注冊與登錄有較為復雜的業務處理流程。從功能重用的角度考慮,應該將賬戶管理作為一個單獨的限界上下文,以滿足核心領域對賬戶管理的功能重用
3)實時性:電商系統中,價格是商品概念的一個重要屬性,僅僅從業務角度考慮,在進行領域建模時,價格僅僅是一個普通的領域值對象,但電商系統的商品數量達到數十億種,每天獲取商品信息的調用量在峰值達到數億乃至數百億次時,價格就從業務問題變成了技術問題。為了保證高并發下價格的實時性,可以將價格領域作為一個獨立的限界上下文,形成自己與眾不同的架構方案,以滿足高并發和實時性的要求
4)第三方服務集成:電商系統需要支持多種常見的支付渠道,如微信、支付寶、銀聯??梢詫⒅Ц斗占蓜澐譃橐粋€單獨的限界上下文,一方面為支付服務額客戶端提供完全統一的支付接口,保證接口調用上的便利性與一致性,另一方面解除第三方支付服務與電商系統內部模塊之間的耦合,避免“供應商鎖定”
5)遺留系統:將遺留系統劃分為一個限界上下文,由于新增需求與原有系統在業務上存在交叉功能,因而可能失去部分代碼的重用機會,但可以避免在新功能開發過程中陷入遺留系統龐大代碼庫的泥沼
二.識別限界上下文
- 從業務邊界、工作邊界、應用邊界三個層次識別
從三個層面識別限界上下文
1.從業務邊界識別限界上下文
- 在梳理主要業務流程后,抽象出不同的業務場景,最后總結出業務活動的描述
- 從語義相關性分析業務活動的描述,如果是相同的語義,可以作為歸類的特征
- 從功能角度去分析業務活動,如果存在關聯和依賴,可以作為歸類的特征
2.從工作邊界識別限界上下文
- 確定限界上下文合理的工作粒度
- 團隊之間是“滲透性邊界”,不能太封閉(拒絕外部輸入),也不能太開放(失去內聚力)
3.從應用邊界識別限界上下文
- 質量屬性:利用限界上下文將可能改變系統架構的風險控制在一個極小范圍內
- 重用和變化
1)運用重用原則分離的限界上下文對應于支撐子領域,作為上游上下文為其他上下文提供業務支撐
2)一個限界上下文不應該存在兩個引起它變化的原因 - 遺留系統:對遺留系統中需要替換的組件進行抽象,從而將消費者與遺留系統中的組件實現進行解耦,最后提供一個新的組件實現,在保留抽象層接口不變的情況下替換遺留系統的舊組件,完成技術棧遷移
遺留系統的組件遷移
三.上下文映射
1.上下文映射概述
- 領域驅動設計通過上下文映射(Context Map)來討論限界上下文之間的協作問題
- 上下文映射是一種設計手段,包括
1)共享內核(Shared Kernel)
2)防腐層(Anticorruption Layer)
3)開發主機服務(Open Host Service)等模式 - 兩個限界上下文之間的關系方向由術語上游(UpStream)和下游(DownStream)描述
1)限界上下文影響作用力的方向與程序員慣常理解的依賴方向相反,上游影響下游,意味下游依賴于上游
2)下游上下文中的用例才是核心領域,而上游限界上下文是下游限界上下文的功能支撐
3)例如在下訂單場景中,訂單上下文調用支付上下文,支付上下文是提供功能支撐的上游上下文,訂單上下文是下游上下文,下訂單是核心領域
限界上下文的上下游關系
- 上下文映射模式分為
1)團隊協作模式:對應于團隊(上下文)合作的工作邊界
2)通信集成模式:從應用邊界的角度分析了限界上下文之間該如何通信才能提升設計質量
2.上下文映射模式1:團隊協作
- 團隊協作應遵循“各司其職,權責分明”的模式,在滿足合理分配職責的前提下,謹慎地確保每個限界上下文的粒度
- DDD根據團隊協作的方式與緊密程度,定義了合作、共享內核、客戶方-供應方開發、遵奉者、分離方式五種團隊協作模式
- 合作關系(Partnership)
表示兩個限界上下文的團隊存在要么一起成功要么一起失敗的強耦合關系,甚至是糟糕的雙向依賴,對于這種槽糕的何種關系,通常有三種解決方法:
1)既然兩個限界上下文存在如此緊密的合作關系,說明與其拆分,不如讓它們合并在一起
2)將產生特性依賴的職責分配到正確的位置,盡力減少一個方向的多于依賴
3)識別產生雙向依賴或循環依賴的原因,然后將它從各個限界上下文中剝離出來,成為單獨的限界上下文,即所謂的“共享內核(Shared Kernel)” - 共享內核(Shared Kernel)
共享內核用于避免重復。這種重用以犧牲限界上下文自由更改的能力為代價 - 客戶方-供應方開發(Customer-Supplier Development)
團隊合作中最常見的合作模式,體現的是上游(供應方)與下游(客戶方)的合作關系。這種合作需要兩個團隊協商以下問題:
1)下游團隊對上游團隊提出的領域需求
2)上游團隊提供的服務采用什么樣的協議與調用方式
3)下游團隊針對上游服務的測試策略
4)上游團隊給下游團隊承諾的交付日期
5)當上游服務的協議或調用方式發生變更時,該如何控制變更 - 遵奉者(Conformist)
客戶方-供應方開發模式,是上游團隊滿足下游團隊提出的領域需求;而遵奉者模式,是由上游團隊來決定是響應還是拒絕下游團隊提出的請求。遵奉者模式意味著
1)可以直接重用上游上下文的模型(好的)
2)減少了兩個限界上下文之間模型的轉換成本(好的)
3)使得下游限界上下文對上游產生了模型上的強依賴(壞的) - 分離方式(Separate Ways)
分離方式的合作模式指兩個限界上下文沒有任何關系
例如在電商網站中,支付上下文與商品上下文就是分離方式,而貨幣上下文其實是支付上下文
支付上下文與商品上下文的分離方式
3.上下文映射模式2:通信集成
- 利用防腐層和開放主機服務降低限界上下文之間的耦合關系
- 防腐層(Anticorruption Layer)
1)在架構層面,通過引入防腐層有效隔離限界上下文之間的耦合
2)防腐層同時還可以扮演適配器、調停者、外觀等角色
3)防腐層往往屬于下游限界上下文,用以隔絕上游限界上下文可能發生的變化
引入防腐層的架構
- 開放主機服務(Open Host Service)
1)開放主機服務即定義公開服務的協議,包括通信的方式、傳遞消息的格式(協議),保證開放的服務不會輕易做出變化
2)防腐層是下游限界上下文對抗上游變化的利器,開放主機服務是上游服務用來吸引更多下游使用者的誘餌
3)上游限界上下文往往會被多個下游限界上下文消費,如果通過防腐層的形式需要為每個下游提供一個相似的防腐層,冗余度高 - 發布/訂閱事件
1)即使確定了發布語言規范的OHS,仍然會導致兩個上下文之間存在耦合關系,下游限界上下文必須知道上游服務的ABC(Address、Binding與Contract),對于不同的分布式實現,還需要在下游定義類似服務樁的客戶端
2)發布/訂閱事件的方式在解耦合方面走得更遠。一個限界上下文作為事件的發布方,另外的多個限界上下文作為事件的訂閱方,二者的協作通過經由消息中間件進行傳遞的事件消息來完成。在確定消息中間件后,發布方與訂閱方唯一存在的耦合點就是事件持有的數據
3)實例:從買家搜索商品并將商品加入購物車開始,到下訂單、支付、配送完成訂單結束,整個過程通過發布/訂閱事件方式由多個限界上下文一起協作完成
多個限界上下文通過發布/訂閱事件協作
電商購物發布-訂閱模式涉及的上下文
四.辨別限界上下文的協作關系
1.限界上下文的通信邊界對協作的影響
- 限界上線的通信邊界分為進程內邊界與進程間邊界,通信邊界直接影響上下文映射模式的選擇
- 進程間邊界需要考慮跨進程訪問的成本,如序列化和反序列化、網絡開銷等。由于跨進程調用的限制,彼此之間的訪問協議也不盡相同,同時還需要控制上游限界上下文可能引入的變化,一個典型的協作方式是同時引入開放主機服務(OHS)與防腐層(ACL)
1)限界上下文A對外通過控制器(Controller)為用戶界面層暴露REST服務,而在內部則調用應用層的應用服務(Application Service),然后再調用領域層的領域模型
2)如果限界上下文A需要訪問限界上下文B的服務,則通過領域層的接口(Interface)調用基礎設施層的客戶端(Client)完成,這個客戶端即限界上下文A的防腐層
3)限界上下文B訪問限界上下文C的方式完全一致,限界上下文C則通過資源庫(Repository)接口經由持久化(Persistence)組件訪問數據庫
2.協作即依賴
- 兩個限界上下文存在協作,意味著彼此存在依賴關系,一方需要知道另一方的知識,包括
1)領域行為:導致行為之間耦合的原因是什么?如果是上下游關系,要確定下游是否是上游服務的真正調用者
2)領域模型:需要重用別人的領域模型,還是自己重新定義一個模型
3)數據:是否需要限界上下文對應的數據庫提供支撐業務行為的操作數據
這三種知識都將產生依賴
3.領域行為及其產生的依賴
- 領域行為
領域行為在設計層面就是每個領域對象的職責,職責可以由實體(Entity)、值對象(Value Object)、領域服務(Domain Service)、資源庫(Repository)或者工廠(Factory)對象承擔。包括三種履行職責的方式
1)親自完成所有工作
2)請求其他對象幫忙完成部分工作(和其他對象協作)
3)將整個服務請求委托給另外的幫助對象 - 領域行為產生的依賴
1)當領域對象履行職責的方式為上述的后兩種時,必然牽涉到對象之間的協作
2)每個領域對象應該只承擔自己擅長處理的部分,而將自己不擅長的職責轉移到別的對象
3)實例:電商系統業務場景客戶已經選擇好要購買的商品,并通過購物車提交訂單,那么提交訂單的職責應該由客戶上下文還是訂單上下文履行?
領域行為產生的依賴可以通過抽象接口來解耦 - 實例:下訂單場景的限界上下文架構
1)領域層處于限界上下文的核心
2)應用層包裹整個領域層,通過RESTful服務與作為調用者的前端通信
3)RESTful服務等同于上下文映射中的開放主機服務(OHS),或MVC模式的控制器(Controller),屬于基礎設施層的組件
下訂單場景的訂單限界上下文架構
3.領域模型產生的依賴
- 以查詢客戶訂單信息為例,可以引入資源庫對象來履行查詢職責。查詢訂單時,將SaleOrder作為聚合根,對應的SaleOrderRepository作為資源庫放到訂單上下文。在分層架構中,資源庫對象可能會被封裝到應用服務中,也可能直接暴露給作為適配器的REST服務,例如
@Path("/saleorder-context/saleorders/{customerId}")
public class SaleOrderController {
@Autowired
private SaleOrderRepository repository;
public List<SaleOrder> allSaleOrdersBy(CustomerId customerId) {
return repository.allSaleOrdersBy(customerId);
}
}
REST服務的調用者并非客戶上下文,而是前端或客戶端,從而接觸客戶與訂單的包含關系
- 以訂單上下文在查詢訂單時需要獲得訂單對應的商品信息為例,應該采用遵奉者模式,即在訂單上下文重用商品上下文的領域模型?還是重新在訂單上下文定義屬于自己的與商品有關的領域模型?
1)選擇前者,即重用商品領域對象時,可以提高代碼重用,在今后的修改中避免散彈式的修改,但是由于兩個限界上下文對商品的需求不同,重用的商品模型需要同時應對兩種不同的需求,從而主鍵成為一個低內聚的對象
2)選擇后者,即分別為商品上下文和訂單上下文建立商品領域對象,雖然會帶來代碼的重復,但分離的兩個模型可以獨自應對不同的需求變化,即"獨立演化"。 - 因此,常常在兩個不同的限界上下文為相同或相似的領域概念分別建立獨立的領域模型,如下圖所示
在商品上下文和訂單上下文分別建立獨立的商品模型
4.數據產生的依賴
- DDD中,通過領域模型的資源庫訪問數據庫,與數據庫交互的對象是領域模型對象(實體和值對象),即使有依賴,也是領域行為與領域模型導致的
- 有時候出于性能或其他原因考慮,一個限界上下文訪問屬于另一個限界上下文邊界的數據時,會跳過領域模型而直接通過SQL或存儲過程的方式對多張表執行關聯查詢。這種訪問跨限界上下文數據表的方式確實是最簡單最高效的實現方式,但需要限界上下文之間數據庫共享
- 這種數據依賴值得警惕,SQL乃至存儲過程形成的數據表關聯難以解耦。一旦系統架構需要從單體架構(或數據庫共享架構)演進到微服務架構,最大的障礙不是代碼層面而是數據庫層面的依賴,即大量復雜的SQL與存儲過程
- SQL與存儲過程的問題在于
1)無法為SQL和存儲過程編寫單元測試
2)SQL與存儲過程的可讀性通常較差,難以重用
3)SQL與存儲過程的優化策略限制太大