鼠年大吉
HAPPY 2020'S NEW YEAR
不少領域驅動設計的專家都非常重視限界上下文。Mike 在文章《DDD: The Bounded Context Explained》中寫道:「限界上下文是領域驅動設計中最難解釋的原則,但或許也是最重要的原則,可以說,沒有限界上下文,就不能做領域驅動設計。在了解聚合根(Aggregate Root)、聚合(Aggregate)、實體(Entity)等概念之前,需要先了解限界上下文。」,然而,現實卻是很少有文章或著作專題講解該如何識別限界上下文。
《實現領域驅動設計》的作者 Vaughn Vernon 曾經被問到過如何在領域驅動設計中識別出正確的限界上下文?他思索了一會兒,回答說:「By experience.(憑經驗)」,這是一個機智的回答,答案沒有錯,可是也沒有任何借鑑意義,等於說了一句正確的廢話。
在軟體開發和設計領域,任何技能都是需要憑藉經驗積累而逐步提升的。然而作為一種設計方法,領域驅動設計強調了限界上下文的重要性,卻沒有提出一個值得參考並作為指引的過程方法,這是不負責任的。
Andy Hunt 在《程式設計師的思維修煉》這本書中分析了德雷福斯模型的 5 個階段:新手、高級新手、勝任者、精通者和專家。對於最高階段的「專家」,Andy Hunt 得到一個有趣的結論:「專家根據直覺工作(Experts work from intuition),而不需要理由。」,這似乎充滿了神秘主義,然而這種專家的直覺實際上是通過不斷的項目實踐千錘百鍊出來的,也可以認為是經驗的累積。經驗的累積過程需要方法,否則所謂數年經驗不過是相同的經驗重複多次罷了,沒有價值。Andy Hunt 認為需要給新手提供某種形式的規則去參照,之後,高級新手會逐漸形成一些總體原則,然後通過系統思考和自我糾正,建立或者遵循一套體系方法,就能從高級新手慢慢成長為勝任者、精通者。因此,從新手到專家是一個量變引起質變的過程,在沒有能夠養成直覺的經驗之前,我們需要有一套方法。
我在一些項目中嘗試著結合了諸多需求分析方法與設計原則,慢慢摸索出了屬於自己的一套體系。歸根結底,限界上下文就是「邊界」,這與面向對象設計中的職責分配其實是同一道理。限界上下文的識別並不是一蹴而就的,需要演化和迭代,結合著我對限界上下文的理解,我認為通過從業務邊界到工作邊界再到應用邊界這三個層次抽絲剝繭,分別以不同的視角、不同的角色協作來運用對應的設計原則,會是一個可行的識別限界上下文的過程方法。當然,這個過程相對過重,如果僅以此作為輸出限界上下文的方法,未免有些得不償失。需要說明的是,這個過程除了能夠幫助我們更加準確地識別限界上下文之外,還可以幫助我們分析需求、識別風險、確定架構方案。整體過程如下圖所示:
從業務邊界識別限界上下文
領域驅動設計圍繞著「領域」來開展軟體設計。在明確了系統的問題域和業務期望後,開發團隊與領域專家經過充分地溝通與交流,可以梳理出主要的業務流程,這些業務流程體現了各種參與者在這個過程中通過業務活動共同協作,最終完成具有業務價值的領域功能。顯然,業務流程結合了參與角色(Who)、業務活動(What)和業務價值(Why)。在業務流程的基礎上,我們就可以抽象出不同的業務場景,這些業務場景又由多個業務活動組成,我們可以利用前面提到的領域場景分析方法剖析場景,以幫助我們識別業務活動,例如採用用例對場景進行分析,此時,一個業務活動實則就是一個用例。
例如,在針對一款文學閱讀產品進行需求分析時,我們得到的業務流程為:
登錄讀者根據作品名或者作者名查詢自己感興趣的作品。在找到自己希望閱讀的作品後,開始閱讀。若閱讀的作品為長篇,可以按照章節閱讀,倘若作品為收費作品,則讀者需要支付相應的費用,支付成功後可以閱讀購買後的作品。在閱讀時,倘若讀者看到自己喜歡的句子或段落,可以作標記,也可以撰寫讀書筆記,還可以將自己喜歡的內容分享給別的朋友。讀者可以對該作品和作者發表評論,關注自己喜歡的作品和作者。註冊用戶可以申請成為駐站作者。審核通過的作者可以在創作平臺上發布自己的作品,發布作品時,可以根據需要設置作品的章節。作者可以在發布作品之前預覽作品,無論作品是否已經發布,都可以對作品的內容進行修改。作者可以設置自己的作品為收費或免費作品,並自行確定閱讀作品所需的費用。如果是新作品發布,系統會發送消息通知該作者的關注者;若連載作品有新章節發布,系統會發送消息通知該作品的關注者。駐站作者可以為自己的作品建立作品讀者群,讀者可以申請加入該群,加入群的讀者與作者可以在線實時聊天,也可以發送離線信息,或者將自己希望分享的內容發布到讀者群中。註冊用戶之間可以發起一對一的私聊,也可以直接給註冊用戶發送私信。通過對以上業務流程進行分析,結合在各個流程環節中需要的知識以及參與角色的不同,可以劃分如下業務場景:
閱讀作品創作作品支付社交消息通知註冊與登錄可以看到,業務流程是一個由多個用戶角色參與的動態過程,而業務場景則是這些用戶角色執行業務活動的靜態上下文。從業務流程中抽象出來的業務場景可能是交叉重疊的,例如在讀者閱讀作品流程與作者創作流程中,都牽涉到支付場景的相關業務。
接下來,我們利用領域場景分析的用例分析方法剖析這些場景。我們往往通過參與者(Actor)來驅動對用例的識別,這些參與者恰好就是參與到場景業務活動的角色。根據用例描述出來的業務活動應該與統一語言一致,最好直接從統一語言中擷取。業務活動的描述應該精準地表達領域概念,且通過儘可能簡潔的方式進行描述,通常格式為動賓形式。以閱讀作品場景為例,可以包括如下業務活動:
查詢作品收藏作品關注作者瀏覽作品目錄閱讀作品標記作品內容撰寫讀書筆記評價作品評價作者分享選中的作品內容分享作品連結購買作品一旦準確地用統一語言描述出這些業務活動,我們就可以從如下兩個方面識別業務邊界,進而提煉出初步的限界上下文:
語義相關性功能相關性語義相關性
從語義角度去分析業務活動的描述,倘若是相同的語義,可以作為歸類的特徵。語義相關性主要來自於描述業務活動的賓語。例如,前述業務活動中的查詢作品、收藏作品、分享作品、閱讀作品都具有「作品」的語義,基於這一特徵,我們可以考慮將這些業務活動歸為同一類。
識別語義相關性的前提是準確地使用統一語言描述業務活動。在描述時,應儘量避免使用「管理(manage)」或「維護(maintain)」等過於抽象的詞語。抽象的詞語容易讓我們忽視隱藏的領域語言,缺少對領域的精確表達。例如,在文學閱讀產品中,我們不能寬泛地寫出「管理作品」、「管理作者」、「維護支付信息」等業務活動,而應該挖掘業務含義,只有如此才能得到諸如收藏作品、撰寫作品、發布作品、設置作品收費模式、查詢支付流水、對帳等符合領域知識的描述。當然,這裡也有一個業務活動層次的問題。在進行業務分析時,若我們發現只能使用「管理」或「維護」之類的抽象字眼來表述該用戶活動時,則說明我們選定的用戶活動層次過高,應該繼續細化。細化後的業務活動既能更好地表達領域知識,又能讓我們更好地按照語義相關性去尋找業務的邊界,可謂一舉兩得。
在進行語義相關性判斷時,還需要注意業務活動之間可能存在不同的語義相關性。例如,在文學閱讀產品中,查詢作品、閱讀作品與撰寫作品具有「作品」的語義相關,而評價作品與評價作者又具有「評價」的語義相關,究竟應該以哪個語義為準呢?沒有標準!我們只能按照相關性的耦合程度進行判斷。如果我們將評價視為一個相對獨立的限界上下文,則評價作品與評價作者放入評價上下文會更好。
功能相關性
從功能角度去分析業務活動是否彼此關聯和依賴,倘若存在關聯和依賴,可以作為歸類的特徵,這種關聯性,代表了功能之間的相關性。倘若兩個功能必須同時存在,又或者缺少一個功能,另一個功能是不完整的,則二者就是功能強相關的。通常,這種功能相關性極具有欺騙性,因為系統總是包含這樣那樣彼此依賴的功能。要判斷這種依賴關係的強弱,並不比分析人與人之間的關係簡單。倘若我們運用用例分析方法,就可以通過用例之間的關係來判別功能相關性,如用例的包含與擴展關係,其中包含關係展現了功能的強相關性。所謂「功能相關性」,指的就是職責的內聚性,強相關就等於高內聚。故而從這個角度看,功能相關性的判斷標準恰好符合「高內聚、鬆耦合」的設計原則。
仍然以前面提到的文學閱讀產品為例。發布作品與驗證作品內容是功能相關的,且屬於用例的包含關係,因為如果沒有對發布的作品內容進行驗證,就不允許發布作品。對於這種強相關的功能,我們通常都會考慮將其歸入到同一個限界上下文。又例如發布作品與設置作品收費模式是功能相關的,但並非強相關,因為設置作品收費模式並非發布作品的前置約束條件,屬於用例中的擴展關係。但由於二者還存在語義相關性,因而將其放入到同一個限界上下文中也是合理的。
兩個相關的功能未必一定屬於同一個限界上下文。例如,購買作品與支付購買費用是功能相關的,且前者依賴於後者,但後者從領域知識的角度判斷,卻應該分配給支付上下文,我們非但不能將其緊耦合在一起,還應該竭盡所能降低二者之間的耦合度。因此,我在識別限界上下文時,僅僅將「功能相關性」作為一種可行的參考,它並不可靠,卻能給你一些提醒。事實上,功能相關性往往會與上下文之間的協作關係有關。由於這種功能相關性恰恰對應了用例之間的包含與擴展關係,它們往往又可成為識別限界上下文邊界的關鍵點。我在後面講解上下文映射時還會詳細闡釋。
為業務邊界命名
無論是語義相關性還是功能相關性,都是分類業務活動的一種判斷標準。一旦我們將識別出來的業務活動進行歸類,就自然而然地為它們劃定了業務邊界,接下來,我們需要對劃定的業務邊界進行命名,這個命名的過程其實就是識別所有業務活動共同特徵,並以最準確地名詞來表達該特徵。倘若我們劃分的業務活動欠妥當,對這個業務邊界命名就會成為一種巨大的挑戰。例如,我們從建立讀者群、加入讀者群,發布群內消息、實時聊天、發送離線消息、一對一私聊與發送私信等業務活動找到「社交」的共同特徵,因而得到社交上下文。但如果我們將閱讀作品、收藏作品與關注作者、查看作者信息放在一個業務邊界內,命名就變得有些棘手了,我們總不可能稱呼其為「作品與作者」上下文吧!因此,對業務邊界的命名可以算作是對限界上下文識別的一種檢驗手段。
整體而言,從業務邊界識別上下文的重點在於「領域」。若理解領域邏輯有誤,就可能影響限界上下文的識別。因此,這個階段需要開發團隊與領域專家緊密合作,這個階段也將是一個充分討論和分析的過程。它是一個迭代的過程。很多時候,如果我們沒有真正去實現這些限界上下文,我們有可能沒有完全正確地理解它。當我們距離真正理解業務還有距離的時候,不妨先「草率」地規劃它,待到一切都明朗起來,再尋機重構。
從工作邊界識別限界上下文
正如架構設計需要多個視圖來全方位體現架構的諸多要素,我們也應藉助更多的角度全方位分析限界上下文。如果說為限界上下文劃分業務邊界,更多的是從業務相關性(內聚)判斷業務的歸屬,那麼基於團隊合作劃分工作邊界可以幫助我們確定限界上下文合理的工作粒度。
倘若我們認可第 3-2 課中提及的三個原則或實踐:2PTs 規則、特性團隊、康威定律,則意味著項目經理需要將一個限界上下文要做的工作分配給大約 7~10 人的特性團隊。如此看來,對限界上下文的粒度識別就變成了對工作量的估算。我們並沒有嚴謹的算法去準確估算工作量,可是對於一個有經驗的項目經理(或者技術負責人),要進行工作量的大致估算,還是能夠辦到的。當我們發現一個限界上下文過大,又或者特性團隊的工作分配不均勻時,就應該果斷對已有限界上下文進行切分。
工作分配的基礎在於「儘可能降低溝通成本」,遵循康威定律,溝通其實就是項目模塊之間的依賴,這個過程同樣不是一蹴而就的。康威認為:
在大多數情況下,最先產生的設計都不是最完美的,主導的系統設計理念可能需要更改。因此,組織的靈活性對於有效的設計有著舉足輕重的作用,必須找到可以鼓勵設計經理保持他們的組織精簡與靈活的方法。
特性團隊正是用來解決這一問題的。換言之,當我們發現團隊規模越來越大,失去了組織精簡與靈活的優勢,實際上就是在傳遞限界上下文過大的信號。項目經理對此需要有清醒認識,當團隊規模違背了 2PTs 時,就該坐下來討論一下如何細分團隊的問題了。因此,按照團隊合作的角度劃分限界上下文,其實是一個動態的過程、演進的過程。
我在給某音樂網站進行領域驅動設計時,通過識別業務相關性劃分了如下限界上下文。
Media Player(online & offline):提供音頻和視頻文件的播放功能,區分在線播放與離線播放;Music:與音樂相關的業務,包括樂庫、歌單、歌詞;FM Radio:電臺;Live:直播;MV:短視頻和 MV;Singer:歌手;Musician:音樂人,注意音樂人與歌手的區別;Music Community:音樂社區;File Sharing:包括下載和傳歌等與文件有關的功能;Tag:支持標籤管理,包括音樂的分類如最新、話題等分類標籤還有歌曲標籤;Loyalty:與提高用戶粘度有關的功能,如關注、投票、收藏、歌單等功能;Utilities:音樂工具,包括音效增強等功能;Recommendation:推薦;Search:對整個音樂網站內容的搜索,包括對人、歌曲、視頻等內容的搜索;Activity:音樂網站組織的活動;Advertisement:推廣與廣告;Payment:支付。在識別限界上下文時,我將直播(Live)視為與音樂、電臺、MV 短視頻同等層次的業務分類,然而,殊不知該音樂網站直播模塊的開發團隊已經隨著功能的逐漸增強發展到了接近 200 人規模的大團隊,這顯然不是一個限界上下文邊界可以控制的規模。即使屬於直播業務的業務活動都與直播領域知識有關,我們也應該基於 2PTs 原則對直播限界上下文作進一步分解,以滿足團隊管理以及團隊成員充分溝通的需要。
如果我們從團隊合作層面看待限界上下文,就從技術範疇上升到了管理範疇。Jurgen Appelo 在《管理 3.0:培養和提升敏捷領導力(Management 3.0: Leading Agile Developers,Developing Agile Leaders)》這本書中提到,一個高效的團隊需要滿足兩點要求:
共同的目標團隊的邊界
雖然 Jurgen Appelo 在提及邊界時,是站在團隊結構的角度來分析的;可在設計團隊組織時確定工作邊界的原則,恰恰與限界上下文的控制邊界暗暗相合。總結書中對邊界的闡釋,大致包括:
團隊成員應對團隊的邊界形成共識,這就意味著團隊成員需要了解自己負責的限界上下文邊界,以及該限界上下文如何與外部的資源以及其他限界上下文進行通信。團隊的邊界不能太封閉(拒絕外部輸入),也不能太開放(失去內聚力),即所謂的「滲透性邊界」,這種滲透性邊界恰恰與「高內聚、鬆耦合」的設計原則完全契合。針對這種「滲透性邊界」,團隊成員需要對自己負責開發的需求「抱有成見」,在識別限界上下文時,「任勞任怨」的好員工並不是真正的好員工。一個好的員工明確地知道團隊的職責邊界,他應該學會勇於承擔屬於團隊邊界內的需求開發任務,也要敢於推辭職責範圍之外強加於他的需求。通過團隊每個人的主觀能動,就可以漸漸地形成在組織結構上的「自治單元」,進而催生出架構設計上的「自治單元」。同理,「任勞任怨」的好團隊也不是真正的好團隊,團隊對自己的邊界已經達成了共識,為什麼還要違背這個共識去承接不屬於自己邊界內的工作呢?這並非團隊之間的「惡性競爭」,也不是工作上的互相推諉;恰恰相反,這實際上是一種良好的合作,表面上維持了自己的利益,然而在一個組織下,如果每個團隊都以這種方式維持自我利益,反而會形成一種「互利主義」。
這種「你給我搔背,我也替你抓抓癢」的互利主義最終會形成團隊之間的良好協作。如果團隊領導者與團隊成員能夠充分認識到這一點,就可以從團隊層面思考限界上下文。此時,限界上下文就不僅僅是架構師局限於一孔之見去完成甄別,而是每個團隊成員自發組織的內在驅動力。當每個人都在思考這項工作該不該我做時,變相地就是在思考職責的分配是否合理,限界上下文的劃分是否合理。
從應用邊界識別限界上下文
質量屬性
管理的目的在於打造高效的團隊,但最後還是要落腳到技術實現上來,不懂業務分析的架構師不是一個好的程式設計師,而一個不懂得提前識別系統風險的程式設計師更不是一個好的架構師。站在技術層面上看待限界上下文,我們需要關注的其實是質量屬性(Quality Attributes)。如果把關乎質量屬性的問題都視為在將來可能會發生,其實就是「風險(Risk)」。
架構是什麼?Martin Fowler 認為:架構是重要的東西,是不容易改變的決策。如果我們未曾預測到系統存在的風險,不幸它又發生了,帶給系統架構的改變可能是災難性的。利用限界上下文的邊界,就可以將這種風險帶來的影響控制在一個極小的範圍,這也是前面提及的安全。為什麼說限界上下文是領域驅動設計中最重要的元素,答案就在這裡。
我曾經負責開發一款基於大數據平臺的 BI 產品,在架構設計時,對性能的評估方案是存在問題的,我們當時考慮了符合生產規模的數據量,並以一個相對可行的硬體與網絡環境,對 Spark + Parquet 的技術選型進行測試,測試結果滿足了設定的響應時間值。然而,兩個因素的缺失為我們的架構埋下了禍根。在測試時,我們沒有考慮並發訪問量,測試的業務場景也過於簡單。我們懷著一種鴕鳥心態,在理論上分析這種決策(Spark 是當時最快速的基於內存的數據分析平臺,Parquet 是列式存儲,尤為適合統計分析)是對的,然後就按照我們期望的形式去測試,實際上是將風險悄悄地埋藏起來。
當產品真正銷售給客戶使用時,我們才發現客戶的業務場景非常複雜,對性能的要求也更加苛刻。例如,它要求達到 100 ~ 500 的並發訪問量,同時對大數據量進行統計分析與指標運算,並期望實時獲得分析結果;而客戶所能提供的 Spark 集群卻是有限度的。事實上,基於 Spark 的 driver-worker 架構,它本身並不擅長完成高並發的數據分析任務。對於一個分析任務,Spark 可以利用集群的力量由多個 worker 同時並行地執行成百上千的 task,但瓶頸在 driver 端,一旦上遊同時有多個請求湧入,響應能力就不足了。最終,我們的產品在真正的壓力測試下一敗塗地。
幸而,我們劃定了限界上下文,並由此建立了數據分析微服務。針對客戶高並發的實時統計分析需求,在保證 REST API 不變的情況下,我們更改了技術選型,選擇基於 ElasticSearch 的數據分析微服務替換舊服務。這種改變幾乎不影響產品的其他模塊與功能,前端代碼僅僅做了少量修改。3 個人的團隊在近一個月的周期內基本完成了這部分數據分析功能,及時掐斷了炸藥的導火線。
重用和變化
無論是重用領域邏輯還是技術實現,都是在設計層面上我們必須考慮的因素,需求變化更是影響設計策略的關鍵因素。我在前面分析限界上下文的本質時,就提及一個限界上下文其實是一個「自治」的單元。基於自治的四個特徵,我們也可以認為這個自治的單元其實就是邏輯重用和封裝變化的設計單元。這時,對限界上下文邊界的考慮,更多是出於技術設計因素,而非業務因素。在後面講解的上下文映射(Context Map)模式時,Eric Evans 總結的共享內核其實就是重用的體現,而開放主機服務與防腐層則是對變化的主動/被動應對。
運用重用原則分離出來的限界上下文往往對應於子領域(Sub Domain),尤其作為支撐子領域。我在為一家公司的物流聯運管理系統提供領域驅動設計諮詢時,通過與領域專家的溝通,我注意到他在描述運輸、貨站以及堆場的相關業務時,都提到了作業和指令的概念。雖然屬於不同的領域,但指令的收發、作業的制訂與調度都是相同的,區別只在於作業與指令的內容,以及作業調度的周期。為了避免在運輸、貨站與堆場各自的限界上下文中重複設計與實現作業與指令等領域模型,我們可以將作業與指令單獨劃分到一個專門的限界上下文中。它作為上遊限界上下文,提供對運輸、貨站與堆場的業務支撐。
限界上下文對變化的應對,其實是「單一職責原則」的體現,即一個限界上下文不應該存在兩個引起它變化的原因。還是這個物流聯運管理系統,最初團隊的設計人員將運費計算與帳目、結帳等功能放在了財務上下文中。當國家的企業徵稅策略發生變化時,會引起財務上下文的變化,引起變化的原因是財務規則與政策的調整。倘若運費計算的規則也發生了變化,同樣會引起財務上下文的變化,但引起變化的原因卻是物流運輸的業務需求。如果我們將運費計算單獨從財務上下文中分離出來,就可以獨立演化,符合前面提及的「自治」原則,實現了兩種不同關注點的分離。
遺留系統
自治原則的唯一例外是遺留系統,因為領域驅動設計建議的通常做法是將整個遺留系統視為一個限界上下文。那麼,什麼是遺留系統?根據維基百科的定義,它是一種舊的方法、舊的技術、舊的計算機系統或應用程式,這個定義並不能解釋遺留系統的真相。我認為,系統之所以成為遺留系統,關鍵在於知識的缺乏。文檔不夠全面真實,掌握系統知識的團隊成員泰半離開,系統的代碼可能是一個大泥團。因此,我對遺留系統的定義是「一個還在運行和使用,但已步入軟體生命衰老期的缺乏足夠知識的軟體系統」。
倘若運用領域驅動設計的系統要與這樣一個遺留系統打交道,應該怎麼辦?竊以為,粗暴地將整個遺留系統包裹在一個限界上下文中,未免太理想化和簡單化了。要點還是自治,這時候我們應該站在遺留系統的調用者來觀察它,考慮如何與遺留系統集成,然後逐步對遺留系統進行抽取與遷移,形成自治的限界上下文。
在這個過程中,我們可以借鑑技術棧遷移中常常運用的「抽象分支(Branch By Abstraction)」手法。該手法會站在消費者(Consumer)一方觀察遺留系統,找到需要替換的單元(組件);然後對該組件進行抽象,從而將消費者與遺留系統中的實現解耦。最後,提供一個完全新的組件實現,在保留抽象層接口不變的情況下替換掉遺留系統的舊組件,達到技術棧遷移的目的:
如上圖所示的抽象層,本質就是後面我們要提到的「防腐層(Anticorruption Layer)」,通過引入這麼一個間接層來隔離與遺留系統之間的耦合。這個防腐層往往是作為下遊限界上下文的一部分存在。若有必要,也可以單獨為其創建一個獨立的限界上下文。
設計驅動力
結合業務邊界、工作邊界和應用邊界,形成一種層層推進的設計驅動力,可以讓我們對限界上下文的設計變得更加準確,邊界的控制變得更加合理,畢竟,限界上下文的識別對於整個系統的架構至關重要。在領域驅動的戰略設計階段,如果我們對識別出來的限界上下文的準確性還心存疑慮,那麼比較實際的做法是保持限界上下文一定的粗粒度。倘若覺得功能的邊界不好把握分寸,可以考慮將這些模稜兩可的功能放在同一個限界上下文中。待到該限界上下文變得越來越龐大,以至於一個 2PTs 團隊無法完成交付目標;又或者該限界上下文的功能各有不同的質量屬性要求;要麼就是因為重用或變化,使得我們能夠更清楚地看到分解的必要性;此時我們再對該限界上下文進行分解,就會更加有把握。這是設計的實證主義態度。
通過以上過程去識別限界上下文,僅僅是一種對領域問題域的靜態劃分,我們還缺少另外一個重要的關注點,即:限界上下文之間是如何協作的?倘若限界上下文識別不合理,協作就會變得更加困難,尤其當一個限界上下文對應一個微服務時,協作成本更會顯著增加。反過來,當我們發現彼此協作存在問題時,說明限界上下文的劃分出現了問題,這算是對識別限界上下文的一種驗證方法。Eric Evans 將這種體現限界上下文協作方式的要素稱之為「上下文映射(Context Map)」。
理解限價上下文
一個軟體系統通常被分為多個限界上下文,這是運用「分而治之」思想來降低業務複雜度的有效手段,設計的難題往往會停留在「如何分」,然而限界上下文之間的「怎麼合」問題同樣值得關注,分與合遵循的還是軟體設計的最高原則——高內聚、鬆耦合。分是合的基礎,基於內聚相關度進行合理的分配,可以在一定程度減少限界上下文之間不必要的關聯。假設分配是合理的,則接下來的「合」就是要儘可能地降低彼此之間的耦合。
既然前面提及限界上下文的識別是一個迭代過程,當我們在思考限界上下文該如何協作時,倘若發現協作總有不合理之處,就可能會是一個「設計壞味道」的信號,它告訴我們:之前識別的限界上下文或有不妥,由是可以審視之前的設計,進而演進為更為準確的限界上下文劃分。即使拋開對設計的促進作用,思考限界上下文是如何協作的,仍然格外重要,我們既要小心翼翼地維護限界上下文的邊界,又需要它們彼此之間良好的協作,並思考協作的具體實現方式,這個思考過程既牽涉到邏輯架構層面,又與物理架構有關,足以引起我們的重視。
領域驅動設計通過上下文映射(Context Map) 來討論限界上下文之間的協作問題,上下文映射是一種設計手段,Eric Evans 總結了諸如共享內核(Shared Kernel)、防腐層(Anticorruption Layer)、開放主機服務(Open Host Service)等多種模式。由於上下文映射本質上是與限界上下文一脈相承的,因此要掌握這些協作模式,應該從限界上下文的角度進行理解,著眼點還是在於「邊界」。領域驅動設計認為:上下文映射是用於將限界上下文邊界變得更清晰的重要工具。所以當我們正在為一些限界上下文的邊界劃分而左右為難時,不妨先放一放,在定下初步的限界上下文後,通過繪製上下文映射來檢驗,或許會有意外收穫。
限界上下文的一個核心價值,就是利用邊界來約束不同上下文的領域模型,以保證模型的一致性。然而,每個限界上下文都不是獨立存在的,多數時候,都需要多個限界上下文通力協作,才能完成一個完整的用例場景。例如,客戶之於商品、商品之於訂單、訂單之於支付,貫穿起來才能完成「購買商品」的核心流程。
兩個限界上下文之間的關係是有方向的,領域驅動設計使用兩個專門的術語來表述它們:「上遊(Upstream)」和「下遊(Downstream)」,在上下文映射圖中,以 U 代表上遊,D 代表下遊,理解它們之間的關係,正如理解該術語隱喻的河流,自然是上遊產生的變化會影響到下遊,反之則不然。故而從上遊到下遊的關係方向,代表了影響產生的作用力,影響作用力的方向與程式設計師慣常理解的依賴方向恰恰相反,上遊影響了下遊,意味著下遊依賴於上遊。
在劃分限界上下文的業務邊界時,我們常常從「語義相關性」與「功能相關性」兩個角度去判別職責劃分的合理性。在上下文映射中,我發現之所以兩個業務邊界的限界上下文能產生上下遊協作關係,皆源於二者的功能相關性,這種功能相關存在主次之分,往往是上遊限界上下文作為下遊限界上下文的功能支撐,這就意味著在當前的協作關係下,下遊限界上下文中的用例才是核心領域。例如,訂單與支付,下訂單用例才是核心功能,支付功能作為支撐的公開服務而被調用;例如,郵件與文件共享,寫郵件用例才是核心功能,上傳附件作為支撐的公開服務而被調用;例如,項目管理與通知,分配任務用例才是核心功能,通知功能作為支撐的公開服務而被調用。巧的是,這種主次功能的調用關係,幾乎對應的就是用例圖中的包含用例或擴展用例。
如果我們通過用例圖來幫助識別限界上下文,那麼,用例圖中的包含用例或擴展用例或許是一個不錯的判斷上下文協作關係的切入點。選擇從包含或擴展關係切入,既可能確定了職責分離的邏輯邊界,又可以確定協作關係的方向,這就是用例對領域驅動設計的價值所在了。
那麼,如何將上下文映射運用到領域驅動的戰略設計階段?Eric Evans 為我們總結了常用的上下文映射模式。為了更好地理解這些模式,結合限界上下文對邊界的控制力,再根據這些模式的本質,我將這些上下文映射模式分為了兩大類:團隊協作模式與通信集成模式。前者對應的其實是團隊合作的工作邊界,後者則從應用邊界的角度分析了限界上下文之間應該如何進行通信才能提升設計質量。針對通信集成模式,結合領域驅動設計社區的技術發展,在原有上下文映射模式基礎上,增加了發布/訂閱事件模式。
本文腦圖:
本文首發: