領域驅動設計(DDD)——大型結構

語言: CN / TW / HK

簡單回顧

上一篇介紹了“精煉”,我們學習了可以用於精煉的幾種模式,比如 CORE DOMAIN 模式, GENERIC SUBDOMAIN 模式, DOMAIN VISION STATEMENT 模式等等。 使用這些模式讓開發者們能夠把注意力集 中到核心元素上,並把其他元素表示為支持作用 。

但如果不貫徹某個主旨來應用一些系統級的設計元素和模式的話,關係仍然可能非常混亂。本章節我將概要介紹幾種大型結構方法,然後詳細討論其中一種模式—— RESPONSIBILITY LAYER(職責層),通過這個示例來探索使用大型結構的含義。

複雜系統面臨的真實問題

背景

硅谷一家小設計公司簽了一份為衞星通信系統創建模擬器的合同。工作進展得很順利,他們正在利用MODEL-DRIVEN DESIG來進行開發。為了澄清模型中的複雜關係,他們已經把設計分解為一些在規模上便於管理的內聚MODULE,於是現在便有了的很多MODULE

問題

隨着越來越多的MODULE被創建,現在系統開發面臨着這樣一些問題,開發人員要想查找某個功能,應該到哪個MODULE中去查呢?如果有了一個新類,應該把它放在哪裏?這些小軟件包的實際意義是什麼?它們又是如何協同工作的呢?而且以後還要創建更多的MODULE

開發人員互相之間仍然能夠進行很好的溝通,而且也知道每天都要做什麼工作,但項目領導者卻不滿足這種一知半解的狀態。他們需要某種組織設計的方式,以便在項目進入到更復雜的階段時能夠理解和掌控它

解決方案1.0

開發人員提出了不同的打包方案。有一些文檔給出了系統的全貌,還有一些使用建模工具繪製的類圖一一新視圖可以用來指引開發人員找到正確的模塊。但項目領導者對這些小花招並不滿意。

缺點

1)方案1.0可以用模型把模擬器的工作流程簡單地描述出來,也可以説清楚基礎設施是如何序列化數據的,以及電信技術層怎樣保證數據的完整性和路由選擇。雖然模型包含了所有細節,但是卻沒有一條清楚的主線。領域的一些重要概念丟失了。這次丟失的不是對象模型中的一兩個類,而是整個模型的結構

解決方案2.0

經過一兩週的仔細思考之後,開發人員有了思路。他們打算把設計放到一個結構中。整個模擬器將被看作由一系列層組成,這些層分別對應於通信系統的各個方面。最下面的層用來表示物理基礎設施,它具有將數據位從一個節點傳送到另一個節點的基本能力。它的上面是封包路由層,與數據流定向有關的問題都被集中到這一層中。其他的層則表示其他概念層次的問題。這些層共同描述了系統的大致情況。

按照新的結構來重構代碼。為了不讓MODULE跨越多個層,必須對它們重新定義。在一些情況下,還需要重構對象職責,以便明確地讓每個對象只屬於一個層。另一方面,藉由應用這些新思路的實際經驗,概念層本身的定義也得到了精化。層、MODULE和對象一起演變,最後,整個設計都符合了這種分層結構的大體輪廓。

簡單總結:將設計放到一個結構中。對整個系統重新分層,整個模擬器將被看作由一系列層組成,這些層分別對應於通信系統的各個方面。同時按照新的結構來重構代碼。

優點:

1)分層結構描述了系統的大體輪廓,設計變得易於理解。人們基本上知道到哪裏去尋找某個特定功能。分工不同的開發人員所做的設計決策
可以大體上互相保持一致。

問題結論

1)在一個大的系統中,如果因為缺少一種全局性的原則而使人們無法根據元素在模式(這些模式被應用於整個設計)中的角色來解釋這些元素,那麼開發人員就會陷入“只見樹木,不見森林”的境地。

2)我們需要理解各個部分在整體中的角色,而不必去深究細節。

由此案例引出大型結構這個概念

