DDD 上下文映射
Y 說
hi,失蹤人口迴歸了。還記得上次發文章,是在上次。
最近有點小忙,狀態也有些低迷,腦子空空的,感覺精力不是很充沛,也不是很想學習新東西。
被熱心網友催更了幾次,自己也覺得應該再把狀態撿起來,畢竟人生如逆水行舟。
還是要多看多學,偶爾產出一點自己學到的東西或者思考,用輸出倒逼輸入,持續成長。
希望與諸君共勉。
什麼是上下文映射
上下文映射,對應的英文單詞是 Context Map,代表的是領域驅動設計中,多個限界上下文之前的關係。方便設計者和開發者能夠一目瞭然地看到每個限界上下文和其它限界上下文之間的關係,最終的產出可能是一張映射圖,或者映射卡片。
下面的一些限界上下文映射設計只是一種參考。實際上的限界上下文映射的設計,不只是跟設計決策和技術實現有關,還跟企業文化、組織架構有關。
有哪些上下文映射
分離方式
分離方式(separate way),也有的資料翻譯爲 “各行其道”。分離方式指的是兩個限界上下文沒有任何關係,沒有關係其實就是一種非常好的設計,因爲它們可以獨立變化,互相影響。
但在實際的開發過程中,可能兩個限界上下文會有一些耦合。如果設計者認爲這兩個限界上下文解耦的價值遠遠大於複用的價值(比如分屬於兩個差異很大的團隊),那可以通過引入少量的重複來徹底解耦開來。
比如:在電商場景中,支付上下文,就和庫存上下文沒有任何關係。
客戶 - 供應
客戶 - 供應(customer/supplier)是我們最中間的一種上下文映射方式。一方提供服務,另一方去調用服務。我們類比水流,上游發生變化可能會影響下游,所以我們把提供服務的一方稱爲 “上游”,使用服務的一方稱爲 “下游”。這與調用關係剛好是相反的。
比如:在電商場景中,訂單上下文依賴庫存上下文,所以庫存上下文就是訂單上下文的上游。
發佈 - 訂閱
發佈 - 訂閱(publisher/subscriber)也是一種很常見的上下文映射方式,在實際的開發過程中往往是通過消息中間件來實現。這個模式並不在 Eric Evans 提出的上下文映射模式裏面,但隨着事件慢慢成爲領域驅動設計中的 “一等公民” 之後,發佈 - 訂閱模式並普遍用於處理限界上下文之間的協作關係。
發佈 - 訂閱模式源自於設計模式中的 “觀察者模式”,上下游通過消息去通信,下游註冊觀察者,上游作爲發佈者,如果上游發生了變化,會發佈一個業務事件,下游收到這個事件後進行後續的操作。
可以看到,發佈 - 訂閱模式與客戶 - 供應模式最大的不同,在於發佈 - 訂閱模式,是上游主動發起業務的變化,而不是被動等下游去調用上游。它相較於客戶 - 供應模式而言,耦合程度會低一些。
比如:在電商場景中,訂單上下文和物流上下文,就可以通過發佈 - 訂閱模式來做。訂單完成後,發生訂單完成事件,物流上下文監聽事件開始物流配送。
開放主機服務和發佈語言
開放主機服務(open host service, OHS),指的是上游提供一些公開的服務,包括它們的通信方式、數據格式等,並且承諾這些服務不會輕易做出變化。
發佈語言(published language)通常和開放主機服務一起配合使用,主要用於兩個限界上下文之間的模型轉換。
其實我在看這方面的資料的時候,對開放主機服務和發佈語言還是有一些疑惑,很多資料都是對它們一筆帶過,無法瞭解更多的細節。開放主機服務和客戶 - 供應有什麼區別?發佈語言到底是什麼?
在我看來,開放主機服務和客戶 - 供應最大的區別在於,它承諾了服務不會輕易變化。那下游服務就可以不用做專門的防禦措施來抵抗上游的變化(也就是下面會介紹的防腐層)。
而發佈語言在開放主機服務上,本質上就是開放主機服務定義的協議、request、response、服務名唯一名字等。因爲開放服務不可能將上下文內部的領域模型暴露出去,所以會需要對外定義一些數據傳輸模型(DTO)來提供服務。
在電商業務中,財務系統就是一個相對穩定的業務,可以高度抽象爲幾個原子的財務操作,比如:資金的申請,佔用,扣減,覈銷等。
防腐層
防腐層(anti corruption layer, ACL)是應對上游服務變化的利器。尤其是當下遊限界上下文有多個地方依賴某一個上游時,一旦上游服務發生變化,下游服務如果不做防腐措施,就會面臨大面積的修改。
如果上游限界上下文存在多個下游時,倘若都需要隔離變化,每個下游都做防腐層成本比較大,可以考慮單獨抽一個只有防腐功能的限界上下文,避免代碼重複。
在電商業務中,可能訂單上下文、售後上下文都會涉及到支付功能,如果支付功能是對接了大量的第三方支付,每個上下文自己去做防腐層就會有一些代碼重複。那可以把支付上下文單獨抽出來做爲一個上游的防腐層。
在面對 “大泥球” 一樣錯綜複雜調用關係的老系統中,防腐層就是可以才幫助遺留系統遷移的利器。
遵奉者
前面也提到了,有時候限界上下文之間關係的設計,還會受到企業文化和團隊協作的影響。當上遊服務不積極響應下游服務的需求時,會有三種方式來解決:
-
分離方式:下游服務切斷上游服務的依賴,自己來實現
-
防腐層:複用上游的服務,但領域模型由下游團隊自己來開發,然後用防腐層實現上下游領域模型之間的轉換。
-
遵奉者:嚴格遵從上游團隊的模型,以消除複雜的模型轉換邏輯
遵奉者(conformist)就是一種妥協,當下遊團隊選擇遵奉上游團隊設計的模型時,意味着它對上游產生了模型上的強依賴。
Eric Evans 的觀點是,限界上下文之間的模型複用是很危險的,如果不是因爲重複開發的成本太高,應該儘量避免遵奉者模式。
比如在電商場景中,財務上下文是一個比較穩定的業務,在電商活動立項的時候,可能需要申請一筆預算,但這筆預算應該是有一個有效期的,也就是活動的起止時間,活動結束後是不能使用預算的。但財務團隊拒絕爲了活動這個特殊的場景,在他們的上下文內部的領域模型增加 “預算有效期” 這個字段。
活動上下文可以選擇上面的三種方式之一來解決這個問題:
-
可以自己實現一套財務模型,
-
複用財務上下文的佔用、扣減等服務,防腐層轉換爲活動上下文內部的帶有預算有效期的內部模型;
-
遵循財務上下文的模型。在其它上下文(比如流量投放上下文)使用預算的時候,再通過通過調用活動上下文去校驗活動時間。
共享內核
共享內核(Shared Kernel),指的是將一個限界上下文將自己的領域模型暴露出去,給其它的限界上下文使用。共享內核不能像其它的限界上下文那樣,自由地更改,但共享內核也會造成耦合。因此我們只可能把那些非常穩定且具有複用價值的領域模型封裝到共享內核上下文中。
共享內核通常以庫的形式(比如 Java 的 jar 包)被其它限界上下文複用,它本身不提供遠程服務。所以可以理解爲它是一種特殊的進程內通信。
耦合的代價是巨大的,筆者個人不是建議使用共享內核這種模式,除非真的重複的代價遠遠大於耦合。
合作者
合作者(partnership),指的是兩個或多個限界上下文彼此依賴,聯繫緊密。具體表現出來的可能就是循環依賴,兩個限界上下文形成了強耦合關係。團隊之間的良好協作是好事,但強耦合會帶來一系列的問題。要解決這種強耦合,通常有三種方式:
-
合併:既然分不開,說明當時拆分得可能不合理,他們本質上也許可以合併爲一個限界上下文。
-
重新分配:理清楚爲什麼相互依賴,嘗試把一些功能或服務重新分配,儘量減少上下文之間的依賴。
-
抽取:如果實在不能合又分配不清楚,那可以考慮重新抽取爲一個新的限界上下文,然後之前的兩個限界上下文去依賴這個新的限界上下文。
如果上述幾種方法都不使用當前的團隊和場景,那就只能允許合作者模式存在了。需要在限界上下文中特別標識出來,這裏存在高耦合,變動會比較容易引發風險。
總結
如果說我們在上下文映射設計時,要儘量做到低耦合,那分離方式、發佈 - 訂閱、客戶 - 供應防腐層是比較推薦的模式。
而遵奉者、共享內核、合作者是需要儘量去避免的。避免的方式大多都是重複或者冗餘,這個時候就要去衡量是否值得了。
軟件設計就是這樣,可能沒有完美的方案,總是在各方面權衡利弊得失,有所取捨,最終力求得到一個最優的解決方案,這就是軟件設計很難的原因,也是它的魅力所在。
任何決策都應該考慮收益和成本,只有收益大於成本,決策纔有可能是合理的。
雖然是限界上下文之間的映射,但其實落地下來,咱們不使用領域驅動設計的微服務拆分也可以參考這幾種模型,儘量使微服務做到 “高內聚,低耦合”。
關於作者
我是 Yasin,一個愛寫博客的技術人
微信公衆號:編了個程 (blgcheng)
個人網站:https://yasinshaw.com
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/rPitaiIEuRarLfAt-C0M7g