模型驅動設計的構造塊(上)——DDD

Ruby_Lu發表於2022-12-15

  為了保證軟體實踐得簡潔並且與模型保持一致,不管實際情況如何複雜,必須運用建模和設計的實踐。

  某些設計決策能夠使模型和程式緊密結合在一起,互相促進對方的效用。這種結合要求我們注意每個元素的細節,對細節問題的精雕細琢能夠打造一個穩定的平臺。

  本部分主要將一些模式,說明細微的模型差別和設計決策如何影響領域驅動設計的過程。

  下面的簡圖是一張導航圖,它描述的是本部分所要講解的模式以及這些模式彼此關聯的方式。

  

 

 

  共用這些標準模式可以使設計有序進行,也使專案組成員能夠更方便地瞭解彼此的工作內容。同時,使用標準模式也使 Ubiquitous Language 更加豐富,所有的專案組成員都可以使用 Ubiquitous Language  來討論模型和設計決策。

  開發一個好的領域模型是一門藝術。而模型中各個元素的實際設計和實現則相對系統化。將領域設計和軟體系統中的其他關注點分離會使設計與模型之間的關係非常清晰。根據不同的特徵來定義模型元素則會使元素的意義更加鮮明。對每個元素使用已驗證的模式有助於建立更易於實現的模型。

 