“大型結構”是一種語言,人們可以用它來從大局上討論和理解系統。它用一組高級概念或規則(或兩者兼有)來為整個系統的設計建立一種模式。這種組織原則既能指導設計,又能幫助理解設計。另外,它還能夠協調不同人員的工作,因為它提供了共享的整體視圖,讓人們知道各個部分在整體中的角色。

注意事項

大型結構可以被限制在一個BOUNDED CONTEXT中,但通常是跨多個BOUNDED CONTEXT中。當團隊規模較小而且模型也不太複雜時,只需將模型分解為合理命名的MODULE,再進行一定程度的精煉,然後在開發人員之間進行非正式協調,以上這些就足以使模型保持良好的組織結構了。不要強行套用大型結構。雖然大型結構可以節省項目的開發費用,但不適當的結構會嚴重妨礙開發的進展。以下內容將探討一些能成功構建這種設計結構的模式。

模式:EVOLVING ORDER (有序演化)

現狀

很多開發人員都親身經歷過由於設計結構混亂而產生的代價。為了避免混亂,項目通過架構從各個方面對開發進行約束。無論架構是面向技術的,還是面向領域的,如果其限定了很多前期設計決策,那麼隨着需求的變更和理解的深入,這些架構會變得束手束腳。

為了規避混亂,不得不做很多限定,但是過多限定,可能會產生如下問題:

一個沒有任何規則的隨意設計會產生一些無法理解整體含義且很難維護的系統。但架構中早期的設計假設又會使項目變得束手束腳,而且會極大地限制應用程序中某些特定部分的開發人員/設計人員的能力。很快,開發人員就會為適應結構而不得不在應用程序的開發上委曲求全,要麼就是完全推翻架構而又回到沒有協調的開發老路上來

問題並不在於指導規則本身應不應該存在,而在於這些規則的嚴格性和來源。如果這些用於控制設計的規則確實符合開發環境,那麼它們不但不會阻礙開發,而且還會推動開發在健康的方向上前進,並且保持開發的一致性。

解決方案

讓這種概念上的大型結構隨着應用程序一起演變,甚至可以變成一種完全不同的結構風格。不要依此過分限制詳細的設計和模型決策,這些決策和模型決策必須在掌握了詳細知識之後才能確定。根據實際經驗和領域知識來選擇結構,並避免採用限制過多的結構,如此可以降低折中的難度。真正適合領域和需求的結構能夠使細節的建模和設計變得更容易,因為它快速排除了很多選項。

結論

要想創建一種既為開發人員保留必要自由度同時又能保證開發工作不會陷入混亂的結構絕非易事。儘管人們已經在軟件系統的技術架構上投入了大量工作,但有關領域層的結構化研究還很少見。一些方法會破壞面向對象的範式,如那些按應用任務或按用例對領域進行分解的方法。整個領域的研究還很貧瘠。
當發現一種大型結構可以明顯使系統變得更清晰,而又沒有對模型開發施加一些不自然的約束時,就應該採用這種結構。

以下會介紹4中較為通用的大型結構模式,可以解決現階段大型系統開發過程中普遍存在的問題

模式:SYSTEM METAPHOR (系統隱喻)

隱喻思維在軟件開發(特別是模型)中是很普遍的。但極限編程中的“隱喻”卻具有另外一種含義,它用一種特殊的隱喻方式來使整個系統的開發井然有序。

系統隱喻

SYSTEM METAPHOR(系統隱喻)是一種鬆散的、易於理解的大型結構,它與對象範式是協調的。由於系統隱喻只是對領域的一種類比,因此不同模型可以用近似的方式來與它關聯,這使得人們能夠在多個BOUNDED CONTEXT中使用系統隱喻,從而有助於協調各個BOUNDED CONTEXT之間的工作。

作用

SYSTEM METAPHOR應該既能促進系統的交流,又能指導系統的開發。它可以增加系統不同部分之間的一致性,甚至可以跨越不同的BOUNDED CONTEXT。

注意事項

系統隱喻試一把雙刃劍,並不適用於所有項目。且所有隱喻都不是完全精確的,因此應不斷檢查隱喻是否過度或不恰當,當發現它起到妨礙作用時,要隨時準備放棄它,不要為了用而用。

結論

當系統的一個具體類比正好符合團隊成員對系統的想象,並且能夠引導他們向着一個有用的方向進行思考時,就應該把這個類比用作一種大型結構。“防火牆”就是一個類比,真實世界的防火牆可以防止火勢從其他建築蔓延到自身,而軟件“防火牆”可以保護局域網免受來自外部網絡的破壞

模式:RESPONSIBILITY LAYER (按照職責分層)(章節重點)

拋出觀點

如果每個對象的職責都是人為分配的,將沒有統一的指導原則和一致性,也無法把領域作為一個整體來處理。為了保持大模型的一致,有必要在職責分配上實施一定的結構化控制。

暗示:按職責分層,對應章節標題

什麼是層?

所謂的層,就是對系統進行劃分,每個層的元素都知道或能夠使用在它“下面”的那些層的服務,但卻不知道它“上面”的層,而且與它上面的層保持獨立。

雖然這種自發的分層方式雖然使跟蹤依賴性變得更容易,而且有時具有一定的直觀意義,但它對模型的理解並沒有多大的幫助,也不能指導建模決策。我們需要一種具有更明確目的的分層方式。

如何進行有意義的分層?

在一個具有自然層次結構的模型中,可以圍繞主要職責進行概念上的分層,這樣可以把分層和職責驅動的設計這兩個強有力的原則結合起來使用。這些職責必須比分配給單個對象的職責廣泛得多才行。
注意觀察模型中的概念依賴性,以及領域中不同部分的變化頻率和變化的原因。如果在領域中發現了自然的層次結構,就把它們轉換為寬泛的抽象職責。這些職責應該描述系統的高層目的和設計。對模型進行重構,使得每個領域對象、AGGREGATE和MODULE的職責都清晰地位於一個職責層當中。

為什麼要分層?

這種明確的職責分組可以提高模塊化系統的可理解性,因為MODULE的職責會變得更易於解釋。而高層次的職責與分層的結合為我們(開發人員/領域專家)提供了一種系統的組織原則

分層模式有一種變體最適合按職責來分層,我們把這種變體稱為RELAXED LAYERED SYSTEM(鬆散分層系統)[Buschmann etal.1996,p.45],如果採用這種分層模式,某一層中的組件可以訪問任何比它低的層,而不限於只能訪問直接與它相鄰的下一層。

上述內容太過抽象,我們可以通過示例來深入理解

案例

以運輸系統為例,以下是運輸系統模型的一部分。此時開發團隊已經提煉出一個CORE DOMAIN,他們正在尋找一個能體現整個系統的主題並且讓大家一致認同的大型結構。

下圖展示了在預訂期間如何使用模型來制定一個貨運線路。

團隊成員觀察到了一些自然的概念層次結構,比如討論運輸時間表時,不需要涉及到貨物。而討論貨物的跟蹤時,如果不知道它的運輸信息,就很難進行跟蹤。這裏有很清晰的概念依賴性。團隊分出了兩個層:作業層和支持這些作業的基礎層(也稱為能力層)。

公司的活動都被放到作業層中,最明顯的作業對象是 Cargo ,它是公司大部分日常活動的焦點。Route Specification 是 Cargo 不可或缺的一部分,它規定了運輸需求,而 Itinerary 是運輸計劃,這些對象都以 Cargo 為聚合根。

能力層反應了公司在執行作業時所能利用的資源。為貨輪制定航程時間表時,需要考慮貨輪的貨運能力,因此 Transport Leg 是一個典型的例子。

把 Customer 對象放在哪裏是一個稍微複雜一點決策。這家運輸公司需要與客户保持長期關係,因為大部分業務來自回頭客。所以 Customer 應該屬於能力層。如果是快遞公司,客户可能只是臨時對象,在包裹投遞完成後就被遺忘了,此時客户僅與作業相關,可以放在作業層。

雖然作業層與能力層的區別是這張圖看上去很清楚了,但次序仍需要進一步細化。經過幾個星期的試驗後,團隊發現 Router 並不是作業(計劃)的一部分,它是用來幫助修改計劃的,因此定義了一個新的層,用來支持決策(Decision Support)。


決策支持層可以為用户提供用於制定計劃和決策的工具。Router 是一個 SERVICE,能幫助選擇運送貨物的最佳路線,因此屬於決策支持層。原先在 Transport Leg 中的 is preferred 屬性是因為公司希望優先使用自己的貨輪,當 Router 選擇最佳路線時會用到這個屬性,因此它與能力層毫無關係,所以最終被重構到了 Route Bias Policy 中。


在多次重構之後這個模型更加符合大型結構了。


從案例中可得知,在選擇合適的 RESPONSIBILITY LAYER 時,可從以下幾個方面考量。

1)場景描述

層應該能夠表達出領域的基本現實或優先級。選擇一種大比例結構與其説是一種技術決策,不如説是一種業務建模決策。層應該顯示出業務的優先級。

2)概念依賴性

較高層概念的意義應該依賴較低層,而底層概念應該獨立於較高的層。

3)CONCEPTUAL CONTOUR(概念輪廓)

如果不同層的對象必須具有不同的變 化頻率或原因,那麼層應該能夠容許它們之間的變化。

以下是常見的幾種分層


1)能力層

我們能夠做什麼?能力層不關心我們打算做什麼,而關心能夠做什麼。也稱為潛能層。

2)作業層

我們正在做什麼?我們利用這些潛能做了什麼事情?像潛能層(能力層)一樣,這個層也應該反映出現實狀況,而不是我們設想的狀況。我們希望在這個層中看到自己的工作和活動:我們正在銷售什麼,而不是能夠銷售什麼。

3)決策支持層

應該採用什麼行動或指定什麼策略?這個層是用來作出分析和制定決策的。它根據來自較低層(比如潛能層或作業層)的信息進行分析。決策支持軟件可以利用歷史信息來主動尋找適用於當前和未來作業的機會。 很多項目利用數據倉庫技術實現決策支持層,此時決策支持層實際上變成了一個獨特的BOUNDED CONTEXT,並且與作業層軟件具有一種CUSTOMER/SUPPLIER關係。

4)策略層

規則和目標是什麼?策略層可以和其他層使用同一種語言來編寫,但他們有時候是使用規則引擎來實現的。所以也可以考慮放到單獨的 BOUNDED CONTEXT 中。


在金融服務或保險業中,潛能(能力)在很大程度上是由當前的運營狀況決定的。 例如金融服務、保險等行業; 一家保險公司在考慮籤保單承擔理賠責任時,要根據當前業務的多樣性來 判斷是否有能力承擔它所帶來的風險。潛能(能力)層有可能會被合併到作業層 中,這樣就會演變出一種不同的分層結構。

這些情況下經常出現的一個層是對客户所做出的承諾。

5)承諾層

我們承諾了什麼?這個層具有策略層的性質,因為它表述了一些指導未來運營的目標;但它也有作業層的性質,因為承諾是作為後續業務活動的一部分而出現和變化的。

上述5個層雖然對很多企業系統都適用,但並不是所有領域的概念都涵蓋在這5個層中。我們需要根據實際情況決定分層,層數過多將無法有效描述領域。

結論

在模型構建過程中,注意觀察模型中的概念依賴性,以及領域中不同部分的變化頻率和變化的原因。如果在領域中發現了自然的層次結構,就把它們轉換為寬泛的抽象職責。這些職責應該描述系統的高層目的和設計。對模型進行重構,使得每個領域對象、AGGREGATE和MODULE的職責都清晰地位於一個職責層當中。這樣重構後模型就會更加符合大型結構。

模式:KNOWLEDGE LEVEL (知識層級)

KNOWLEDGE LEVEL 是一組描述了另一組對象應該有哪些行為的現象。它並不像其他分析模式那樣對領域進行建模,而是用來構造模型的。

章節開頭,大圖説明什麼是KNOWLEDGE LEVEL

場景描述


當我們需要讓用户對模型的一部分有所控制,而模型又必須滿足更 大的一組規則時,可以利用KNOWLEDGE LEVEL(知識級別)來處理 這種情況。它可以使軟件具有可配置的行為,其中實體中的角色和關係 必須在安裝時(甚至在運行時)進行修改。

