《領域驅動設計精簡版》讀書筆記(4)——保持模型一致性

保持模型一致性

當遇到需要若干個團隊通力配合的大型項目,我們應該做的是有意識地將大模型分解成數個較小的部分。只要遵守相綁定的契約,整合得好的小模型會越來越有獨立性。每個模型都應該有一個清晰的邊界,模型之間的關係也應該被精確地定義。有一整套技術可以保證模型的完整性:
保證模型完整性技術

界定上下文(Defined Context)

定義模型的範圍,畫出它的上下文的邊界,然後盡最大可能保持模型的一致性。要在模型涵蓋整個企業項目時保持它的純潔是很困難的,但是在它被限定到一個特定區域時就相對容易很多。要在應用到模型的地方明確定義上下文。在團隊組織裏明確定義邊界,在應用的具體部分明確定義用法,以及像代碼庫和數據庫 Schema 的物理顯示。保持模型在這些邊界裏的嚴格一致,不要因外界因素的干擾而有異動。被界定的上下文不是模型,界定的上下文提供有模型參與的邏輯框架。模塊被用來組織模型的要素,因此界定的上下文包含模塊

當不同的團隊不得不共同工作於一個模型時,我們必須要各司其職。要時刻意識到任何針對模型的變化都有可能破壞現有的功能。當使用多個模型時,每個人可以自由使用自己的那一部分。我們都知道自己模型的侷限,都恪守在這些邊界裏。我們需要確保模型的純潔、一致和完整。每個模型應能使重構儘可能容易,而不會影響到其他的模型。而且爲了達到純潔的最大化,設計還要可以被精簡和提煉。

持續集成(Continuous Integration)

一個團隊工作於一個界定的上下文,也有犯錯誤的空間。在團隊內部我們需要充分的溝通,以確保每個人都能理解模型中每個部分所扮演的角色。如果一個人不理解對象間的關係,他就可能會以和原意完全相反的方式修改代碼。如果我們不能百分之百地專注於模型的純潔性,就會很容易犯這種錯誤。團隊的某個成員可能會在不知道已經有自己所需代碼的情況下增加重複代碼,或者擔心破壞現有的功能而不改變已有的代碼選擇重複增加。

模型不是一開始就被完全定義。模型先被創建,然後基於對領域新的發現和來自開發過程的反饋等再繼續完善,這意味着新的概念會進入模型,新的部分也會被增加到代碼中。所有的這些需求都會被集成進一個統一的模型,進而用代碼實現之。這也就是爲什麼持續集成在界定的上下文中如此必要的原因。我們需要這樣一個集成的過程,以確保所有新增的部分和模型原有的部分配合得很好,在代碼中也被正確地實現。我們需要有個過程來合併代碼,合併得越早越好。

持續集成是基於模型中概念的集成,然後再通過測試實現。任何不完整的模型在實現過程中都會被檢測出來。持續集成應用於界定的上下文,不會被用來處理相鄰上下文之間的關係。

上下文映射(Context Map)

一個企業應用有多個模型,每個模型有自己界定的上下文。建議用上下文作爲團隊組織的基礎。在同一個團隊裏的人們能更容易地溝通,也能很好地將模型集成和實現。但是每個團隊都工作於自己的模型,所以最好讓每個人都能瞭解所有的模型。上下文映射是指抽象出不同界定上下文和它們之間關係的文檔,它可以是像下面所說的一個示意圖(Diagram),也可以是其他任何寫就的文檔。詳細的層次各有不同。它的重要之處是讓每個在項目中工作的人都能夠得到並理解它。
示意圖
只有獨立的統一模型還不夠。它們還要被集成,因爲每個模型的功能都只是整個系統的一部分。在最後,單個的部分要被組織在一起,整個的系統必須能正確工作。如果上下文定義的不清晰,很有可能彼此之間互相覆蓋。如果上下文之間的關係沒有被抽象出來,在系統被集成的時候它們就有可能不能工作。

共享內核(Shared Kernel)

當缺少功能集成時,持續集成可能就遙不可及了。尤其是在團隊不具備相關的技術或者行政組織來維護持續集成,或者是某個團隊又大又笨拙的時候。協同工作於有緊密關係的應用程序上的不協調團隊有時會進展很快,但他們所做的有可能很難整合。他們在轉換層和技巧花樣上花費了過多的時間,而沒有在最重要的持續集成上下功夫,做了許多重複勞動也沒有體味到通用語言帶來的好處。因此,需要指派兩個團隊統一共享的領域模型子集,當然除了模型的子集部分,還要包括代碼自己或者和模型相關聯的數據庫設計子集。這個明確被共享的東西有特別的狀態,沒有團隊之間的溝通不能做修改。要經常整合功能系統,但是可以不用像在團隊裏進行持續集成那麼頻繁。在集成的時候,在兩個團隊裏都要運行測試。

共享內核的目的是減少重複,但是仍保持兩個獨立的上下文。對於共享內核的開發需要多加小心。兩個開發團隊都有可能修改內核代碼,還要必須整合所做的修改。如果團隊用的是內核代碼的副本,那麼要儘可能早地融合(Merge)代碼,至少每週一次。還應該使用測試工具,這樣每一個針對內核的修改都能快速地被測試。內核的任何改變都應該通知另一個團隊,團隊之間密切溝通,使大家都能瞭解最新的功能。

客戶-供應商(Customer-Supplier)

我們經常會遇到兩個子系統之間關係特殊的時候:一個嚴重依賴另一個。兩個子系統所在的上下文是不同的,而且一個系統的處理結果被輸入到另外一個。它們沒有共享的內核,因爲從概念上理解也許不可以有這樣一個內核,或者對兩個子系統而言要共享代碼在技術上也不可能實現。

我們曾討論了一個關於在線商店應用的模型,包括報表和通訊兩部分內容。我們已經解釋說最好要爲所有的那些上下文創建各自分開的模型,因爲只有一個模型時會在開發過程中遇到瓶頸和資源的爭奪。假設我們同意有分開的模型,那麼在Web 商店字系統和報表系統間的關係是什麼樣子的呢?共享內核看上去不是好的選擇。子系統很可能會用不同的技術被實現。一個是純瀏覽器體驗,而另一個可能是豐富的 GUI 應用。儘管如果報表應用是用 Web 接口實現,各自模型的注意概念也是不同的。也許會有越界的情況,但還不足以應用共享內核。所以我們選擇走不同的道路。另外,E 商店子系統並不全依賴報表系統。E 商店應用的用戶是 Web 客戶,是那些瀏覽商品並下單的人。所有的客戶、商品和訂單數據被放在一個數據庫裏。就是這樣。E 商店應用不會真的關心各自的數據發生了什麼。而同時,報表應用非常關心和需要由 E 商店應用保存的數據。它還需要一些額外的信息以執行它提供的報表服務。客戶可能在購物籃裏放了一些商品,但在結賬的時候又去掉了。某個客戶訪問的鏈接可能多於其他人等。這樣的信息對 E 商店應用沒有什麼意義,但是對報表應用卻意義非凡。由此,供應商子系統不得不實現一些客戶子系統會用到的規範。這是聯繫兩個子系統的紐帶。

當我們面對這樣一個場景時,應該就開始“演出”了。報表團隊應該扮演客戶角色,而 E 商店團隊應該扮演供應商角色。兩個團隊應該定期碰面或者提邀請,像一個客戶對待他的供應商那樣交談。客戶團隊應該代表系統的需求,而供應商團隊據此設置計劃。當客戶團隊所有的需求都被激發出來後,供應商團隊就可以決定實現它們的時間表。如果認爲一些需求非常重要,那麼應該先實現它們,延遲其他的需求。客戶團隊還需要輸入和能被供應商團隊分享的知識。

