戰(zhàn)略設(shè)計(jì)之上下文映射和系統(tǒng)分層架構(gòu)
在完成了限界上下文的識別(也就是系統(tǒng)“最粗粒度”的模塊劃分)后,我們需要對這些上下文之間的協(xié)作關(guān)系進(jìn)行分析——即“限界上下文關(guān)系映射”。也只有在完成上下文關(guān)系映射后,我們才能真正的判定自己所做出的“限界上下文識別”是否真的達(dá)到了自己想要的“低耦合、高內(nèi)聚”的目標(biāo)。因?yàn)?,通過“限界上下文映射”我們就能夠看到:
- 這些上下文之間有哪些協(xié)作關(guān)系?
- 這些關(guān)系是強(qiáng)關(guān)聯(lián)還是弱關(guān)聯(lián)?
關(guān)于“限界上下文識別”和“限界上下文關(guān)系映射”,我認(rèn)為這是 DDD 戰(zhàn)略設(shè)計(jì)中最重要的部分,甚至可以說:這兩個(gè)工作將決定了微服務(wù)切分是否有效的關(guān)鍵因素!
但是,肯定會有人說:限界上下文不用 DDD,我憑直覺就能識別出來。我的回答是:是的,你貌似可以!但更重要的是限界上下文的關(guān)系映射,這將決定做微服務(wù)拆分后、這些微服務(wù)之間是怎樣做到“高內(nèi)聚、低耦合”的。如果你不用 DDD 戰(zhàn)略設(shè)計(jì),我可以很負(fù)責(zé)任的告訴你:你將來的“微服務(wù)”之間的調(diào)用關(guān)系一定會變得無法控制!
在我實(shí)際工作中接觸的某大型國企 IT 系統(tǒng)中,所謂業(yè)務(wù)中臺上千萬行代碼,部署在十多個(gè)微服務(wù)中心,而 80%以上的外圍接口調(diào)用、或前端界面服務(wù)請求,都要從十個(gè)以上的微服務(wù)中心全部走一遍!這是不是很可怕的災(zāi)難?系統(tǒng)的高可用性、業(yè)務(wù)需求的快速響應(yīng)還有什么保障可言?
所以說:掌握好 DDD 戰(zhàn)略設(shè)計(jì),幾乎是做好微服務(wù)設(shè)計(jì)必不可少的前提!不懂得 DDD,你做的“微服務(wù)拆分”可能還不如不拆分,單體應(yīng)用可能更適合你!
在本節(jié)內(nèi)容中,除了搞定限界上下文的映射之外,我還將對系統(tǒng)分層架構(gòu)進(jìn)行設(shè)計(jì),并在最終給出項(xiàng)目組可直接用于開發(fā)的代碼框架結(jié)構(gòu)。
需要特別說明的是:在寫到這篇的過程中,我反復(fù)和多位業(yè)界大拿請教,大家普遍認(rèn)為我前面第三篇中業(yè)務(wù)子域的分類上“核心子域”太多了。我經(jīng)過再三考慮,將“核心子域”進(jìn)行了調(diào)整,這將影響到本篇關(guān)于系統(tǒng)分層結(jié)構(gòu)的設(shè)計(jì),您可以跳回到第三篇重新看下“業(yè)務(wù)子域識別與分類”這一小段。這里再次重申下:您在閱讀中發(fā)現(xiàn)我的任何不妥之處 ,可隨時(shí)與我交流,我發(fā)現(xiàn)不妥之處一定認(rèn)真思考改進(jìn),感謝您的支持!
限界上下文映射
1.跨上下文用例識別
為了識別限界上下文之間的映射關(guān)系,我們需要對跨上下文的業(yè)務(wù)用例(也叫業(yè)務(wù)服務(wù)),從架構(gòu)設(shè)計(jì)的技術(shù)視角繪制服務(wù)序列圖,進(jìn)而識別它們之間的映射關(guān)系。
首先,第一步,我們識別出所有的跨限界上下文的業(yè)務(wù)用例。在識別某個(gè)業(yè)務(wù)用例是否跨限界上下文時(shí),一定要注意兩個(gè)基本事實(shí):1)其實(shí)大部分業(yè)務(wù)用例都是從“群買菜”小程序前端界面發(fā)起的,前端與服務(wù)端的交互,不算跨限界上下文;2)限界上下文提供的是服務(wù)端的業(yè)務(wù)服務(wù),跨限界上下文一定要是服務(wù)端邏輯的相互關(guān)系。
關(guān)于如何識別“服務(wù)端的跨限界上下文”業(yè)務(wù)邏輯,我認(rèn)為需要逐個(gè)分析前面羅列的所有業(yè)務(wù)用例,從如下兩個(gè)角度篩選:
初步分析業(yè)務(wù)用例內(nèi)部的邏輯,看是否需要多個(gè)上下文來承擔(dān)職責(zé)。如果是,則需將該用例納入分析范圍;
分析業(yè)務(wù)用例圖中的被包含的“子用例”,看是否存在上下文包含了被歸類到別的上下文的情況。如果是,則需將該用例納入分析范圍;為此,識別結(jié)果如下圖羅列:
需要說明的是:有幾個(gè)用例看起來好像是跨上下文的,其實(shí)不是。分別說明如下:
- “確認(rèn)購買并付款”、“創(chuàng)建訂單預(yù)支付”、“完成訂單支付”、“補(bǔ)收客戶貨款”、“退客戶貨款”,這幾個(gè)用例其實(shí)是訂單上下文和支付系統(tǒng)之間的關(guān)系??紤]到我們的系統(tǒng)上下文設(shè)計(jì),支付系統(tǒng)其實(shí)不是我們目標(biāo)系統(tǒng)的工作范圍,故這里不納入范圍。
- “后臺查詢店鋪商品”、“后臺瀏覽店鋪訂單列表”、“管理店鋪客戶信息”、“管理店鋪員工”、“開通店鋪加盟并設(shè)置分成政策”、“添加品牌店鋪到加盟列表”,看起來每個(gè)用例的名稱上都涉及到 2 個(gè)限界上下文、并都跟“店鋪”上下文發(fā)生關(guān)系,其實(shí)它們只是需要店鋪 ID,并沒有與店鋪上下文發(fā)現(xiàn)任何業(yè)務(wù)交互,故實(shí)際上不作為“跨上下文”的用例來對待。
其次,這些業(yè)務(wù)用例中,從跨上下文協(xié)作關(guān)系角度來看,其中有些用例畫服務(wù)序列圖是會出現(xiàn)重復(fù)的,我們需要識別出來:
- “創(chuàng)建新店鋪”、“編輯店鋪信息”,會用到短信驗(yàn)證碼驗(yàn)證手機(jī)號,故都屬于跨店鋪、平臺集成兩個(gè)限界上下文的,且交互邏輯一致,故重復(fù)。
- “加商品到購物車”、“選購接龍商品到購物車”,其實(shí)都是涉及到訂單和商品兩個(gè)限界上下文的相同交互關(guān)系,故重復(fù)。
- “創(chuàng)建接龍活動”、“編輯接龍活動”,這兩個(gè)業(yè)務(wù)用例的操作序列,從跨上下文協(xié)作關(guān)系角度來看也是完全一樣的,只是后者需加載原有“接龍”上下文內(nèi)部已持久化的信息、前者不用。
- “創(chuàng)建付款訂單”是“確認(rèn)接龍付款”的子用例,故只需要保留“確認(rèn)接龍付款”即可。
- “添加品牌店鋪到加盟列表”、“從加盟列表刪除品牌店鋪”,這兩個(gè)業(yè)務(wù)用例都是涉及到店鋪和加盟兩個(gè)上下文的關(guān)系,并且都是獲取店鋪相關(guān)信息、并將店鋪信息傳輸給加盟上下文處理,故重復(fù)。
最終,我們確定下來,被用來繪制服務(wù)序列圖,進(jìn)而確定各上下文協(xié)作關(guān)系的業(yè)務(wù)用例羅列如下圖:
2.基于跨上下文用例映射上下文關(guān)系
我們接下來的工作,就是要對這些業(yè)務(wù)用例,逐個(gè)進(jìn)行技術(shù)分析,設(shè)計(jì)出服務(wù)序列圖,然后根據(jù)服務(wù)序列圖確定限界上下文的關(guān)系映射。
創(chuàng)建新店鋪
創(chuàng)建新店鋪涉及到手機(jī)號短信驗(yàn)證,故需要跨店鋪和平臺集成兩個(gè)上下文。服務(wù)序列圖如下:
基于該服務(wù)序列圖識別出“店鋪”和“平臺集成”上下文關(guān)系如圖(C 表示服務(wù)調(diào)用客戶端、S 表示被調(diào)用服務(wù)端,以下同。DDD 標(biāo)準(zhǔn)方法中一般用“上下游關(guān)系”、及諸如“開發(fā)主機(jī)服務(wù) OHS”等方式表達(dá),但我認(rèn)為那是廢話,并沒有清晰的表達(dá)強(qiáng)弱關(guān)聯(lián),故不在這里采用):
初始化店鋪默認(rèn)選項(xiàng)
根據(jù)產(chǎn)品 UI 原型的設(shè)計(jì)方案,新店鋪創(chuàng)建后,系統(tǒng)機(jī)器人需要基于異步事件,對店鋪的相關(guān)選項(xiàng)立即進(jìn)行初始化:
- 店鋪默認(rèn)門頭圖片(UI 允許創(chuàng)建人未設(shè)置門頭圖片);
- 店鋪是否開通訂單短信提醒(隨著業(yè)務(wù)發(fā)展可能會調(diào)整,故不放在創(chuàng)建店鋪時(shí)設(shè)定);
- 店鋪的第一個(gè)員工(就是創(chuàng)建人自己);
- 店鋪默認(rèn)的加盟分銷政策(隨著業(yè)務(wù)發(fā)展可能會調(diào)整,故不放在創(chuàng)建店鋪時(shí)設(shè)定);
- 店鋪的首個(gè)接龍地點(diǎn)(詳見產(chǎn)品 UI 原型,店鋪接龍支持多個(gè)地點(diǎn),但首個(gè)地點(diǎn)就是店鋪地址);
- 創(chuàng)建商家及商家賬戶(如果是創(chuàng)建人的第一家店鋪);
- 店鋪默認(rèn)商品分類(系統(tǒng)規(guī)則會按季節(jié)調(diào)整);
- 店鋪默認(rèn)的商品搜索熱詞(系統(tǒng)后臺會不定期根據(jù)某種策略更新);
這些默認(rèn)選項(xiàng),涉及到“店鋪”、“員工”、“商品”、“接龍”、“商家賬戶”、“鑒權(quán)”共 6 個(gè)上下文的協(xié)作。服務(wù)序列圖涉及如下:
根據(jù)該服務(wù)序列圖,我們得出這 6 個(gè)限界上下文的協(xié)作關(guān)系如下圖:
從上圖的上下文依賴關(guān)系中,看到店鋪對員工、商品、接龍、商家賬戶 4 個(gè)上下文產(chǎn)生了調(diào)用關(guān)系依賴,這是不合理的。因?yàn)閺臉I(yè)務(wù)角度來說,其實(shí)是后 4 者依賴于店鋪的存在而存在,而不是反過來。為此,我們做這樣的調(diào)整:
- 采用消息發(fā)布者/訂閱者模式,讓后 4 者依賴店鋪上下文發(fā)布的“店鋪已創(chuàng)建”消息;
- 去掉“接龍”上下文對店鋪首個(gè)接龍地點(diǎn)初始化的邏輯。本質(zhì)上,仔細(xì)分析產(chǎn)品 UI 界面設(shè)計(jì)的要去,我們發(fā)現(xiàn)這其實(shí)是“群買菜小程序”在展示創(chuàng)建接龍活動的前端界面時(shí),需調(diào)用“店鋪”上下文服務(wù)來獲取的信息:如果所選擇店鋪已經(jīng)有接龍地址,則返回已有的接龍地址列表供選擇;如果該店鋪尚無首個(gè)接龍地址,則返回店鋪地址作為默認(rèn)接龍地址。
經(jīng)過修改后的服務(wù)序列圖如下:
根據(jù)該服務(wù)序列圖,我們修改上下文的協(xié)作關(guān)系如下圖(P 表示消息發(fā)布者,S 表示消息訂閱者,下同):
加商品到購物車
客戶瀏覽店鋪商品,選中感興趣的商品到購物車,以便后續(xù)的購買結(jié)算??紤]到添加商品到購物車中去的“時(shí)間”特殊性:我們需要在客戶將商品加入到購物車時(shí),為商品創(chuàng)建“快照”,以避免商品信息在后面被編輯修改(比如改了圖片或描述、尤其是價(jià)格)時(shí),影響到對客戶的購買承諾。
為此,該業(yè)務(wù)用例(服務(wù))就涉及到“訂單”和“商品”兩個(gè)上下文,服務(wù)序列圖設(shè)計(jì)如下:
該序列圖展示出訂單和商品的上下文關(guān)系如圖:
發(fā)送訂單提醒
“發(fā)送訂單提醒”需要在訂單上下文中發(fā)起、“通知”上下文中發(fā)送通知,故涉及“訂單”和“通知”兩個(gè)上下文。該用例服務(wù)序列圖如下:
考慮到訂單消息提醒是沒有副作用的、而且也不需要保證必須成功,故采用消息發(fā)布者/訂閱者模式比較合適。也就是得到如下所示的“訂單”和“通知”上下文的映射關(guān)系:
創(chuàng)建接龍活動
與“加商品到購物車”用例類似,商家在創(chuàng)建接龍活動、添加商品到接龍中去時(shí)也存在“時(shí)間”特殊性,需要為商品創(chuàng)建“快照”,以避免商品在后面被編輯修改(比如改了圖片或描述、尤其是價(jià)格)時(shí),影響到接龍活動的開展。為此,該業(yè)務(wù)用例(服務(wù))就涉及到“接龍”和“商品”兩個(gè)上下文,服務(wù)序列圖設(shè)計(jì)如下:
該序列圖展示出接龍和商品的上下文關(guān)系如圖:
瀏覽我的接龍
按照產(chǎn)品 UI 設(shè)計(jì)文檔,接龍只能在店鋪下存在。而“瀏覽我的接龍”實(shí)際上是“瀏覽我被授權(quán)操作的所有店鋪發(fā)布的、或其被授權(quán)店鋪所加盟品牌店鋪發(fā)布的、或我參與的”的所有接龍。故該用例涉及到“接龍”、“店鋪”兩個(gè)上下文,服務(wù)序列圖如下:
該服務(wù)序列圖展示出,實(shí)際上“接龍”、“店鋪”這 2 個(gè)上下文沒有發(fā)生關(guān)聯(lián)關(guān)系。但這個(gè)服務(wù)序列圖設(shè)計(jì),有個(gè)“壞味道”的感覺:讓群買菜小程序客戶端承擔(dān)了過多業(yè)務(wù)邏輯,這是不合理的。于是,我們將服務(wù)序列圖調(diào)整為如下:
該服務(wù)序列圖會導(dǎo)致如下的 2 個(gè)上下文之間的關(guān)系:
確認(rèn)接龍付款
確認(rèn)接龍付款從產(chǎn)品界面原型可以看出,確認(rèn)接龍付款是從“查看接龍?jiān)斍椤苯缑姘l(fā)起的。客戶在該界面上點(diǎn)擊相應(yīng)的商品加入購物車、或從購物車移除,然后點(diǎn)擊“我要接龍”按鈕進(jìn)入該用例。
該用例允許用戶設(shè)置提貨方式、提貨時(shí)間、聯(lián)系人等信息后,點(diǎn)擊“確認(rèn)付款”按鈕完成支付。該按鈕點(diǎn)擊后,按照產(chǎn)品原型設(shè)計(jì),需要完成如下任務(wù):
- 創(chuàng)建訂單;
- 更新關(guān)聯(lián)商品的銷量,以便于后續(xù)商品列表和詳情頁面顯示商品銷量;
- 如果下單客戶首次購買對應(yīng)店鋪商品,則為該店鋪初始化對應(yīng)客戶資料,以便于商家后續(xù)維護(hù)客戶資料;
- 記錄客戶參與了該接龍,以便于客戶“瀏覽我的接龍”時(shí),可包含該接龍;
根據(jù)上面的邏輯,我們畫出服務(wù)序列圖設(shè)計(jì)如下:
該服務(wù)序列圖展示的相關(guān)限界上下文關(guān)系如下圖:
這里可以看到上下文之間的調(diào)用關(guān)系比較多,并且“訂單”與“商品”、“訂單”與“客戶”之間,還存在數(shù)據(jù)一致性的要求,不利于系統(tǒng)的“松耦合”。為了降低上下文之間的耦合性,我們分析業(yè)務(wù)需求發(fā)現(xiàn):其實(shí)“訂單創(chuàng)建”后“增加商品銷量”、以及“為指定店鋪初始化客戶”是可以有一定的數(shù)據(jù)延遲的,并不需要通過“強(qiáng)”服務(wù)調(diào)用關(guān)系保障嚴(yán)格的數(shù)據(jù)一致性。為此,我們將服務(wù)序列圖修改如下:
根據(jù)改進(jìn)后的服務(wù)序列圖,可以調(diào)整上下文映射關(guān)系如下圖:
結(jié)算訂單收入
結(jié)算訂單收入分為兩步:第一步,在客戶確認(rèn)訂單完成、或機(jī)器人超時(shí)自動確認(rèn)訂單完成時(shí),訂單上下文通知商家賬戶上下文記錄待結(jié)算的訂單 ID(由于訂單上下文缺乏“判斷什么訂單屬于待結(jié)算狀態(tài)”這樣的領(lǐng)域知識,故只能由商家賬戶上下文來記錄);第二步,系統(tǒng)機(jī)器人定時(shí)觸發(fā)商家賬戶上下文對待結(jié)算訂單進(jìn)行收入結(jié)算。服務(wù)序列圖設(shè)計(jì)如下:
其中第一步的操作,可以和“發(fā)送訂單提醒”中的“訂單已完成”領(lǐng)域事件合并處理。不過,因?yàn)檫@里是不可丟失的領(lǐng)域事件,所以這就要求采用“可靠事件模式”?;谠摲?wù)序列圖,識別出“訂單”和“商家賬戶”上下文的關(guān)系如圖:
結(jié)算傭金收入
結(jié)算傭金收入可以和結(jié)算訂單收入同時(shí)進(jìn)行,也可以分開在兩個(gè)用例中異步執(zhí)行??紤]到為了將來支持允許品牌商在加盟政策中設(shè)置結(jié)算周期,故將其分開在兩個(gè)業(yè)務(wù)用例異步執(zhí)行。為此,設(shè)計(jì)其服務(wù)序列圖如下:
該序列圖展示出商家賬戶和訂單的上下文關(guān)系如圖:
3.限界上下文映射圖
我們將上面針對各跨上下文業(yè)務(wù)用例分析后,得到的上下文映射關(guān)系進(jìn)行匯總后最終得出下圖:
圖中實(shí)線是服務(wù)調(diào)用關(guān)系,屬于“強(qiáng)關(guān)系”;虛線是消息通知關(guān)系,屬于“弱關(guān)系”。
通過該限界上線文映射關(guān)系可以看出:我們總共有 9 個(gè)限界上下文,其中有強(qiáng)依賴關(guān)系的涉及到 9 個(gè)、強(qiáng)依賴關(guān)系鏈有 9 條。分別是:接龍依賴于店鋪、商品和訂單,店鋪依賴于平臺集成,訂單依賴于商品,商家賬戶、員工、客戶依賴于鑒權(quán),商家賬戶依賴于訂單。9 個(gè)上下文,理論上最多有 72 條依賴鏈條,我們分析匯總后只有 11 條,已經(jīng)是很好的設(shè)計(jì)。
基于這樣的綜合評估,我們認(rèn)為目前的設(shè)計(jì)已經(jīng)到了“可接受程度”,暫時(shí)不再做更多的優(yōu)化和調(diào)整了,以避免“過度設(shè)計(jì)”。
系統(tǒng)架構(gòu)和代碼框架
1.業(yè)務(wù)子域與上下文的映射
完成了限界上下文的關(guān)系映射,其實(shí)就有了對整體系統(tǒng)架構(gòu)進(jìn)行分層設(shè)計(jì)的基礎(chǔ)。系統(tǒng)整體架構(gòu)設(shè)計(jì)從分層角度來看,包括:邊緣層、業(yè)務(wù)價(jià)值層、基礎(chǔ)層。邊緣層一般都是各種針對前端 UI 的控制器,業(yè)務(wù)價(jià)值層和基礎(chǔ)層包含所有的限界上下文,其中基礎(chǔ)層放的是對應(yīng)到支撐子域、通用子域的限界上下文,而核心子域?qū)?yīng)的限界上下文作為業(yè)務(wù)價(jià)值層。
為了區(qū)分業(yè)務(wù)價(jià)值層和基礎(chǔ)層,我們先將前面得出的業(yè)務(wù)子域(見第三篇中全局分析內(nèi)容)、與限界上下文進(jìn)行如下表所示的關(guān)系映射:
需要說明的是:其中“商家管理”和“店鋪管理”雖然是支撐子域,但由于分別被合并到“商家賬戶”、“店鋪”上下文來開發(fā)實(shí)現(xiàn),故其中邏輯的代碼實(shí)現(xiàn)也被劃分到了“業(yè)務(wù)價(jià)值層”。
2.菱形對稱架構(gòu)簡介
在對整個(gè)系統(tǒng)進(jìn)行“分層架構(gòu)設(shè)計(jì)”之前,我們還需要先熟悉一個(gè)軟件架構(gòu)模式——菱形對稱架構(gòu)(以下簡稱菱形架構(gòu)),如下圖所示:
對“菱形架構(gòu)”的說明如下:
菱形架構(gòu)的基礎(chǔ),是依賴于“領(lǐng)域?qū)印钡慕缍?。這涉及到后面才會進(jìn)行的 DDD 戰(zhàn)術(shù)設(shè)計(jì)內(nèi)容,這里不做展開,您只需要知道:“領(lǐng)域?qū)印狈诺氖菢I(yè)務(wù)領(lǐng)域的關(guān)鍵業(yè)務(wù)知識,包括“聚合(內(nèi)含實(shí)體對象、值對象)”和“領(lǐng)域服務(wù)”兩類內(nèi)容。事實(shí)上,我們所追求的“高內(nèi)聚”主要體現(xiàn)的“領(lǐng)域?qū)印保驗(yàn)闃I(yè)務(wù)需求變化而引起的變化,我也希望主要通過“領(lǐng)域?qū)印钡摹熬酆稀焙汀邦I(lǐng)域服務(wù)”的變化來實(shí)現(xiàn)。所以說,“領(lǐng)域?qū)印笔? DDD 的“核心代碼所在地”。
所有非“領(lǐng)域邏輯”層的代碼,我們都希望封裝到“北向網(wǎng)關(guān)”或“南向網(wǎng)關(guān)”中去。具體說明如下:
“北向網(wǎng)關(guān)”其實(shí)就是上下文向外提供服務(wù)、或接受消息通知的入口處。如果上下文只需要向外提供同一個(gè)進(jìn)程內(nèi)部的應(yīng)用服務(wù)調(diào)用接口、或接受消息通知(通過消息總線),則需要“本地”服務(wù);如果上下文需要作為獨(dú)立進(jìn)程(這時(shí)候一般是云原生的獨(dú)立“微服務(wù)”)向外輸出服務(wù)、或接受消息通知(通過消息中間件),則需要將“本地服務(wù)”包裝為“遠(yuǎn)程”服務(wù)。
也就是說:北向網(wǎng)關(guān)中的“本地服務(wù)”和“遠(yuǎn)程服務(wù)”是一一對應(yīng)的,“遠(yuǎn)程服務(wù)”只負(fù)責(zé)將遠(yuǎn)程調(diào)用的出入?yún)⒆龈袷睫D(zhuǎn)換并傳給“本地服務(wù)”,而“本地服務(wù)”負(fù)責(zé)調(diào)用領(lǐng)域?qū)拥摹熬酆稀被颉邦I(lǐng)域服務(wù)”來完成具體的業(yè)務(wù)邏輯(關(guān)于“聚合”和“領(lǐng)域服務(wù)”的內(nèi)容,我會在本專題后面的章節(jié)中演示)。
“南向網(wǎng)關(guān)”其實(shí)就是上下文用來將這 3 類技術(shù)細(xì)節(jié)從業(yè)務(wù)邏輯中“剝離”出來的主要手段(圖中只畫了第一種):訪問底層數(shù)據(jù)庫、調(diào)用其它上下文服務(wù)、向其它上下文發(fā)布消息通知。這 3 個(gè)技術(shù)細(xì)節(jié)的封裝,在 DDD 戰(zhàn)術(shù)設(shè)計(jì)中分別叫“資源庫”、“客戶端(也可以叫防腐層——ACL)”、“消息發(fā)布者”。
從圖中可以看出,“南向網(wǎng)關(guān)”有“端口”和“適配器”兩種角色。簡單點(diǎn)來說,“端口”是抽象接口(以 java 為例就是 interface),而“適配器”則是“接口實(shí)現(xiàn)”(以 java 為例就是實(shí)現(xiàn)了對應(yīng) interface 的類)。并且,一般來說“適配器”都是通過依賴注入(DI,控制反轉(zhuǎn) IoC 的一種)的方式集成到限界上下文的代碼中(java spring 中一般是 bean 注入)。
“南向網(wǎng)關(guān)”區(qū)分“端口”和“適配器”兩個(gè)角色的好處:一方面是可以讓限界上下文內(nèi)的任何代碼都不直接依賴于具體的底層技術(shù)細(xì)節(jié),如:采用哪種數(shù)據(jù)庫(oracle 還是 mysql、甚至 nosql 數(shù)據(jù)庫)、怎么調(diào)用其它上下文服務(wù)(本地調(diào)用還是遠(yuǎn)程 RPC)、怎么發(fā)布消息通知(本地消息總線、還是消息中間件);另一方面的好處是允許我們隨時(shí)將構(gòu)建在一個(gè)“微服務(wù)”甚至“單體應(yīng)用”中的多個(gè)限界上下文進(jìn)行拆分到多個(gè)進(jìn)程(即多個(gè)“微服務(wù)”中),而并不會引起“領(lǐng)域?qū)印?、以及“北向網(wǎng)關(guān)”的任何代碼修改(只要替換并重新打包被依賴注入的“適配器”類即可)。
很明顯可以看出,“菱形架構(gòu)”其實(shí)是限界上下文內(nèi)部所采用的一個(gè)“軟件架構(gòu)模式”。而在整個(gè)系統(tǒng)范圍內(nèi),因?yàn)榘鄠€(gè)限界上下文,DDD 設(shè)計(jì)理念并沒有要求所有的上下文都嚴(yán)格遵循“菱形架構(gòu)”——而完全可以根據(jù)實(shí)際需要(尤其是“基礎(chǔ)層”的上下文),視情況而采用其它架構(gòu)模式(如 MVC 三層架構(gòu)、大數(shù)據(jù)計(jì)算架構(gòu)等)。
3.系統(tǒng)分層架構(gòu)圖
有了前面的將上下文分為“基礎(chǔ)層”和“業(yè)務(wù)價(jià)值層”、以及菱形架構(gòu)概念的基礎(chǔ),再考慮到“群買菜”系統(tǒng)前端界面針對 3 類用戶:商家、客戶、平臺運(yùn)營,前兩者使用微信小程序,最后一個(gè)使用 PC 端,以及相應(yīng)“伴生系統(tǒng)”的協(xié)作邊界。我們最后畫出系統(tǒng)架構(gòu)分層設(shè)計(jì)圖如下:
對于上圖,需要特別說明的是:
其中騰訊地圖、短信系統(tǒng)、微信公眾號因?yàn)椴簧婕暗綐I(yè)務(wù)流程,只是一個(gè)功能點(diǎn),故前面的業(yè)務(wù)子域、上下文、業(yè)務(wù)用例都沒有展現(xiàn)。
有的上下文有“事件訂閱”。這是根據(jù)我們前面“限界上下文映射圖”中的描述,需要進(jìn)行消息訂閱的上下文才有。
其中比較特殊的一點(diǎn):“平臺集成上下文”沒有“領(lǐng)域?qū)印?,這是因?yàn)樗饕瓿蓪ξ⑿殴娞柦涌?、短信接口、微信開放平臺接口的封裝,并不存在 DDD 戰(zhàn)術(shù)設(shè)計(jì)所需要的“實(shí)體”對象這一“領(lǐng)域?qū)印北仨毜幕疽亍?/p>
4.代碼框架結(jié)構(gòu)
基于前面的系統(tǒng)分層架構(gòu)設(shè)計(jì),結(jié)合菱形架構(gòu)模式,我們給出目標(biāo)系統(tǒng)的如下代碼框架結(jié)構(gòu)(采用 java spring 框架開發(fā)):
對上圖中各目錄的劃分和命名說明如下:
foundation 目錄存放的是“基礎(chǔ)層”限界上下文,valueadded 目錄存放的是“業(yè)務(wù)價(jià)值層”限界上下文,edge 目錄存放的是“邊緣層”限界上下文;
本質(zhì)上,每個(gè)限界上下文都可以作為獨(dú)立的 java 工程存在。但因?yàn)楸卷?xiàng)目由我一個(gè)人開發(fā),所以就沒有劃分工程了;
每個(gè)限界上下文,采用“上下文名稱+context”命名風(fēng)格。如:“訂單上下文”命名為 ordercontext;
每個(gè)限界上下文中,其內(nèi)部目錄結(jié)構(gòu)說明如下:
north 目錄存放的是“北向網(wǎng)關(guān)”的內(nèi)容,包括 local 和 remote 子目錄,分別對應(yīng)北向網(wǎng)關(guān)的“本地服務(wù)”和“遠(yuǎn)程服務(wù)”。有的上下文的 north 目錄下有 subsrciber 子目錄,是為了存放消息訂閱者代碼的。
south 目錄存放的是“南向網(wǎng)關(guān)”的內(nèi)容,包括 port 和 adapter 子目錄,分別對應(yīng)南向網(wǎng)關(guān)的“端口”和“適配器”。
domain 目錄存放的是“領(lǐng)域?qū)印眱?nèi)容,也就是核心的業(yè)務(wù)邏輯所在。
pl 目錄存放的是“發(fā)布語言”,其實(shí)就是北向網(wǎng)關(guān)的“本地服務(wù)”向外提供服務(wù)時(shí),允許外部調(diào)用服務(wù)所使用的出入?yún)ο箢愋汀V詫⒊鋈雲(yún)ο箢悓iT設(shè)定一個(gè)目錄來管理,是因?yàn)檫@些出入?yún)⑵鋵?shí)是和 domain 目錄下的“實(shí)體類”是不同的,我們不希望將“領(lǐng)域?qū)印眱?nèi)部的業(yè)務(wù)邏輯暴露出去(也就是不將“實(shí)體類”的業(yè)務(wù)邏輯暴露出去)。
業(yè)務(wù)價(jià)值層有個(gè) sharedcontext,這是因?yàn)榭紤]到代碼復(fù)用、可能會出現(xiàn)某些“值對象”、“發(fā)布語言(即服務(wù)接口出入?yún)?”類被多個(gè)上下文共用,具體細(xì)節(jié)我在后面的戰(zhàn)術(shù)設(shè)計(jì)中會講到。
對于“群買菜”系統(tǒng)來說,由于小程序前端界面已經(jīng)存在,本次是服務(wù)端做 DDD 改造,故不打算對前后端交互接口進(jìn)行調(diào)整。為此,我為其設(shè)計(jì)了“邊緣層”,也就是 BFF 層。這就是 edge 目錄存放的代碼內(nèi)容。