當對系統進行修改或替換時,開發人員會發現,有一些功能的真實含義並不想他們看上去的那樣。它們在不同情況下具有完全不同的含義。如果不破壞這些互相疊加的含義,修改任何東西都是非常困難的。想要把數據遷移到一個“更合適”的系統中,必須要理解這些奇怪的部分,並對其進行編碼。

看不懂/(ㄒoㄒ)/~~,通過案例深入理解

案例

以員工工資和退休金管理系統為例,員工分為小時工和受薪員工,小時工的退休金計劃為固定代扣制(Defined Contribution),而受薪員工為固定受益制(Defined Benefit)。

在這個模型中,每個員工隨便加入哪一種退休計劃都可以,因此每位辦公室行政人員都可以改變退休計劃。管理層最後放棄了這個模型,因為它沒有反映出公司的策略。

辦公室行政人員按小時付薪酬,且採用固定受益退休計劃。這個策略暗示出job title(工作頭銜)字段現在表示了一個重要的領域概念。開發人員可以重構模型,用Employee Type(員工類型)把這個概念明確顯示出來,如圖16-19和圖16-20所示。

只有超級管理員才能編輯 Employee Type 對象,而且只有公司策略變更時,他才能修改此對象。這些受限制的對象讓開發人員想到了 KNOWLEDGE LEVEL 模式,他嘗試按照這種思想,重新組織了一下模型:

受限制的對象都在 KNOWLEDGE LEVEL 中,可以自由編輯的對象都在 Operational Level 中,區分得非常清楚。由於模型變得更清晰了,開發人員發現 Employee Type 可以被指定為 Retirement Plan 中的任何一種, 也可以被指定為兩種工資類型中的任何一種。這裏實際上隱含了一個 Payroll 的概念,它和 Employee Type 混在了一起。於是開發人員重構了模型:

通過案例得出結論

如果在一個應用程序中,ENTITY的角色和它們之間的關係在不同的情況下有很大變化,那麼複雜性會顯著增加。這種情況下,無論是一般的模型還是高度定製的模型,都無法滿足用户的需求。為了兼顧各種不同的情形,對象需要引用其他的類型,或者需要具備一些屬性,用於在不同情況下采用不同使用方式。相當於在我們模型中嵌入了另一個模型,作用是用來描述我們的模型。KNOWLEDGE LEVEL分離了模型的自我定義的功能,並清晰地表達了它的限制。

KNOWLEDGE LEVEL 模式下,需要創建一組不同的對象,用他們來描述和約束基本模型的結構和行為。把這些對象分為兩個“級別”,一個是非常具體的級別,另一個級別則提供了一些可供用户或者超級用户定製的規則和知識。

相似和區別

KNOWLEDGE LEVEL 是 REFLECTION(反射)模式在領域層中的一種應用。Java中有一些基本的反射機制,用於查詢一個類的方法和屬性等信息。

KNOWLEDGE LEVEL 看上去像是 RESPONSIBILITY LAYER 的一個特例,但事實並非如此。KNOWLEDGE LEVEL 中兩個級別之間的依賴性是雙向的,而在層次結構中,依賴是單向的。

KNOWLEDGE LEVEL具有兩個很有用的特性。

1)首先,它關注的是應用領域,這一點與人們所熟悉的REFLECTION模式的應用正好相反。

2)其次,它並不追求完全的通用性。KNOWLEDGE LEVEL顯得更簡單,而且可以傳達設計者的特別意圖。

創建一組不同的對象,用它們來描述和約束基本模型的結構和行為。把這些對象分為兩個“級別”,一個是非常具體的級別,另一個級別則提供了一些可供用户或超級用户定製的規則和知識

模式:PLUGGABLE  COMPONENT FRAMEWORK (可插入式組件框架)

背景