在兩個團隊之間確定一個明顯的客戶/供應商關係。在計劃場景裏,讓客戶團隊扮演和供應商團隊打交道的客戶角色。爲客戶需求做充分的解釋和任務規劃,讓每個人理解相關的約定和日程表。

順從者(Compliant)

在兩個團隊都有興趣合作時,客戶-供應商關係是可行的。客戶非常依賴於供應商,但供應商不是。如果有管理保證合作的執行,供應商會給於客戶需要的關注,並聆聽客戶的要求。如果管理沒有清晰地界定在兩個團隊之間需要完成什麼,或者管理很差,或者就沒有管理,供應商慢慢地會越來越關心它的模型和設計,而也越來越疏於幫助客戶。畢竟他們有自己的工作完成底線。即使他們是好人,願意幫助其他團隊,時間的壓力卻不允許他們這麼做,客戶團隊深受其害。在團隊屬於不同公司的情況下,這樣的事情也會發生。交流是困難的,供應商的公司也許沒興趣在關係溝通上投資太多。他們要麼提供少許幫助,或者直接拒絕合作。結果是客戶團隊孤立無援,只能儘自己的努力摸索模型和設計。

如果客戶不得不使用供應商團隊的模型,而且這個模型做得很好,那麼就需要順從了。客戶團隊遵從供應商團隊的模型,完全順從它。這和共享內核很類似,但有一個重要的不同之處——客戶團隊不能對內核做更改,他們只能用它做自己模型的一部分,可以在所提供的現有代碼上完成構建。在很多情況下,這種方案是可行的。當有人提供一個豐富的組件,並提供了相應的接口時,我們就可以將這個組件看作我們自己的東西構建我們的模型。如果組件有一個小的接口,那麼最好只爲它簡單地創建一個適配器,在我們的模型和組件模型之間做轉換。這會隔離出我們的模型,可以有很高的自由度去開發它。

防崩潰層(Anticorruption Layer)

我們會經常遇到所創建的新應用需要和遺留軟件或者其他獨立應用相交互的情況。對領域建模器而言,這又是一個挑戰。很多遺留應用從前沒有用領域建模技術構建,而且它們的模型模糊不清,難於理解,也很難使用。即使做得很好,遺留應用的模型對我們也不是那麼有用,因爲我們的模型很可能與它完全不同。因此,在我們的模型和遺留模型之間就須要有一個集成層,這也是使用舊應用的需求之一。

讓我們的客戶端系統和外面的系統交互有很多種方法。一種是通過網絡連接,兩個應用需要使用同一種網絡通信協議,客戶端需要遵從使用外部系統使用的接口。另外一個交互的方法是數據庫。外部系統使用存儲在數據庫裏的數據。客戶端系統被假定訪問同樣的數據庫。在這兩個案例中,我們所處理的兩個系統之間傳輸的都是原始數據。但是這看上去有些簡單,事實是原始數據不包括任何和模型相關的信息。我們不能將數據從數據庫中取出來,全部作爲原始數據處理。在這些數據後面隱含着很多語義。一個關係型數據庫含有和創建關係網的其他原始數據相關的原始數據。數據語義非常重要,並且需要被充分考慮。客戶端應用不能訪問數據庫,也不能不理解被使用數據的含義就進行寫入操作。我們看到外部模型的部分數據被反映在數據庫裏,然後進入我們的模型。

如果我們允許這樣的事情發生,那麼就會存在外部模型修改客戶端模型的風險。我們不能忽視和外部模型的交互,但是我們也應該小心地將我們的模型和它隔離開來。我們應該在我們的客戶端模型和外部模型之間建立一個防崩潰層。從我們模型的觀點來看,防崩潰層是模型很自然的一部分,並不像一個外部的什麼東西。它對概念和行爲的操作和我們的模型類似,但是防崩潰層用外部語言和外部模型交流,而不是客戶端語言。這個層在兩個域和語言之間扮演雙向轉換器,它最大的好處在於可以使客戶端模型保持純潔和持久,不會受到外部模型的干擾。