1. 分離領域

  在軟體中,雖然專門用於解決領域問題的那部分通常只佔整個軟體系統的很小一部分,但其卻出乎意料的重要。要想實現 Model- Driver-Design(模型驅動設計)的想法,我們需要著眼於模型中的元素並且將它們視為一個系統。絕不能被迫從一大堆混雜的物件中將領域物件挑選出來。

  我們需要將領域物件與系統中的其他功能分離,這樣就能夠避免將領域概念和其他只與軟體技術相關的概念搞混了,也不會在紛繁複雜的系統中完全迷失了領域。

 

  1.1 模式:Layered Architecture(分層架構)

  

 

   在物件導向的程式中,常常會在業務物件中直接寫入使用者介面、資料庫訪問等支援程式碼。而一些業務邏輯則會被嵌入到使用者介面元件和資料庫指令碼中。這麼做就是為了以最簡單的方式在短期內完成開發工作。

  如果與領域有關的程式碼分散在大量的其他程式碼之中,那麼檢視和分析領域程式碼就會變得異常困難。對使用者介面的簡單修改實際上很可能會改變業務邏輯,而想要調整業務規則也很可能需要對使用者介面程式碼、資料庫操作程式碼或者其他的程式元素進行仔細的篩查。這樣就不太可能實現一致的、模型驅動的物件了,同時也會給自動化測試帶來困難。考慮到程式中各個活動所涉及的大量邏輯和技術,程式本身必須簡單明瞭,否則就會讓人無法理解。

 

  要想建立出能夠處理複雜任務的程式,需要做到關注點分離——使設計中的每個部分都能得到單獨的關注。在分離的同時,也需要維持系統內部複雜的互動關係。

  軟體系統有各種各樣的劃分方式,但是根據軟體行業的經驗和慣例,普遍採用 Layered Architecture(分層架構),特別是有幾個層已成了標準層。Layered Architecture 的基本原則是層中任何元素都依賴於本層的其他元素或其下層的元素。向上的通訊必須透過間接的方式進行,這些將在後面討論。

  分層的價值在於每一層都只代表程式中的某一特定方面。這種限制使每個方面的設計都更具內聚性,更容易理解。當然,要分離出內聚設計中最重要的方面,選擇恰當的分層方式是至關重要的。大多數架構使用的都是下面這4個概念層的某種變體。

  

 

   有些專案沒有明顯劃分出使用者介面層和應用層,而有些專案則有很多個基礎設施層。但是將領域層分離出來才是實現 Model- Driver-Design 的關鍵。

  因此:

  給複雜的應用程式劃分層次。在每一層分別進行設計,使其具有內聚性並且只依賴於它的下層。採用標準的架構模式,只與上層進行鬆散的耦合。將所有與領域模型相關的程式碼放在一個層中,並把它與使用者介面、應用層以及基礎設施層的程式碼分開。領域物件應該將重點放在如何表達領域模型上,而不需要考慮自己的顯示和儲存的問題,也無需管理應用任務等內容。這使得模型的含義足夠豐富,結構清晰,可以捕捉到基本的業務知識、並有效的使用這些知識。

  將領域層與基礎設施層以及使用者介面層分離,可以使每層的設計更加清晰。彼此獨立的層更容易維護,因為它們往往以不同的速度發展並且滿足不同的需求。層與層的分離也有助於在分散式系統中部署程式,不同的層可以靈活地放在不同伺服器或者客戶端中,這樣可以減少通訊開銷,並最佳化程式效能。

 

  1.1.1 將各層關聯起來

  各層之間需要互相連線,在連線各層的同時不影響分離帶來的好處,這是很多模式的目的所在。

  各層之間是鬆散連線的,層與層的依賴關係只能是單向的。上層可以直接使用或操作下層元素,方法是透過呼叫下層元素的公共介面,保持對下層元素的引用(至少是暫時的),以及採用常規的互動手段。而如果下層元素需要與上層元素進行通訊(不只是回應直接查詢),則需要採用另一種通訊機制,使用架構模式來連線上下層,如回撥模式或者 Observes模式(觀察者模式)。

  還有許多其他連線使用者介面和應用層的方式。對我們而言,只要連線方式能夠維持領域層的獨立性,保證在設計領域物件時不需要同時考慮可能與其互動的使用者介面,那麼這些連線方式都是可用的。

  通常,基礎設施層不會發起領域層中的操作,它處於領域層之下,不包含其所服務的領域中的知識。事實上這種技術能力最常以 Service 的形式提供。

  應用層和領域層可以呼叫基礎設施層所提供的 Service。然而,並不是所有的基礎設施都以可供上層呼叫的 Service 的形式出現的。有些技術元件被設計成直接支援其他層的基本功能(如為所有的領域物件提供抽象基類),並且提供關聯機制(如 MVC 及類似框架的實現)。這種 “架構框架” 對於程式其他部分的設計有著更大的影響。

 

  1.1.2 架構框架

  如果基礎設施透過介面呼叫 Service 的形式來實現,那麼如何分層以及如何保持層與層之間的鬆散連線就相當顯而易見了。但是有些技術問題要求更具有侵入性的基礎設施。整合了大量基礎設施需求的框架通常會要求其他層以某種特定的方式實現,如以框架類的子類形式或者帶有結構化的方法簽名。(子類在父類的上層似乎是違反常理的,但是要記住哪個類反映了另一個類的更多知識。)最好的架構框架既能解決複雜技術問題,也能讓領域開發人員集中精力去表達模型,而不考慮其他問題。然而使用框架很容易為專案製造障礙:要麼設定了太多的假設,減小了領域設計的可選範圍;要麼是需要實現太多的東西,影響開發進度。

  專案中一般都需要某種形式的架構框架(儘管有時候專案團隊選擇了不太合適的框架)。當使用框架時,專案團隊應該明確其使用目的:建立一種可以表達領域模型的實現並且用它來解決重要問題。專案團隊必須想方設法讓框架滿足這些需求,即使這意味著拋棄框架中的一些功能。

  不妄求萬全之策,只要有選擇性地運用框架來解決難點問題,就可以避開框架的很多不足之處。明智而謹慎地選擇框架中最具有價值的功能能夠減少程式實現和框架之間的耦合,使隨後的設計決策更加靈活。更重要的是,現在許多框架的用法都極其複雜,這種簡化方式有助於保持業務物件的可讀性,使其更富有表達力。

 

  1.2 領域層是模型的精髓

  現在,大部分軟體系統都採用了 Layered Architecture ,只是採用的分層方案存在不同而已。許多型別的開發工作都能從分層中受益。然而,領域驅動設計只需要一個特定的層存在即可。

  領域模型是一系列概念的集合。“領域層” 則是領域模型以及所有與其直接相關的設計元素的表現,它由業務邏輯的設計和實現組成。在 Model- Driven Design 中,領域層的軟體構造反應出了模型概念。

  如果領域邏輯與程式中的其他關注點混在一起,就不可能實現這種一致性。將領域實現獨立出來是領域驅動設計的前提。

 

  1.3 模式:The Smart UI “反模式”

  如果一個經驗並不豐富的專案團隊要完成一個簡單的專案,卻決定使用 Model-Driven Design 以及 Layered Architecture ,那麼這個專案組將經歷一個艱難的學習過程。團隊成員不得不去掌握複雜的技術,艱難地學習物件建模。對基礎設施和各層的管理工作使得原本簡單的任務卻要花費很長時間來完成。簡單專案的開發週期短,期望值也不是很高。所以,早在專案團隊完成任務之前,該專案就會被取消,更談不上去論證有關這種方法的許多種令人激動的可行性了。

  即使專案有更充裕的時間,如果沒有專家的幫助,團隊成員也不太可能掌握這些技術。最後,加入他們確實能夠克服這些困難,恐怕也只會開發出一套簡單的系統。因為這個專案本來就不需要豐富的功能。

  

  因此,當情況需要時:

  在使用者介面中實現所有的業務邏輯。將應用程式分成小的功能模組,分別將它們實現成使用者介面,並在其中嵌入業務規則。用關聯式資料庫作為共享的資料儲存庫。使用自動化程度最高的使用者介面建立工具和可用的視覺化程式設計工具。

  但,在領域驅動設計中,將 The Smart UI 看作是 “反模式” 。然而在其他情況下,它也是完全可行的。

  如果專案團隊有意識地應用這個模式,那麼就可以避免其他方法所需要的大量開銷。專案團隊常犯的錯誤是採用一種複雜的設計方法,卻無法保證專案從頭到尾始終使用它。另一種常見的也是代價高昂的錯誤則是為專案構建一種複雜的基礎設施以及使用工業級的工具,而這樣的專案根本不需要它們。

 

  這裡討論 Smart UI 只是為了讓我們認清為什麼以及何時需要採用諸如 Layered Architecture 這樣的模式來分離出領域層。

  如果一個架構能夠把那些與領域相關的程式碼隔離出來,得到一個內聚的領域設計,同時又使領域設計與系統其他部分保持鬆散耦合,那麼這種架構也許可以支援領域驅動設計。

 

  1.4 其他分離方式

  除了基礎設施和使用者介面之外,還有一些其他的元素也會破壞精心設計的領域模型。必須要考慮那些沒有完全整合到模型中的領域元素。不得不與同一領域中使用不同模型的其他開發團隊合作。還有其他的因素會讓你的模型結構不再清晰,並且影響模型的使用效率。後面會討論這方面的問題,同時會介紹其他模式。非常複雜的領域模型本身是難以使用的,後面會說明如何在領域層內進行進一步分割槽,以便從次要細節中凸顯出領域的核心概念。

  接下來討論一些具體細節,即如何讓一個有效的領域模型和一個富有表達力的實現同時演進。畢竟,把領域隔離出來的最大好處就是可以真正專注於領域設計,而不用考慮其他方面。

 