當很多應用程序需要進行互操作時,如果所有應用程序都基於相同的一些抽象,但它們是獨立設計的,那麼在多個BOUNDEDCONTEXT之間的轉換會限制它們的集成。各個團隊之間如果不能緊密地協作,就無法形成一個SHARED KERNEL。重複和分裂將會增加開發和安裝的成本,而且互操作會變得很難實現。

解決方案

將它們的設計分解為組件,每個組件負責提供某些 類別的功能。通常所有組件都插入到一箇中央hub上,這個hub支持組件 所需的所有協議,並且知道如何與它們所提供的接口進行對話。還有其 他一些將組件連在一起的可行模式。對這些接口以及用於連接它們的 hub的設計必須要協調,而組件內部的設計則可以更獨立一些。

可插入式組件框架的白話文解釋

使用場景和要求

從接口和交互中提煉出一個 ABSTRACT CORE,並創建一個框架,這個框架需要允許這些接口的各種不同實現被自由替換。通用,無論是什麼應用程序,只要它嚴格的通過 ABSTRACT CORE 的接口進行操作,那麼就可以允許它使用這些組件。

缺點

1) 一個缺 點是它是一種非常難以使用的模式。它需要高精度的接口設計和一個非 常深入的模型,以便把一些必要的行為捕獲到ABSTRACT CORE中。

2) 它只為應用程序提供了有限的選擇。如果一個應用程 序需要對CORE DOMAIN使用一種非常不同的方 法,那麼可插入式組 件框架將起到妨礙作用。

結構應該有一種什麼樣的約束

從非常寬鬆的SYSTEM METAPHOR到嚴格的PLUGGABLE COMPONENT FRAMEWORK。當然,還有很多其他結構,而且,甚至在一個通用的結構模式中,在制定規則上也可以選擇多種不同的嚴格程度。

我們可以為每種不同的情況設計不同的事件機制,也可以讓特殊層 中的對象在交互時遵守一種一致的模式。結構越嚴格,一致性就越高, 設計也越容易理解。如果結構適當的話,規則將推動開發人員得出好的設計。不同的部分之間會更協調。

另一方面,約束也會限制開發人員所需的靈活性。在異構系統中, 特別是當系統使用了不同的實現技術時,可能無法跨越不同的 BOUNDED CONTEXT來使用非常特殊的通信路徑。

因此一定要剋制,不要濫用框架和死板地實現大比例結構。大比例 結構的最重要的貢獻在於它具有概念上的一致性,並幫助我們更深入地 理解領域。每條結構規則都應該使開發變得更容易實現。

通過重構得到更合適的結構

控制成本的一個關鍵是保持一種簡單、輕量級的結構。不要試圖使結構面面俱到。只需解決最主要的問題即可,其他問題可以留到後面一個一個地解決。開始最好選擇一種鬆散的結構,如SYSTEM  METAPHOR或幾個RESPONSIBILITY LAYER。不管怎樣,一種最小化的鬆散結構可以起到輕量級的指導作用,它有助於避免混亂。

整個團隊在新的開發和重構中必須遵守結構。要做到這一點,整個團隊必須理解這種結構。必須把術語和關係納入到UBIQUITOUS LANGUAGE中。

對模型施加的另一項關鍵工作是持續精煉。這可以從各個方面減小修改結構的難度。

總結

本章,重點闡述了大型結構系統中可能存在的一些真實問題,並分別給出了幾個指導原則進行加以改進。

模式:EVOLVING ORDER (有序演化):大型系統中的一些複雜規則和約束,在初期不要限制的太嚴格,因為隨着需求的不斷變化,這些嚴格的規則和約束往往會限制住開發人員的手腳,在初期可以稍微寬鬆點,對規則和約束逐步演進。

模式:SYSTEM METAPHOR (系統隱喻):有利於理解大型系統中的一些構件,需要把隱喻加到通用領域語言內。

模式:RESPONSIBILITY LAYER (按照職責分層):對於複雜系統,需要能通過職責劃分出不同的層。分層的藝術可以重點看下示例。

模式:KNOWLEDGE LEVEL (知識等級):用來描述另一組對象該有哪些行為,主要作用用來構造對象。構造的藝術需要重點消化下文中的示例

以下是各種大型結構和模型的關係、