實現防奔潰層一個非常好的方案是將這個層看作從客戶端模型來的一個服務。使用服務是非常簡單的,因爲它抽象了其他系統並讓我們在自己的範圍內定位它。服務會處理所需要的轉換,所以我們的模型保持獨立。考慮到實際的實現,可以將服務看作比作一個Facade。除了這一點,防崩潰層最可能需要一個適配器(Adapter)。適配器可以使你將一個類的接口轉換成客戶端能理解的語言。在我們的這個例子中,適配器不需要一定包裝類,因爲它的工作是在兩個系統之間做轉換。
防奔潰層
防崩潰層也許包含多個服務。每一個服務都有一個相應的 Facade,對每一個 Facade 我們爲之增加一個適配器。我們不應該爲所有的服務使用一個適配器,因爲這樣會使我們無法清晰地處理繁多的功能。

我們還必須再增加一些組件。適配器將外部系統的行爲包裝起來。我們還需要對象和數據轉換,這會使用一個轉換器來解決。它可以是一個非常簡單的對象,有很少的功能,滿足數據轉換的基本需要。如果外部系統有一個複雜的接口,最好在適配器和接口之間再增加一個額外的 Facade。這會簡化適配器的協議,將它和其他系統分離開來。

獨立方法(Independent Method)

獨立方法模式適合一個企業應用可由幾個較小的應用組成,而且從建模的角度來看彼此之間有很少或者沒有相同之處的情況。它有一套自己的需求,從用戶角度看這是一個應用,但是從建模和設計的觀點來看,它可以由有獨立實現的獨立模型來完成。我們應該先看看需求,然後瞭解一下它們是否可以被分割成兩個或者多個不太相同的部分。如果可以這樣做,那麼我們就創建獨立的界定上下文(Bounded Context),並獨立建模。這樣做的好處是有選擇實現技術的自由。我們正創建的應用可能會共享一個通用的瘦 GUI,作爲鏈接和按鈕的一個門戶來訪問每一個程序。相對於集成後端的模型,組織應用是一個較小的集成。在繼續談論獨立方法之前,我們需要明確的是我們不會回到集成系統。獨立開發的模型是很難集成的,它們的相通之處很少,不值得這樣做。

開放主機服務

當我們試圖集成兩個子系統時,通常要在它們之間創建一個轉換層。這個層在客戶端子系統和我們想要集成的外部子系統之間扮演緩衝的角色。這個層可以是個永久層,這要看關係的複雜度和外部子系統是如何設計的。如果外部子系統不是被一個客戶端子系統使用,而是被多個服務端子系統使用的話,我們就需要爲所有的服務端子系統創建轉換層。所有的這些層會重複相同的轉換任務,也會包含類似的代碼。當一個子系統要和其他許多子系統集成時,爲每一個子系統定製一個轉換器會使整個團隊陷入困境。會有越來越多的代碼需要維護,當需要做出改變時,也會越來越擔心。

解決這一個問題的方法是,將外部子系統看作服務提供者。如果我們能爲這個系統創建許多服務,那麼所有的其他子系統就會訪問這些服務,我們也就不需要任何轉換層。問題是每一個子系統也許需要以某種特殊的方式和外部子系統交互,那麼要創建這些相關的服務可能會比較麻煩。

定義一個能以服務的形式訪問你子系統的協議。開放它,使得所有需要和你集成的人都能獲取到。然後優化和擴展這個協議,使其可以處理新的集成需求,但某團隊有特殊需求時除外。對於特殊的需求,使用一個一次性的轉換器增加協議,從而使得共享的協議保持簡潔和精幹。

參考

《領域驅動設計精簡版》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章