2. 軟體中所表示的模型

  要想在不削弱模型驅動設計能力的前提下對實現做出一些這種,需要重新組織基本元素並理解它們。我們需要將模型與實現的各個細節一一聯絡起來。以下主要討論這些基本模型元素。

  下文的討論從如何設計和簡化關聯開始。物件之間的關聯很容易想出來,也很容易畫出來,但實現它們卻存在很多潛在的麻煩。關聯也表明了具體的實現決策在 Model- Driven- Design 中的重要性。

  本文的討論將側重於模型本身,但仍繼續考察具體模型選擇與實現問題之間的關係,將著重區分用於用於表示模型的3種模型元素模式: Entity、Value Object 和 Service。

  從表面上看,定義那些用來捕獲領域概念的物件很容易,單要想反映其含義卻很困難。這要求我們明確區分各種模型元素的含義,並與一系列設計實踐結合起來,從而開發出特定型別的物件。

  一個物件是用來表示某種具有連續性和標識的事物的呢(可以跟蹤它所經歷的不同狀態,甚至可以跨不同的實現跟蹤它),還是用於描述某種狀態的屬性呢?這是 Entity 和 Value Object 之間的根本區別。明確地選擇這兩種模式中的一個來定義物件,有利於減少分歧,並幫助我們做出特定的選擇,這樣才能得到健壯的設計。

  

  領域中還有一些方面適合用動作或操作來表示,這比用物件表示更加清楚。這些方面最好用 Service 來表示,而不應把操作的責任強加到 Entity 或 Value Object 上,儘管這樣做稍微違背了物件導向的建模傳統。Service 是應客戶端請求來完成某事。在軟體的技術層中有很多Service。在領域中也可以使用 Service ,當對軟體要做的某項無狀態的活動進行建模時,就可以將該活動作為一項 Service

   最後,Module 的討論將有助於理解這樣一個要點——每個設計決策都應該是在深入理解領域中的某些深層知識之後做出的。高內聚、低耦合這種思想(通常被認為是一種技術指標)可應用於概念本身。在 Model- Driven Design 中,Module 是模型的一部分,它們應該反映領域中的概念。

 

  2.1 關聯

  物件之間的關聯使得建模與實現之間的互動更為複雜。

  模型中每個可遍歷的關聯,軟體中都要有相同屬性的機制。

 

  一個顯示了顧客與銷售代表之間關聯的模型有兩個含義。一方面,它把開發人員所認為的兩個真實的人之間的關係抽象出來。另一方面,它相當於兩個 Java 物件之間的物件指標,或者相當於資料庫查詢(或類似實現)的一種封裝。

  例如,一對多關聯可以用一個集合型別的例項變數來實現。但設計無需如此直接。可能沒有集合,這時可以使用一個訪問方法(accessor method)來查詢資料庫,找到相應的記錄,並用這些記錄來例項化物件。這兩種設計方法反映了同一個模型。設計必須制定一種具體的遍歷機制,這種遍歷的行為應該與模型中的關聯一致。

 

  現實生活中有大量 “多對多” 關聯,其中有很多關聯天生就是雙向的。我們在模型開發的早期進行頭腦風暴活動並探索領域時,也會得到很多這樣的關聯。但這些普遍的關聯會使實現和維護變得很複雜。此外,它們也很少能表示出關係的本質。

  至少有三種方法可以使得關聯更易於控制。

  (1)規定一個遍歷方向。

  (2)新增一個限定符,以便有效地減少多重關聯。

  (3)消除不必要的關聯。

  儘可能地對關係進行約束是非常重要的。雙向關聯意味著只有將這兩個物件放在一起考慮才能理解它們。當應用程式不要求雙向遍歷時,可以指定一個遍歷方向,以便減少互相依賴,並簡化設計。理解了領域之後就可以自然地確定一個方向。

 

  限定多對多關聯的遍歷方向可以有效地將其實現簡化為一對多關聯,從而得到一個簡單的多的設計。

  堅持將關聯限定為領域所傾向的方向,不僅可以提高這些關聯的表達力並簡化其實現,而且還可以突出剩下的雙向關聯的重要性。當雙向關聯是領域的一個語義特徵時,或者當應用程式的功能要求雙向關聯時,就需要保留它,以便表達出這些需求。

  當然,最終的簡化是清除那些對當前工作或模型物件的基本含義來說不重要的關聯。

 

  從仔細地簡化和約束模型的關聯到 Model-Driven Design ,還有一段漫長的探索過程。現在我們轉向物件導向本身。仔細區分物件可以使得模型更加清晰,並得到更實用的實現。

 

  2.2 模式:Entity(又稱為 Reference Object)

  很多物件不是透過它們的屬性定義的,而是透過連續性和標識定義的。我們一般認為,一個人有一個標識,這個標識會陪伴他走完一生()甚至死後。這個人的物理屬性會發生變化,最後消失。他的名字可能改變,財務關係也會發生變化,沒有哪個屬性是一生不變的,但標識卻是永久的。我跟我5歲時是同一個人嗎?稍微變化一下問題的角度:應用程式的使用者是否關心現在的5和5歲時的我是不是同一個人?

 

  在物件的多個市縣、儲存形式和真實世界的參與者之間,概念性標識必須是匹配的。屬性可以不匹配,例如,銷售代表可能已經在聯絡軟體中更新了地址,而這個更新正在傳送給到期應收賬款軟體。兩個客戶可能同名。在分散式軟體中,多個使用者可能從不同地點輸入資料,這需要在不同的資料庫中非同步地協調這些更新事物,使它們傳播到整個系統。

 

  物件建模有可能把我們的注意力引到物件的屬性上,但實體的基本概念是一種貫穿整個生命週期的抽象的連續性。

  一些物件主要不是由它們的屬性定義的。它們實際上表示了一條 “標識線” (A Thread of Identity),這條線跨越時間,而且常常經歷多種不同的表示。有時,這樣的物件必須與另一個具有不同屬性的物件相匹配。而有時一個物件必須與具有相同屬性的另一個物件區分開。錯誤的標識可能會破壞資料。

   主要由標識定義的物件被稱作 Entity 。Entity(實體)有特殊的建模和設計思路。它們具有生命週期,這期間它們的形式和內容可能發生根本改變,但必須保持一種內在的連續性。為了有效地跟蹤這些物件,必須定義它們的標識。它們的類定義、職責、屬性和關聯必須由其標識來決定,而不是依賴其所具有的屬性。即使對於那些不發生根本變化或者生命週期不太複雜的 Entity ,也應該在語義上把它們作為 Entity 來對待,這樣可以得到更清晰的模型和更健壯的實現。

  當然,軟體系統中的大多數 “Entity” 並不是人,也不是其通常意義上所指的 “實體” 或者 “存在”。Entity 可以是任何事物,只要滿足兩個條件即可。一是它在生命週期中具有連續性,二是它的區別並不是由那些對使用者非常重要的屬性決定的。Entity 可以是一個人、一座城市、一輛汽車或一次交易。

  另一方面,在一個模型中,並不是所有物件都是具有有意義標識的Entity。但是,由於面嚮物件語言存在每個物件都構建了一些與 “標識” 有關的操作(如Java中的 “==” 運算子),這些操作透過比較兩個引用在記憶體中的位置(或透過其他機制)來確定這兩個引用是否指向同一個物件。但這種標識機制在其他應用領域中卻沒什麼意義。標識是Entity的一個微妙的、有意義的屬性,我們是不能把它交給語言的自動特性來處理的。

  標識的重要性並不僅僅體現在特定的軟體系統中,在軟體系統之外它通常也是非常重要的,如銀行交易。但有時標識只有在系統上下文中才重要,如一個計算機程式的標識。

  因此:

  當一個物件由其標識(而不是屬性)區分時,那麼在模型中應該主要透過標識來確定該物件的定義。使類定義變得簡單,並集中關注生命週期的連續性和標識。定義一種區分每個物件的方式,這種方式應該與其形式和歷史無關。要格外注意那些需要透過屬性來匹配物件的需求。在定義標識操作時,要確保這種操作為每個物件生成唯一的結果,這可以透過附加一個保證唯一性的符號來實現。這種定義標識的方法可能來自外部,也可能是由系統建立的任意識別符號,但它在模型中必須是唯一的標識。模型必須定義出 “符合什麼條件才算是相同的事物”。

  在現實世界中,並不是每個事物都必須有一個標識,標識重不重要,完全取決於它是否有用。實際上,現實世界中的同一個事物在領域模型中可能需要表示為 Entity ,也可能不需要表示為 Entity。

 

  2.2.1 Entity 建模

  當對一個物件進行建模時,我們自然而然會考慮它的屬性,而且考慮它的行為也顯得非常重要。但 Entity 最基本的職責是確保連續性,以便使其行為更清楚且可預測。保持實體的簡練是實現這一責任的關鍵。不要將注意力集中在屬性或行為上,應該擺脫這些細枝末節,抓住 Entity 物件定義的最基本特徵,尤其是那些用於識別、查詢或匹配物件的特徵。只新增那些對概念至關重要的行為和這些行為所必需的屬性。此外,應該將行為和和屬性轉移到與核心實體關聯的其他物件中。這些物件中,有些可能是 Entity,有些可能是 Value Object。除了標識問題之外,實體往往透過協調其關聯物件的操作來完成自己的職責。

 

  2.2.2 設計標識操作

  每個 Entity 都必須有一種建立標識的操作方式,以便與其他物件區分開,即使這些物件與它具有相同的描述屬性。不管系統是如何定義的,都必須確保標識屬性在系統中是唯一的,即使是在分散式系統中,或者物件已被歸檔,也必須確保標識的唯一性。

  有時,某些資料屬性或屬性組合可以它們在系統中具有唯一性,或者在這些屬性上加一些簡單約束可以使其具有唯一性。這種方法為 Entity 提供了唯一鍵。

  當物件屬性沒辦法形成真正唯一鍵時,另一種經常用到的解決方案是為每個例項附加一個在類中唯一的符號(如一個數字或字串)。一旦這個ID符號被建立並儲存為 Entity 的一個屬性,必須將它指定為不可變的。它必須永遠不變,即使開發系統無法直接強制這條規則。例如,當物件被扁平化到資料庫中或從資料庫中重新建立時,ID 屬性應該保持不變。有時可以利用技術框架來實現此目的,但如果沒有這樣的框架,就需要透過工程紀律來約束。

   當自動生成ID時,使用者可能永遠不需要看到它。ID可能只是在內部需要,例如,在一個可以按人名查詢記錄的聯絡人管理應用程式中。這個程式需要用一種簡單、明確的方式來區分兩個同名聯絡人,這就可以可以透過唯一的內部ID來實現。在檢索出兩個不同的條目之後,系統將顯示這兩個不同的聯絡人,但可能不會顯示ID。使用者可以透過這兩個人的公司、地點等屬性來區分他們。

  在有些情況下使用者會對生成的ID感興趣。當我委託一個包裹運送服務寄包裹時,我會得到一個跟蹤號,它是由運送公司的軟體生成的,我可以用這個號碼來識別和跟蹤我的包裹。當我預訂機票或酒店時,會得到一個確認號碼,它是預訂交易的唯一識別符號。

  在某些情況下,需要確保ID在多個計算機系統之間具有唯一性。例如,如果需要在兩傢俱有不同計算機系統的醫院之間交換醫療記錄,那麼理想情況下對同一病人應該使用同一個ID,但如果這兩個系統各自生成自己的ID,這就很難實現。這樣的系統通常使用由另外一家機構發放的識別符號。

 

  2.3 模式:Value Object

  很多物件沒有概念上的標識,它們描述了一個事務的某種特徵。

  跟蹤 Entity 的標識是非常重要的,但為其他物件也加上標識會影響系統效能並增加分析工作,而且會使模型變得混亂,因為所有物件看起來都是相同的。

  軟體設計要時刻與複雜性做鬥爭。我們必須區別對待問題,僅在真正需要的地方進行特殊處理。

  然而,如果僅僅把這類物件當作沒有標識的物件,那麼就忽略了它們的工具價值或術語價值。事實上,這些物件有其自己的特徵,對模型也有著自己的重要意義。這些是用來描述事物的物件。

  用於描述領域的某個方面而本身沒有概念標識的物件稱為 Value Object(值物件)。Value Object 被例項化之後用來表示一些設計元素,對於這些設計元素,我們只關心它們是什麼,而不關心他們是誰。

 

  Value Object 甚至可以引用 Entity。例如,如果我請線上地圖服務為我提供一個從舊金山到洛杉磯的駕車風景路線,它可能會得出一個 “路線” 物件,此物件透過太平洋海岸公路連線舊金山和洛杉磯。這個 “路線” 物件是一個Value,儘管它所引用的三個物件(兩座城市和一條公路)都是 Entity。

  Value Object 經常作為引數在物件之間傳遞訊息。它們常常是臨時物件,在一次操作中被建立,然後丟棄。Value Object 可以用作 Entity(以及其他 Value)的屬性。我們可以把一個人建模為一個具有標識的 Entity,但這個人的名字是一個 Value。

  當我們只關心一個模型元素的屬性時,應把它歸類為 Value Object。我們應該使這個模型元素能夠表示出其屬性的意義,併為它提供相關功能。Value Object 應該是不可變的。不要為它分配任何標識,而且不要把它設計成像 Entity 那麼複雜。

  Value Object 所包含的屬性應該形成一個概念整體。例如,street、city不應是Person 物件的單獨的屬性。它們是整個地址的一部分,這樣可以使得 Person 物件更簡單,並使地址成為一個人更一致的 Value Object。

 

  2.3.1 設計 Value Object

  我們並不關心使用的是 Value Object 的哪個例項。由於不受這方面的約束,設計可以獲得更大的自由,因此可以簡化設計或最佳化效能。在設計 Value Object 時有多種選擇,包括複製、共享或保持 Value Object 不變。

  

  兩個人同名並不意味著他們是同一個人,也不意味著他們是可以互換的。但表示名字的物件是可以互換的,因為他們只涉及名字的拼寫。一個 Name 物件可以從第一個 Person 物件複製給第二個 Person 物件。

  事實上,這兩個Person物件可能不需要自己的名字例項,它們可以共享同一個 Name 物件(其中每個 Person 物件都有一個指向同一個名字例項的指標),而無需改變它們的行為或標識。如此一來,當修改其中一個人名字時就會產生問題,這時另一個人的名字也將改變!為了防止這種錯誤發生,以便安全地共享一個物件,必須確保 Name 物件是不變的——它不能改變,除非將其整個替換掉。或者傳遞一個副本來解決。

 

  Value Object 為效能最佳化提供了更多選擇,這一點可能很重要,因為 Value Object 往往為數眾多。在大型系統中,這種效果可能會被放大數千倍,而且這樣的最佳化可能決定一個系統是可用的,還是由於數百萬個多餘物件而變得異常緩慢。這只是無法應用於 Entity 的最佳化技巧中的一個。

  複製和共享哪個更划算取決於實現環境。雖然複製有可能導致系統被大量的物件阻塞,但共享可能會減慢分散式系統的速度。當兩個機器之間傳遞一個副本時,只需傳送一條訊息,而且副本達到接收端後是獨立存在的。但如果共享一個例項,那麼只會傳遞一個引用,這要求每次互動都要向傳送方返回一條訊息。

  以下幾種情況最好使用共享,這樣可以發揮共享的最大價值並最大限度地減少麻煩:

    節省資料庫空間或減少物件數量是一個關鍵要求時;

    通訊開銷很低時(如在中央伺服器中);

    共享的物件被嚴格限定為不可變時。

 

  在有些語言和環境中,可以將屬性或物件宣告為不可變的,但有些卻不具備這種能力。這種宣告能夠體現出設計決策,但它們並不是十分重要。我們在模型中所做的很多區別都無法用當前工具和程式語言在實現中顯式地宣告出來。例如,我們無法宣告 Entity 並自動確保其具有一個標識操作。但是,程式語言沒有直接支援這些概念的區別並不說明這些區別沒有用處。這只是說明我們需要更多的約束機制來確保一些重要的規則(這些規則只有在實現中才是隱式的)。命名規則、精心準備的文件和大量討論都可以強化這些需求。

  只要Value Object 是不可變的,變更管理就會很簡單,因為除了整體替換之外沒有其他的更改。不變的物件可以自由共享。如果垃圾回收時可靠的,那麼刪除操作就只是將所有指向物件的引用刪除。當在設計中將一個Value Object指定為不可變時,開發人員就可以完全根據技術需求來決定是使用複製,還是使用共享,因為他們沒有後顧之憂——應用程式不依賴於物件的特殊例項。

 

  在有些情況下出於效能考慮,仍需要讓Value Object 是可變的。這包括以下因素:

    如果 Value 頻繁改變;

    如果建立或刪除物件的開銷很大;

    如果替換(而不是修改)將打亂叢集;

    如果 Value 的共享不多,或者共享不會提高叢集效能,或其他某種技術原因。

 

  2.3.2 設計包含 Value Object 的關聯

  前面說的與關聯有關的大部分內容也適用於 Entity 和 Value Object 。模型中的關聯越少越好,越簡單越好。

  但是,兩個 Value Object 之間的雙向關聯則完全沒有意義。當一個 Value Object 指向另一個 Value Object 時,由於沒有標識,說一個物件指向的物件正是那個指向它的物件並沒有任何意義的。我們充其量只能說,一個物件指向的物件與那個指向它的物件是等同的,但這可能要求我們必須在某個地方實施這個固定規則。而且,儘管我們可以這樣做,並設定雙向指標,但很難想出這種安排有什麼用處。因為,我們應儘量完全清楚Value Object 之間的雙向關聯。如果在你的模型中看起來確實需要這種關聯,那麼首先應重新考慮一下將物件宣告為 Value Object 這個決定是否正確。或許它擁有一個標識,而你還沒有注意到它。

  

  2.4 模式:Service

  有時,物件不是事物。

  在某些情況下,最清楚、最實用的設計會包含一些特殊的操作,這些操作從概念上講不屬於任何物件。與其把它們強制地歸於哪一類,不如順其自然地在模型中引入一種新的元素,這就是 Service(服務)。

 

  有些重要的領域操作無法放到 Entity 或 Value Object 中。這當中有些操作從本質上講是一些活動或動作,而不是事物,但由於我們的建模正規化是物件,因此要想辦法講它們劃歸到物件這個範疇裡。

  現在,一個比較常見的錯誤是沒有努力為這類行為找到一個適當的物件,而是逐漸轉為過程化的程式設計。但是,當我們勉強將一個操作放到不符合物件定義的物件中時,這個物件就會產生概念上的混淆,而且會變得很難理解或重構。複雜的操作很容易把一個簡單物件搞亂,使物件的角色變得模糊。此外,由於這些操作常常會牽扯到很多領域物件——需要協調這些物件以便使它們工作,而這會產生對所有這些物件的依賴,將那些本來可以單獨理解的概念摻雜在一起。

  有時,一些 Service 看上去就像是模型物件,他們以物件的形式出現,但除了執行一些操作之外並沒有其他意義。這些 “實幹家” 的名字通常以 “Manager” 之類的名字結尾。它們沒有自己的狀態,而且除了所承載的操作之外在領域中也沒有其他意義。儘管如此,該方法至少為這些特立獨行的行為找到一個容身之所,避免它們擾亂真正的模型物件。

 

  一些領域概念不適合被建模為物件。如果勉強把這些重要的領域功能歸為 Entity 或 Value Object 的職責,那麼不是歪曲了基於模型的物件的定義,就是人為地增加了一些無意義的物件。

  Service 是作為介面提供的一種操作,它在模型中是獨立的,它不像 Entity 和 Value Object 那樣具有封裝的狀態。Service 是技術框架的一種常見模式,但它們也可以在領域層中使用。

  所謂 Service,它強調的是與其他物件的關係。與 Entity 和 Value Object 不同,它只是定義了能夠為客戶做什麼。Service 往往是以一個活動來命名,而不是以一個 Entity 來命名,也就是說,它是動詞而不是名詞。Service 可以有抽象而有意義的定義,只是它使用了一種與物件不同的定義風格。Service 也應該有定義的職責,而且這種職責以及履行它的介面也應該作為領域模型的一部分來加以定義。操作名稱應來自於 Ubiquitous Language ,如果 Ubiquitous Language 中沒有這個名稱,則應該將其引入到 Ubiquitous Language 中。引數和結果應該是領域物件。

  

  使用 Service 時應謹慎,它們不應該替代 Entity 和 Value Object 的所有行為。但是,當一個操作實際上是一個重要的領域概念時,Service 很自然就會成為 Model- Driven Design 中的一部分。將模型中的獨立操作宣告為一個 Service,而不是宣告為一個不代表任何事情的虛擬物件,可以避免對任何人產生誤導。

  好的 Service 有以下3個特徵:

    (1)與領域概念相關的操作不是 Entity 或 Value Object 的一個自然組成部分。

    (2)介面是根據領域模型的其他元素定義的。

    (3)操作是無狀態的。

  這裡所說的無狀態是指任何客戶都可以使用某個 Service 的任何例項,而不必關心該例項的歷史狀態。Service 執行時將使用可全域性訪問的資訊,甚至會更改這些全域性資訊(也就是說,它可能具有副作用)。但 Service 不保持影響其自身行為的狀態,這一點與大多數領域物件不同。

  當領域中的某個重要的過程或轉換操作不是 Entity 或 Value Object 的自然職責時,應該在模型中新增一個作為獨立介面的操作,並將其宣告為 Service 。定義介面時要使用模型語言並確保操作名稱是 Ubiquitous Language 中的術語。此外,應該使 Service 成為無狀態的。

 

  2.4.1 Service 與孤立的領域層

  這種模式只重視那些在領域中具有重要意義的 Service,但 Service 並不只是在領域層中使用。我們需要注意區分屬於領域層的 Service 和那些屬於其他層的 Service ,並劃分責任,以便將它們明確地區分開。

  文獻中所討論的大多數 Service 是純技術的 Service,它們都屬於基礎設施層。領域層和應用層的 Service 與這些基礎設施層 Service 進行協作。例如,銀行可能有一個用於向客戶傳送電子郵件的應用程式,當客戶的賬戶餘額小於一個特定的臨界值時,這個程式就向客戶傳送一封電子郵件。封裝了電子郵件系統的介面(也可能是其他的通知方式)就是基礎設施層中的 Service。

  應用層 Service 和領域層 Service 可能很難區分。應用層負責通知的設定,而領域層負責確定是否滿足臨界值,儘管這項任務可能並不需要使用 Service,因為它可以作為 “account”(賬戶)物件的職責中。這個銀行應用程式可能還負責資金轉賬。如果設計一個 Service 來處理資金轉賬相應的借方和貸方,那麼這項功能將屬於領域層。資金轉賬在銀行領域語言中是一項有意義的操作,而且它涉及基本的業務邏輯。而純技術的 Service 應該沒有任何業務意義。

 

  很多領域或應用層 Service 是在 Entity 和 Value Object 的基礎上建立起來的,它們的行為類似於將領域的一些潛在功能組織起來以執行某種任務的指令碼。Entity 和 Value Object 往往由於粒度過細而無法提供對領域層功能的便捷訪問。我們在這裡會遇到領域層和應用層之間很微妙的分界線。例如,如果銀行應用程式可以把我們的交易進行轉換併到處到一個電子表格檔案中,以便進行分析,那麼這個匯出操作就是應用層 Service 。“檔案格式” 在銀行領域中是沒有意義的,它也不涉及業務規則。

  另一方面,賬戶之間的轉賬功能屬於領域層 Service ,因為它包含重要的業務規則(如處理相應的借方賬戶和貸方賬戶),而且“資金轉賬”是一個有意義的銀行術語。在這種情況下,Service 自己並不會做太多的事情,而只是要求兩個 Account 物件完成大部分工作。但如果將 “轉賬” 操作強加在 Account 物件上會很彆扭,因為這個操作涉及兩個賬戶和一些全域性規則。

  我們可能喜歡建立一個 Funds Transfer(資金轉賬)物件來表示兩個賬戶,外加一些與轉賬有關的規則和歷史記錄。但在銀行間的網路中進行轉賬時,仍然需要使用 Service 。此外,在大多數開發系統中,在一個領域物件和外部資源之間建立一個介面是很彆扭的。我們可以利用一個 Facade(外觀)將這樣的外部 Service 包裝起來,這個外觀可能以模型作為輸入,並返回一個 “Funds Transfer” 物件(作為它的結果)。但無論中間涉及什麼Service,甚至那些超出我們掌控範圍的 Service ,這些 Service 都是在履行資金轉賬的領域職責。

  

 

 

  2.4.2 粒度

  上述對 Service 的討論強調的是將一個概念建模為 Service 的表現力,但 Service 還有其他有用的功能,它可以控制領域層中的介面的粒度,並且避免客戶端與 Entity 和 Value Object 的耦合。

  在大型系統中,中等粒度的、無狀態的 Service 更容易被複用,因為它們在簡單的介面背後封裝了重要功能。此外,細粒度的物件可能導致分散式系統的訊息傳遞的效率低下。

  如前所述,由於應用層負責對領域物件的行為進行協調,因此細粒度的領域物件可能會把領域層的知識洩漏到應用層中,這產生的結果是應用層不得不處理複雜的、細緻的互動,從而使得領域知識蔓延到應用層或使用者介面程式碼當中,而領域層會丟失這些知識。明智地引入領域層服務有助於在應用層和領域層之間保持一條明確的界限。

  這種模式有利於保持介面的簡單性,便於客戶端控制並提供了多樣化的功能。它提供了一種在大型或分散式系統中便於對組建進行打包的中等粒度的功能。而且,有時 Service 是表示領域概念的最自然的方式。

 

  2.4.3 對 Service 的訪問

  像 J2EE 和 CORBA 這樣的分散式系統架構提供了特殊的 Service 釋出機制,這些釋出機制具有一些使用上的慣例,並且增加了釋出和訪問功能。但是,並非所有專案都會使用這樣的框架,即使在使用了它們的時候,如果只是為了在邏輯上實現關注點的分離,那麼它們也是大材小用了。

  與分離特定職責的設計決策相比,提供對 Service 的訪問機制的意義並不是十分重大。一個 “操作” 物件可能足以作為 Service 介面的實現。我們很容易編寫一個簡單的 Singleton 物件來實現對 Service 的訪問。從編碼慣例可以明顯看出,這些物件只是 Service 介面的提供機制,而不是有意義的領域物件。只有當真正需要實現分散式系統或充分利用框架功能的情況下才應該使用複雜的架構。

 

  2.5 模式:Module(也稱為 Package)

   Module 是一個傳統的、較成熟的設計元素。雖然使用模組有一些技術上的原因,但主要原因是 “認知超載”。Module 為人們提供了兩種觀察模型的方式,一是可以在 Module 中檢視細節,而不會被整個模型淹沒,二是觀察 Module 之間的關係,而不考慮其內部細節。

  Module 從更大角度描述了領域。

 

  每個人都會使用 Module,但卻很少有人把它們當作模型中的一個成熟的組成部分。程式碼按照各種各樣的型別進行分解,有時按照技術架構來分割,有時是按照開發人員的任務分工來分割的。甚至那些從事大量重構工作的開發人員也傾向使用專案早起形成的一些 Module 。

  眾所周知,Module 之間應該是低耦合的,而在 Module 內部則是高內聚的。耦合和內聚的解釋使得 Module 聽上去像是一種技術指標,彷彿是根據關聯和互動的分佈情況來機械地判斷它們。然而,Module 並不僅僅是程式碼的劃分,而且也是概念的劃分。一個人一次考慮的事情是有限的(因此才要低耦合)。不連貫的思路和 “一鍋粥” 似的思想同樣難於理解(因此才要高內聚)。

  低耦合高內聚作為通用的設計原則既適用於各種物件,也適用於 Module,但 Module 作為一種更粗粒度的建模和設計元素,採用低耦合高內聚原則顯得更為重要。

  在一個好的模型中,元素之間是要協同工作的,而仔細選擇的Module可以將那些具有緊密概念關係的模型元素集中到一起。將這些具有相關職責的物件聚合到一起,可以把建模和設計工作集中到單一 Module 中,這會極大地降低建模和設計的複雜性,使人們可以從容面對這些工作。

  

  像領域驅動設計中的其他元素一樣,Module 是一種表達機制。Module 的選擇應該取決於被劃分到模組中的物件的意義。當你將一些類放到 Module 中時,相當於告訴下一位看到你的設計的開發人員要把這些類放在一起考慮。如果說模型講述了一個故事,那麼Module 就是這個故事的各個章節。模組的名稱表達了其意義。這些名稱應該被新增到 Ubiquitous Language 中。你可能會向一位業務專家說 “現在讓我們討論一下‘客戶’模組”,這就為你們接下來的對話設定了上下文。

  因此:

  選擇能夠描述系統的 Module ,並使之包含一個內聚的概念集合。這通常會實現 Module 之間的低耦合,但如果效果不理想,則應尋找一種更改模型的方式來消除概念之間的耦合,或者找到一個可作為 Module 基礎的概念(這個概念先前可能被忽視了),基於這個概念組織的 Module 可以以一種有意義的方式將元素集中到一起。找到一種低耦合的概念組織方式,從而可以相互獨立地理解和分析這些概念。對模型進行精化,直到可以根據高層領域概念對模型進行劃分,同時相應的程式碼不會產生耦合。

  Module 的名稱應該是Ubiquitous Languag 中的術語。Module 及其名稱應反映出領域的深層只是。

  僅僅研究概念關係是不夠的,它並不能代替技術措施。這二者是相同問題的不同層次,都是必須完成的。但是,只有以模型為中心進行思考,才能得到更深層次的解決方案,而不是隨便找一個解決方案應付了事。當必須做出一個折中選擇時,務必保證概念清晰,即使這意味著 Module 之間會產生更多引用,或者更改 Module 偶爾會產生 “漣漪效應” 。開發人員只要理解模型所描述的內容,就可以應付這些問題。

 

  技術框架對打包決策有極大的影響,有些技術框架是有幫助的,有些則要堅決抵制。

  一個非常有用的框架標準是 Layered Architecture ,它將基礎設施和使用者介面程式碼放到兩組不同的包中,並且從物理上把領域層隔離到它自己的一組包中。

  但從另一個方面看,分層架構可能導致模型物件實現的分裂。一些框架的分層方法是把一個領域物件的職責分散到多個物件當中,然後把這些物件放到不同的包中。這樣做除了導致每個元件的實現變得更加複雜以外,還破壞了物件模型的內聚性。物件的一個基本概念是將資料和操作這些資料的邏輯封裝在一起。

   除非真正有必要將程式碼分佈到不同的伺服器上,否則就把實現單一概念物件的所有程式碼放在同一模組中(如果不能放在同一物件中的話)。

  

                                        **************************************************

  領域模型中的每個概念都應該在實現元素中反映出來。Entity、Value Object、它們之間的關聯、領域 Service 以及用於組織元素的 Module 都是實現與模型直接對應的地方。實現中的物件、指標和檢索機制必須直接、清楚地對映到模型元素。如果沒有做到這一點,就要重寫程式碼,或者回頭修改模型,或者同時修改程式碼和模型。

  不要在領域物件中新增任何與領域物件所表示的概念沒有緊密關係的元素。領域物件的職責是表示模型。當然,其他一些與領域相關的職責也是要實現的,而且為了使系統工作,也必須管理其他資料,但它們不屬於領域物件。後面講講到一些支援物件,這些物件履行領域層的技術職責,如定義資料庫搜尋和封裝複雜的物件建立。

  上述介紹的4種模式為物件模型提供了構造塊。但 Model- Driven Design 並不是說必須將每個元素都建模為物件。一些工具還支援其他的模型正規化,如規則引擎。專案需要在它們之間做出契合實際的折中選擇。這些其他的工具和技術是 Model- Driven Design 的補充,而不是要取而代之。

 

  2.6 建模正規化

  Model- Driven Design 要求使用一種與建模正規化協調的實現技術。目前主流的正規化是物件導向設計,這種正規化的流行有許多原因,包括物件本身的固有因素和一些環境因素。

  不管在專案中使用哪種主要的模型正規化,領域中都會有一些部分更容易用其他正規化來表示。當領域中只有個別元素適合用其他正規化時,開發人員可以接受一些蹩腳的物件,以使整個模型保持一致。

 

相關文章