基于上下文圖的策略性領域驅動開發
英文原文:Strategic Domain Driven Design with Context Mapping
作者:Alberto Brandolini 譯者:韓鍇 發布于 2010年4月6日
簡介
當應用程序逐漸變得龐大和復雜后,很多面向對象建模的方法都達不到非常好的可伸縮性。上下文圖(Context Mapping)是一種通用目的的技術,作為領域驅動開發大家族的一名成員,它幫助架構師和開發人員管理他們在軟件開發項目中不得不面對的形形色色的復雜性。與其他廣為人知的DDD模式相比,上下文圖可以應用在任何軟件開發的場景中,在開發者進行策略性決策時,為他們提供一個高層視圖,比如是采用全套的DDD實現,還是根據項目的特定條件進行取舍等。
在這篇文章中,我們將探討界限上下文,以及如何在構建上下文圖時應用它們,來支持軟件開發項目中的關鍵決策。
多個模型共存
領域驅動開發花了很大力氣強調一件事,即維護應用程序模型的概念完整性。要做到這一點,需要很多因素:
- 一種敏捷的流程,確保能從用戶和領域專家那里頻繁地獲得反饋,
- 確保有若干領域專家在場,并且與他們開展創造性的合作,
- (在應用和測試代碼中)維護單一的、可共享的模型,并用通用語言精確地進行定義它,
- 營造一種開放透明的環境,鼓勵學習與探索。
這些對于營造一個可以讓高質量的設計繁榮發展的環境至關重要。即使是這樣的環境,那些常見的DDD元素,比如實體、值對象、聚合,也會逐漸地形成一個復雜領域模型。所以,如果不對模型的概念完整性進行妥協的話,傳統的DDD方法也不能盲目地應用在一個無限大的領域模型中。
如圖1所示,在DDD中,通用語言的關鍵責任是對模型進行完整性檢查。無論是與領域專家的討論,還是最終的實現代碼,都可以通過使用相同的術語,并將領域知識清晰準確地進行定義,以此來保證團隊中的每位成員可以分享都相同的領域知識和軟件。
圖1. 通用語言應該是用于表達模型的唯一語言。團隊中的每位成員應該對每個特定術語達成一致。這些術語不能有歧義,也不允許在不同角色間進行翻譯。
代碼是表達模型的主要形式。雖然其他一些工件或許也能捕獲需求或者局部設計,但是只有代碼自身才會與應用程序的行為永遠保持一致。不過這種看上去美妙的建模方式其實非常脆弱:如果滿足了前面提到的四條要求,它能做到,但是不能被無限地擴展。真正讓模型得以最大程度地擴展,并且不必犧牲其概念完整性的方法叫做“上下文”。
了解“界限上下文”
在領域驅動設計的世界里,“上下文”是這樣定義的:
“一個單詞或一個句子所出現的環境,這個環境會反過來影響它們的含義。”
這段定義初看起來相當含糊。它并沒有直接告訴我們“上下文”的大小、形狀或者其他什么特性。但是最后我們又發現這個定義其實非常準確地描述了“上下文”是什么,如果要想窺得其全貌的話,大概還需要一些具體的例子。
示例1:術語相同,含義不同
第一個例子很簡單,它示范了在術語層面出現歧義的情況。有些詞匯根據不同的使用場景,會有不同的含義。
假設我們正在開發一個基于Web的個人金融管理程序(PFM)。該程序可能用于管理銀行帳戶(Account)、股票、儲蓄,未來可能用于追蹤預算和開銷記錄等等。
在這個程序中,領域術語“帳戶”可能是指不同的概念。談論銀行的時候,一個“帳戶”在邏輯上其實是“金錢的容器”;于是我們自然而然地為相應的類加上“余額”、“帳戶號”等屬性。但是,在“Web應用程序”的上下文中,術語“帳戶”會有非常不同的含義,它和用戶的認證、可信度有關。如圖2所示,相應的模型將是完全不同的。
圖2. 一個出現歧義的簡單場景:當術語“帳戶”應用在不同的上下文時,它的含義可以是完全不同的。
這應該是我們在對應用程序建模的時候,所遇到的最簡單的歧義場景了:一個術語,有兩個與上下文相關的含義。這個問題的解決辦法通常是在類的全名(類名或者包名)前面加一些前綴,以此來劃分名字空間。但是在概念層面上,必須清楚我們在和兩個上下文打交道,有時不同上下文之間的區別很大,足以防止開發人員犯錯誤,但有時這樣的區別卻非常微妙。
不過,在類名層面上解決問題的方式并不能用在所有的情況下:在銀行的領域里,術語“銀行帳戶”或許已經存在了,但卻有不同的含義;或者領域專家堅持使用“帳戶”作為術語。此時切記不要發明一個特殊的兩頭折衷的術語,或者在專家術語和代碼之間引入一個“翻譯層”。否則你將不得不面對兩個獨立的上下文。
繪制第一份上下文圖
當歧義真的到來的時候,我們需要一種工具來讓開發團隊明白,應用程序中正存在著兩個不同的上下文。“歧義”是通用語言的頭號大敵,我們必須鏟除它。消除歧義的最好辦法就是在上下文圖中,將領域結構分拆在多個界限上下文中。
圖3. 包含兩個領域上下文的上下文圖
按照領域驅動設計一書的描述,上下文圖是用于將上下文邊界變得更清晰的重要工具。其基本的想法是在白板上畫出上下文的邊界,然后選擇性地將相關類的領域術語填寫在上面。它不是一幅繪制精美的UML圖:它是一個可用的工具,允許我們描繪那種模糊不清的狀況,因此它自身看上去模糊不清也就不足為其了。
示例2:概念相同,用法不同
還有一種更加令人困惑的情況,就是底層的概念相同,但是使用的方式不同,最終導致了不同的模型。銀行帳戶的模型是一個BankingAccount類,如圖4所示。
圖4. 精簡版本的BankingAccount類
通常,有些PFM應用也允許我們管理支付行為,并且持有一個收款人(Payee)注冊器。在這個場景中,收款人可能與一個或者多個銀行帳戶關聯,但是對于收款人來說,我們既不能獲取其銀行帳戶的內部情況,也不能在該帳戶上觸發任何操作。那么將“收款人帳戶”與剛剛定義的BookingAccount類關聯在一起是否正確呢?
圖5. Payee類與BankingAccount類
恩......這聽上去有些道理:畢竟它們都是相同的概念,在現實世界中,我們的帳戶和收款人的帳戶甚至會處在同一個物理上的銀行里。另一方面,這樣做似乎又不完全正確:因為我們不允許調用收款人銀行帳戶的任何操作,也不能追蹤他們的任何信息。更糟的是,這樣做了后,可能會在我們的程序中埋下一個概念的錯誤。
我們應該如何做?我們應該再一次回到應用程序的兩個不同的上下文里去:這一次我們可以采取兩種不同的方式對同一個領域概念進行建模,因為對領域概念的兩種使用場景明顯不同,每一種都需要一個不同的模型。BankingAccount類仍然允許我們執行(或者跟蹤)特定的操作(比如存款與取款),同時另一個獨立的PayeeAccount類可能有一些和BankingAccount相同的通用數據(比如accountNumber),但是有一個簡化的模型和完全不同的行為(比如我們不能訪問收款人的余額信息)。圖6所示的正是這種場景:盡管“銀行帳戶”有著清晰的含義,其底層概念也是惟一的,但是在應用程序中卻以不同的方式被使用著。
圖6. BankingAccount和PayeeAccount類
這看著似乎挺明顯的,其實不然。當你設計類圖,或者使用UML建模工具時,你可能很自然地讓收款人具有一個bankingAccount屬性,而且會慶幸“我剛好有一個這樣的類”。Pavlovian試圖去除代碼中的重復,有時,它的作用會弊大于利。
如圖7所示的上下文圖,可以用于表述上面討論的示例。注意,只要我們關于環境的知識在增加,就將它反映在圖中。在這個例子中,我們將PFM的應用上下文分成了“銀行”和“開銷跟蹤”。
圖7. 非常簡單的上下文圖:畫上了領域模型區域間的輪廓,可以看出在這些區域內保證了概念的完整性
在這個例子中,兩個上下文擁有一些邏輯上重疊的區域,即“銀行帳戶”的概念,它在應用程序的不用區域中,使用方式也不同,這意味著我們要使用不同的模型。但是兩個模型又可能有非常緊密的交互。上下文圖除了能幫助我們保證模型的概念在不同上下文邊界內的完整性,它還能幫助我們關注在不同上下文之間出現的情況。在這個例子中,假設同一個團隊正在兩個上下文上同時工作,我們就需要讓團隊中的每位成員的明確兩個上下文的區別,并且就兩個上下文中出現的術語和概念,分享同一個轉換的映射關系。
示例3:外部系統
再來考慮一下PFM。很多這種應用程序都需要與某些金融在線服務進行數據交換。在這個例子中,銀行會為家庭銀行服務提供實時的訪問。其他的例子還包括允許用戶下載通用標準格式(比如Money或者Quicken格式)的銀行對帳單。但是從上下文圖的視角來看,無論是交互活動還是通訊的方向(單向或是雙向),并不重要。有一件事是要關注的,我們有了不同的模型。圖8展示了PFM與在線銀行應用程序的交互行為。
圖8. 在上下文圖中,與外部應用的交互行為很自然地需要獨立的界限上下文
即使設計兩個模型之初是讓它們展現相同的數據(至少在一定程度上),但隨著時間的推移,它們還是會受到不同因素的影響,而且它們也會用于不同的目的。因此,分離上下文邊界是必須的。如果假設用戶檔案(User profiling)模塊是由第三方庫實現的,那么示例1也能夠歸入到這一類中。
管理多個上下文
當應用程序跨越了多個上下文后,我們必須管理上下文之間的關聯。不同的界限上下文之間的關系,通常是我們深入觀察項目的線索。
有一件事情非常關鍵,即兩個上下文之間的聯系是有方向的。DDD用兩個專門的術語表述它們:“上游(upstream)”和“下游(downstream)”,一個上游上下文會影響到相應的下游上下文,但是反過來就不一定了。這不僅體現在代碼上(一個庫依賴于另一個),還體現在技術含義較少的因素上,比如進度、對外部請求的響應,比如,當在線銀行服務更改了API或者其他什么原因,我們的PFM銀行應用程序都必須要快速地更新。所以我們的PFM上下文應該是下游的,而在線銀行服務很明顯就是上游的了。圖9演示了這兩種領域上下文的關系。
圖9. 分離的上下文之間的Upstream-downstream關系
如果外部系統發生變化,我們可以接受這種變化,來更新與外部系統通訊的方式。不過我們仍然需要一些保護措施隔離來自上游上下文的變化,保證我們自己的“銀行”的上下文的概念完整性。DDD包含了幾種組織模式,幫助我們描述和管理不同的上下文交互方式。最適合我們在這里使用的是模式叫做反腐化層(Anti-Corruption Layer,ACL),它會在代碼層面上實現顯式的轉換,轉換可以在兩個上下文之間,或者在“銀行”上下文的外部邊界上完成。這不僅局限于技術上的轉換,比如Java轉化為XML,同時也是一個很合適的機會,能夠管理各個模型之間的所有微妙的不同。如下面的圖10所示,我們在上下文圖上添加了ACL。
圖10. PFM程序邊界上的反腐化層,防止在線銀行服務的變化影響到我們的邊界上下文
很明顯,一個外部系統需要一個獨立的上下文。然而對于一個已有的遺留組件,通常也伴有一個非常難以修改的模型。盡管遺留組件是在我們組織內部來維護的,甚至這個模型也會受到不同因素的影響,它會被其他的上下文所使用。如果必須和遺留系統進行交付,不同模型之間的轉換應該放在一個不同的界限上下文里。
上下文圖中還有其他的關系嗎?我們能夠根據相關的DDD模式對它們進行分類嗎?如果假設開發活動是在單一的團隊內進行的,那這里的模式就不會引起太多的關注。但是,如果“銀行”和“開銷”是由不同的團隊來維護的話,團隊之間應該是一種合作關系:他們的開發會朝向一個共同的目標(這里談論上游和下游沒有意義,因為他們處于同一級別)。如果Web用戶檔案模塊來自于外部,我們將會作為下游的上下文。
圖11. 加入了關系模式后的上下文圖
示例4:向組織擴展
到目前為止,我們只考慮了包含一個開發團隊的簡單場景。在這種場景下,我們可以忽略溝通的開銷,假設團隊中的每位開發者都很明確“模型將會如何發展”(也許有些樂觀)。更復雜的場景中還可能包含下面的影響因素:
- 領域復雜度(需要很多不同的領域專家)
- 組織復雜度
- 項目跨時很長
- 項目需要大量的人天
- 涉及到很多外部的、獨立的或者遺留的系統
- 大型團隊,多個開發組
- 分布的、離岸的團隊
- 個人因素
每個因素都會影響開發團隊和組織的通訊方式,并最終影響到要交付的軟件。
每個獨立的團隊,尤其是一個處在敏捷環境的團隊,團隊內的成員間有很多共享信息的方式:面對面的交談,多人參與的設計討論、結對編程、會議、信息散播裝置(information radiator)等等。但問題在于,當團隊規模、人數增加后,這些技術很難再繼續使用了,跨團隊地共享模型的概念完整性也非常困難。
畢竟,能夠對模型保持統一看法,是溝通中相當成熟的方式,這涉及到對問題具有一致的理解,以及對可行解決方案大致相似的看法。在那些溝通不順暢的場景下,“埋頭干”很容易取代了“識別和確認”。這種溝通瓶頸帶來的典型后果就是在同一個代碼庫中的不同地方散布著不同的類,它們做著基本上同樣的事情。
總有一天PFM應用會變得更大,這樣就要有另一個團隊(團隊B)和我們一起工作(顯然我們是團隊A),他們開發一個名為“交易”的新模塊。團隊B可能在一個不同的房間、建筑物、城市、公司里,他們全心投入到新模塊的開發工作上。如下圖所示,團隊A與團隊B共享了一些代碼,雖然他們很可能會使用彼此獨立的代碼。最后,團隊B會寫一些類(比如圖12所示的A')來實現自己所需的功能,不過這些功能已經存在于類A了。
圖12. 當不同的團隊訪問相同的代碼庫時,他們會去關心模型上的不同部分。物理上的團隊分割會令信息共享的效果大打折扣
這是重復代碼,萬惡之源啊!在一個獨立的、良好定義的、有界的上下文內,這是毋庸置疑的。但是由于某些原因,這種現象幾乎會出現在所有復雜的項目中。這通常是個信號,告訴我們在項目的同一個區域內,或許存在沒有恰當隔離的上下文。不過在有些時候,使用兩個獨立的上下文是組織領域模型更加有效的手段,而不會強迫兩個不同的團隊不斷地去整合他們的模型。
那么,我們如何在圖上畫出這些呢?上下文圖反映了當前我們對整個系統的理解水平。一旦我們學到了更多東西,或者環境發生了改變,還會馬上更新它。現在,我們還不能準確地預知接下來會發生什么,所以這就是“我們當前的理解水平”。
圖13. 尚未很好劃分的“交易”上下文,它還需要進一步探索或更切合實際的設計決策
圖中的危險警告符告訴我們那里有些問題:兩個上下文有局部的重疊,它們的關系還不是非常清晰。這可能是需要解決的第一類問題,可以嘗試著在上下文內設置一個被廣泛認可的、合理的關系,比如消費者-供應者、持續集成或者共享內核(Shared Kernel)。不過,這是明天的工作。上下文圖是為今天準備的工具,而問題在今天還存在著,所以我們還把警告符號留在圖中。
不要被圖中的顏色和陰影搞迷惑了。我不過是想讓上下文圖的打印效果更好一些。一個真實的上下文應該是很亂的,起碼和你的項目一樣亂。不過這個警告符提醒我們這里有一個危險區域,此處的上下文尚未被清晰地分離,事態很容易朝著“一團大泥球”發展(最有彈性的DDD組織模式),除非我們采取行動。
一種非傳統觀點的視角
上下文圖迫使我們將非軟件的因素也包含在整體考慮中,這樣我們更能識別出一些污點熱區,而這些熱區在傳統架構分析的觀點中是“不在范圍內”的。
比如,組織內部的信息流通方式會在很大程度上影響最終的軟件。通常,在小型組織中,組件自身的用途是定義上下文邊界的主要因素,而在大型組織中,這個關鍵因素變成了溝通效率和項目組織方式。像Wiki、email或即時消息軟件會給我們一種假象——團隊中每位成員的知識都不斷地保持著同步。但是我們都知道這只是個夢想罷了:在一個典型的大型項目中,我們不是Borg人(譯注:源自《星際旅行》中的外星生物,所有Borg人的思想是互聯的,可以完全共享知識)那樣的智能聯合體,很多人對于自己團隊以外的情況知之甚少。
在大型組織中定義上下文邊界是一項頗具挑戰的任務,但回報卻也相當豐厚。很多時候,各個團隊并不清楚多個模型存在的事實;之所以概念的完整性會頻遭破壞,是因為只有很少人或者沒有人看到完整的圖景。繪制上下文圖是一個不斷探索的過程,很多部分的內容在首次嘗試時都是不正確的,邊界在初期也是很模糊的,還需要很長的路要走,才能獲得一個更清晰的完整圖景。
圖14. 上下文圖的最新版本。不要指望它是“最終”的,我們總是會學到一些新的東西。
涉及到的上下文還可能更多,比如“交易”模塊可能需要鏈接到一些在線股票價格服務,但這是交易模塊的事!這個上下文圖是關于我們(團隊A)的,我們的工作內容是“銀行”和“開銷跟蹤”模塊:我們只對直接關聯的、會影響到自身軟件的那些上下文感興趣。
一旦我們收集到更多的信息,上下文圖就會變得更加清晰。正如前面提到的,只要認識到應用程序中存在著各種不同的模型,而且這些模型的完整性可以在一個良好定義的有界上下文中得以保存,這會為我們的領域建模的視角提供諸多益處。很多模型都在成長的過程中逐漸失去完整性,上下文圖會在這個方面給予我們很多幫助。
談談策略性DDD模式
此處我們使用模式的方式略有不同:盡管定義是一樣的——為一類反復出現的問題提供解決方案——但這些模式很少能展現出可供我們選擇的解決方案。更可能的場景是,組織架構會決定模式,我們惟一的希望就是在事態走到死胡同以前識別出它們。有些時候我們有機會選擇最好的選項,或者改變現有的狀況,但是我們必須清楚的是,在組織級別的改變所需的時間可能已經遠遠超過了項目持續的時間,或者這個改變根本就是不可能的。
如果你還在猶豫應該從那里開始,那么就從開發團隊開始吧。對于有效地共享模型信息來說,一個團隊應該是最大的組織單元。當識別出多個上下文后,可以由一個團隊管理它們,這樣很大程度上將問題歸結為架構的抉擇。
每一種模式都有不同的開銷:即使它們解決的是類似的問題(相近的上下文),也不能簡單地交換。比如,反腐化層會在代碼層面(一個額外層)上留下痕跡,并在組織里留下很小的痕跡。盡管Partnership或者“客戶-供應者”模式可能需要更少的代碼和一個單獨的代碼庫,但是如果沒有有效的溝通渠道和良好的過程的話,也很難應用起來。企圖在沒有合作的環境下安排執行Partnership模式,無異于自尋死路。
結論
讓我們在回到“上下文”最初的定義上來——“一個單詞或一個句子所出現的環境,這個環境會反過來影響它們的含義”——它非常準確,而且可以應用在設計層面、架構層面乃至組織層面上,卻沒有損失其準確性和有效性。盡管有些“對統一性的期望”是合情合理的,但是模型不能被無限地擴張。界限上下文提供了一種非常安全的機制,它允許模型在其內部不斷變得復雜,同時又不犧牲概念的完整性。
當把上下文圖應用到大型的項目上后,它還可以顯示出當前組織內的隱式邊界,并提供一個來自第一手的、沒有PS過的項目境況的快照。一個好的上下文圖能讓你看到所面對的不利條件的大致狀況。可能你已經知道——但通常都是不知道——組織是否在扮演項目成功的絆腳石,即使項目還沒有開始。
作為一名顧問,我發現上下文圖能夠奇跡般地讓我快速獲取客戶項目的細節。上下文圖還充當了策略決策的支持工具(畢竟,這是“圖”的本意)。上下文圖提供了系統的全局視圖,幫助我們關注在選擇那些能在你的環境中真正可行的方案,而不是把錢浪費在對系統不切實際的構想中,這是UML或者架構圖所做不到的。
關于作者
Alberto是來自Avanscoperta的一名咨詢顧問和培訓師。他致力于幫助客戶管理軟件開發的復雜度。他堅信只有那種全盤考慮的軟件開發方法才能開發出有用的軟件。
留言列表