三分鐘搞懂領域驅動設計

今天的企業應用程序無疑是複雜的,並依賴一些專門技術(持久性,AJAX,Web 服務等)來完成它們的工作。作爲開發人員,我們傾向於關注這些技術細節是可以理解的。但事實是,一個不能解決業務需求的系統對任何人都沒有用,無論它看起來多麼漂亮或者如何很好地構建其基礎設施。

領域驅動設計(DDD)的理念 - 首先由 Eric Evans 在他的同名書 [1] 中描述 - 是關於將我們的注意力放在應用程序的核心,關注業務領域固有的複雜性本身。我們還將核心域(業務獨有)與支持子域(通常是通用的,如金錢或時間)區分開來,並將更多的設計工作放在覈心上。

域驅動設計包含一組用於從域模型構建企業應用程序的模式。在您的軟件生涯中,您可能已經遇到過許多這樣的想法,特別是如果您是 OO 語言的經驗豐富的開發人員。但將它們一起應用將允許您構建真正滿足業務需求的系統。

在本文中,我將介紹 DDD 的一些主要模式,瞭解一些新手似乎很難解決的問題,並重點介紹一些工具和資源(特別是一個),以幫助您在工作中應用 DDD。

代碼和模型......

使用 DDD,我們希望創建問題域的模型。持久性,用戶界面和消息傳遞的東西可以在以後出現,這是需要理解的領域,因爲正在構建的系統中,可以區分公司的業務與競爭對手。(如果不是這樣,那麼考慮購買包裝產品)。

按模型,我們不是指圖表或一組圖表; 確定,圖表很有用,但它們不是模型,只是模型的不同視圖(參見圖)。不,模型是我們選擇在軟件中實現的概念集,以代碼和用於構建交付系統的任何其他軟件工件表示。換句話說,代碼就是模型。文本編輯器提供了一種使用此模型的方法,儘管現代工具也提供了大量其他可視化(UML 類圖,實體關係圖,Spring beandocs [2],Struts / JSF 流等)。

Figure 1: Model vs Views of the Model

這是 DDD 模式的第一個:模型驅動設計(model-driven design)。這意味着能夠將模型中的概念映射到設計 / 代碼的概念(理想情況下)。模型的變化意味着代碼的變化; 更改代碼意味着模型已更改。DDD 並沒有強制要求您使用面向對象來構建域 - 例如,我們可以使用規則引擎構建模型 - 但鑑於主流企業編程語言是基於 OO 的,大多數模型本質上都是 OO。畢竟,OO 基於建模範例。模型的概念將表示爲類和接口,作爲類成員的職責。

語言

現在讓我們看一下域驅動設計的另一個基本原則。回顧一下:我們想要構建一個捕獲正在構建的系統的問題域的域模型,並且我們將在代碼 / 軟件工件中表達這種理解。爲了幫助我們做到這一點,DDD 提倡領域專家和開發人員有意識地使用模型中的概念進行溝通。因此,域專家不會根據屏幕或菜單項上的字段描述新的用戶故事,而是討論域對象所需的基礎屬性或行爲。類似地,開發人員不會討論數據庫表中的類或列的新實例變量。

嚴格要求我們開發一種普世的語言(ubiquitous language)。如果一個想法不能輕易表達,那麼它表明了一個概念,這個概念在領域模型中缺失,並且團隊共同努力找出缺失的概念是什麼。一旦建立了這個,那麼數據庫表中的屏幕或列上的新字段就會繼續顯示。

像 DDD 一樣,這種開發無處不在的語言的想法並不是一個新想法:XPers 稱之爲 “名稱系統”,多年來 DBA 將數據字典組合在一起。但無處不在的語言是一個令人回味的術語,可以出售給商業和技術人員。現在,“整個團隊” 敏捷實踐正在成爲主流,這也很有意義。

模型和上下文......

每當我們討論模型時,它總是在某種情況下。通常可以從使用該系統的最終用戶集推斷出該上下文。因此,我們有一個部署到交易員的前臺交易系統,或超市收銀員使用的銷售點系統。這些用戶以特定方式與模型的概念相關,並且模型的術語對這些用戶有意義,但不一定對該上下文之外的任何其他人有意義。DDD 稱之爲有界上下文(BC)。每個域模型都只存在於一個 BC 中,而 BC 只包含一個域模型。

我必須承認,當我第一次讀到關於 BC 時,我看不出這一點:如果 BC 與域模型同構,爲什麼要引入一個新術語?如果只有與 BC 相互作用的最終用戶,則可能不需要這個術語。然而,不同的系統(BC)也相互交互,發送文件,傳遞消息,調用 API 等。如果我們知道有兩個 BC 相互交互,那麼我們知道我們必須注意在一個概念之間進行轉換。領域和其他領域。

在模型周圍設置明確的邊界也意味着我們可以開始討論這些 BC 之間的關係。實際上,DDD 確定了 BC 之間的一整套關係,因此當我們需要將不同的 BC 鏈接在一起時,我們可以合理地確定應該做什麼:

你可以看到,在這個列表中,兩個 BC 之間的合作水平逐漸降低(見圖 2)。使用已發佈的語言(published language),我們從 BC 建立一個他們可以互動的共同標準開始; 既不擁有這種語言,而是由他們所居住的企業所擁有(甚至可能是行業標準)。有了開放主機服務(open host),我們仍然做得很好; BC 提供其作爲任何其他 BC 調用的運行時服務的功能,但是(可能)隨着服務的發展將保持向後兼容性。

Figure 2: Spectrum of Bounded Context Relationship

然而,當我們走向順從時,我們只是和我們一起生活; 一個 BC 明顯屈服於另一個。如果我們必須與購買 megabucks 的總分類帳系統集成,那可能就是我們所處的情況。如果我們使用反腐敗層,那麼我們通常會與遺留系統集成,但是 額外的層將我們儘可能地隔離開來。當然,這需要花錢來實施,但它降低了依賴風險。反腐敗層也比重新實現遺留系統便宜很多,這最多會分散我們對核心域的注意力,最壞的情況是以失敗告終。

DDD 建議我們制定一個上下文圖(context map t)來識別我們的 BC 以及我們依賴或依賴的 BC,以確定這些依賴關係的性質。圖 3 顯示了我過去 5 年左右一直在研究的系統的上下文映射。

Figure 3: Context Mapping Example

所有這些關於背景圖和 BC 的討論有時被稱爲戰略性 DDD( strategic DDD),並且有充分的理由。畢竟,當你想到它時,弄清楚 BC 之間的關係是非常政治的:我的系統將依賴哪些上游系統,我是否容易與它們集成,我是否能夠利用它們,我相信它們嗎?下游也是如此:哪些系統將使用我的服務,我如何將我的功能作爲服務公開,他們會對我有利嗎?誤解了這一點,您的應用程序可能很容易失敗。

層和六邊形

現在讓我們轉向內部並考慮我們自己的 BC(系統)的架構。從根本上說,DDD 只關心域層,實際上,它對其他層有很多話要說:表示,應用程序或基礎架構(或持久層)。但它確實期望它們存在。這是分層架構模式(圖 4)。

Figure 4: Layered Architecture

當然,我們多年來一直在構建多層系統,但這並不意味着我們必須擅長它。確實,過去的一些主流技術 - 是的,EJB 2,我正在看着你!- 對域模型可以作爲有意義的層存在的想法產生了積極的影響。所有的業務邏輯似乎滲透到應用層或(更糟糕的)表示層,留下一組貧血的域類 [3] 作爲數據持有者的空殼。這不是 DDD 的意思。

因此,要絕對清楚,應用程序層中不應存在任何域邏輯。相反,應用程序層負責事務管理和安全性等事務。在某些體系結構中,它還可能負責確保從基礎結構 / 持久層中檢索的域對象在與之交互之前已正確初始化(儘管我更喜歡基礎結構層執行此操作)。

在表示層在單獨的存儲空間中運行的情況下,應用層也充當表示層和域層之間的中介。表示層通常處理域對象或域對象(數據傳輸對象或 DTO)的可序列化表示,通常每個 “視圖” 一個。如果這些被修改,那麼表示層會將任何更改發送迴應用程序層,而應用程序層又確定已修改的域對象,從持久層加載它們,然後轉發對這些域對象的更改。

分層體系結構的一個缺點是它建議從表示層一直到基礎結構層的依賴性的線性堆疊。但是,我們可能希望在表示層和基礎結構層中支持不同的實現。如果(正如我認爲的那樣!)我們想要測試我們的應用程序就是這種情況:

我們可能還想區分 “內部” 和“外部”層之間的交互,其中內部我指的是兩個層完全在我們的系統(或 BC)內的交互,而外部交互跨越 BC。

因此,不要將我們的應用程序視爲一組圖層,另一種方法是將其視爲六邊形 [5],如圖 5 所示。我們的最終用戶使用的查看器以及 FitNesse 測試使用內部客戶端 API(或端口),而來自其他 BC 的調用(例如,RESTful 用於開放主機交互,或來自 ESB 適配器的調用用於已發佈的語言交互)命中外部客戶端端口。對於後端基礎架構層,我們可以看到用於替代對象存儲實現的持久性端口,此外,域層中的對象可以通過外部服務端口調用其他 BC。

Figure 5: Hexagonal Architecture

但這足夠大的東西; 讓我們來看看 DDD 在煤炭面板上的樣子。

構建模塊

正如我們已經注意到的,大多數 DDD 系統可能會使用 OO 範例。因此,我們的域對象的許多構建塊可能很熟悉,例如實體,值對象和模塊(entities, value objects and modules. )。例如,如果您是 Java 程序員,那麼將 DDD 實體視爲與 JPA 實體基本相同(使用 @Entity 註釋)就足夠安全了; 值對象是字符串,數字和日期之類的東西; 一個模塊就是一個包。

但是,DDD 傾向於更多地強調值對象(value objects ),而不是過去習慣。所以,是的,您可以使用 String 來保存 Customer 的 givenName 屬性的值,例如,這可能是合理的。但是一筆錢,例如產品的價格呢?我們可以使用 int 或 double,但是(甚至忽略可能的舍入錯誤)1 或 1.0 是什麼意思?$ 1 嗎?€1?¥1?1 分,甚至?相反,我們應該引入一個 Money 值類型,它封裝了 Currency 和任何舍入規則(將特定於 Currency)。

而且,值對象應該是不可變的,並且應該提供一組無副作用的函數來操作它們。我們應該寫:

Money m1 = new Money("GBP", 10);
Money m2 = new Money("GBP", 20);
Money m3 = m1.add(m2);

將 m2 添加到 m1 不會改變 m1,而是返回一個新的 Money 對象(由 m3 引用),它表示一起添加的兩個 Money。

值也應該具有值語義,這意味着(例如在 Java 和 C#中)它們實現 equals()和 hashCode()。它們通常也可以序列化,可以是字節流,也可以是 String 格式。當我們需要堅持它們時,這很有用。

值對象常見的另一種情況是標識符。因此,(US)SocialSecurityNumber 將是一個很好的例子,車輛的 RegistrationNumber 也是如此。URL 也是如此。因爲我們已經重寫了 equals()和 hashCode(),所以這些都可以安全地用作哈希映射中的鍵。

引入價值對象不僅擴展了我們無處不在的語言,還意味着我們可以將行爲推向價值觀本身。因此,如果我們確定 Money 永遠不會包含負值,我們可以在 Money 內部實現此檢查,而不是在使用 Money 的任何地方。如果 SocialSecurityNumber 具有校驗和數字(在某些國家 / 地區就是這種情況),則該校驗和的驗證可以在值對象中。我們可以要求 URL 驗證其格式,返回其方案(例如 http),或者確定相對於其他 URL 的資源位置。

我們的另外兩個構建塊可能需要更少的解釋。實體通常是持久的,通常是可變的並且(因此)傾向於具有一生的狀態變化。在許多體系結構中,實體將作爲行保存在數據庫表中。同時,模塊(包或命名空間)是確保域模型保持解耦的關鍵,並且不會成爲泥漿中的一大塊 [6]。在他的書中,埃文斯談到概念輪廓,這是一個優雅的短語,用於描述如何區分域的主要關注領域。模塊是實現這種分離的主要方式,以及確保模塊依賴性嚴格非循環的接口。我們使用諸如 Uncle“Bob”Martin 的依賴倒置原則[7] 之類的技術來確保依賴關係是嚴格單向的。

實體,值和模塊是核心構建塊,但 DDD 還有一些不太熟悉的構建塊。我們現在來看看這些。

聚合和聚合根

如果您精通 UML,那麼您將記住,它允許我們將兩個對象之間的關聯建模爲簡單關聯,聚合或使用組合。聚合根(有時縮寫爲 AR)是通過組合組成其他實體(以及它自己的值)的實體。也就是說,聚合實體僅由根引用(可能是可傳遞的),並且可能不會被聚合外的任何對象(永久地)引用。換句話說,如果實體具有對另一個實體的引用,則引用的實體必須位於同一聚合內,或者是某個其他聚合的根。

許多實體是聚合根,不包含其他實體。對於不可變的實體(相當於數據庫中的引用或靜態數據)尤其如此。示例可能包括 Country,VehicleModel,TaxRate,Category,BookTitle 等。

但是,更復雜的可變(事務)實體在建模爲聚合時確實會受益,主要是通過減少概念開銷。我們不必考慮每個實體,而只考慮聚合根; 聚合實體僅僅是聚合的 “內部運作”。它們還簡化了實體之間的相互作用; 我們遵循以下規則:(持久化)引用可能只是聚合的根,而不是聚合中的任何其他實體。

另一個 DDD 原則是聚合根負責確保聚合實體始終處於有效狀態。例如,Order(root)可能包含 OrderItems 的集合(聚合)。可能存在以下規則:訂單發貨後,任何 OrderItem 都無法更新。或者,如果兩個 OrderItem 引用相同的產品並具有相同的運輸要求,則它們將合併到同一個 OrderItem 中。或者,Order 的派生 totalPrice 屬性應該是 OrderItems 的價格總和。維護這些不變量是 root 的責任。

但是...... 只有聚合根才能完全在聚合中維護對象之間的不變量。OrderItem 引用的產品幾乎肯定不會在 AR 中,因爲還有其他用例需要與 Product 進行交互,而不管是否有訂單。因此,如果有一條規則不能對已停產的產品下達訂單,那麼訂單將需要以某種方式處理。實際上,這通常意味着在訂單交易更新時使用隔離級別 2 或 3 來 “鎖定” 產品。或者,可以使用帶外過程來協調交叉聚合不變量的任何破壞。

在我們繼續前進之前退一步,我們可以看到我們有一系列粒度:

value < entity < aggregate < module < bounded context

現在讓我們繼續研究一些 DDD 構建塊。

存儲庫,工廠和服務(Repositories, Factories and Services)

在企業應用程序中,實體通常是持久的,其值表示這些實體的狀態。但是,我們如何從持久性存儲中獲取實體呢?

存儲庫是持久性存儲的抽象,返回實體 - 或者更確切地說是聚合根 - 滿足某些標準。例如,客戶存儲庫將返回 Customer 聚合根實體,訂單存儲庫將返回 Orders(及其 OrderItems)。通常,每個聚合根有一個存儲庫。

因爲我們通常希望支持持久性存儲的多個實現,所以存儲庫通常由具有不同持久性存儲實現的不同實現的接口(例如,CustomerRepository)組成(例如,CustomerRepositoryHibernate 或 CustomerRepositoryInMemory)。由於此接口返回實體(域層的一部分),因此接口本身也是域層的一部分。接口的實現(與一些特定的持久性實現耦合)是基礎結構層的一部分。

我們搜索的標準通常隱含在名爲的方法名稱中。因此,CustomerRepository 可能會提供 findByLastName(String)方法來返回具有指定姓氏的 Customer 實體。或者我們可以讓 OrderRepository 返回 Orders,findByOrderNum(OrderNum)返回與 OrderNum 匹配的 Order(請注意,這裏使用值類型!)。

更復雜的設計將標準包裝到查詢或規範中,類似於 findBy(Query ),其中 Query 包含描述標準的抽象語法樹。然後,不同的實現解包查詢以確定如何以他們自己的特定方式定位滿足條件的實體。

也就是說,如果你是. NET 開發人員,那麼值得一提的是 LINQ [8]。因爲 LINQ 本身是可插拔的,所以我們通常可以使用 LINQ 編寫存儲庫的單個實現。然後變化的不是存儲庫實現,而是我們配置 LINQ 以獲取其數據源的方式(例如,針對 Entity Framework 或針對內存中的對象庫)。

每個聚合根使用特定存儲庫接口的變體是使用通用存儲庫,例如 Repository 。這提供了一組通用方法,例如每個實體的 findById(int)。當使用 Query (例如 Query )對象指定條件時,這很有效。對於 Java 平臺,還有一些框架,例如 Hades [9],允許混合和匹配方法(從通用實現開始,然後在需要時添加自定義接口)。

存儲庫不是從持久層引入對象的唯一方法。如果使用對象關係映射(ORM)工具(如 Hibernate),我們可以在實體之間導航引用,允許我們透明地遍歷圖形。根據經驗,對其他實體的聚合根的引用應該是延遲加載的,而聚合中的聚合實體應該被急切加載。但與 ORM 一樣,期望進行一些調整,以便爲最關鍵的用例獲得合適的性能特徵。

在大多數設計中,存儲庫還用於保存新實例,以及更新或刪除現有實例。如果底層持久性技術支持它,那麼它們很可能存在於通用存儲庫中,但是從方法簽名的角度來看,沒有什麼可以區分保存新客戶和保存新訂單。

最後一點...... 直接創建新的聚合根很少見。相反,它們傾向於由其他聚合根創建。訂單就是一個很好的例子:它可能是通過客戶調用一個動作來創建的。

這整齊地帶給我們:

工廠

如果我們要求 Order 創建一個 OrderItem,那麼(因爲畢竟 OrderItem 是其聚合的一部分),Order 知道要實例化的具體 OrderItem 類是合理的。實際上,實體知道它需要實例化的同一模塊(命名空間或包)中的任何實體的具體類是合理的。

假設客戶使用 Customer 的 placeOrder 操作創建訂單(參見圖 6)。如果客戶知道具體的訂單類,則意味着客戶模塊依賴於訂單模塊。如果訂單具有對客戶的反向引用,那麼我們將在兩個模塊之間獲得循環依賴。

Figure 6: Customers and Orders (cyclic dependencie

如前所述,我們可以使用依賴性反轉原則來解決這類問題:從訂單中刪除依賴關係 - > 客戶模塊我們將引入 OrderOwner 接口,使 Order 引用爲 OrderOwner,並使 Customer 實現 OrderOwner(參見圖 7))。

Figure 7: Customers and Orders (customer depends order)

那麼另一種方式呢:如果我們想要訂單 - > 客戶?在這種情況下,需要在客戶模塊中有一個表示 Order 的接口(這是 Customer 的 placeOrder 操作的返回類型)。然後,訂單模塊將提供訂單的實現。由於客戶不能依賴訂單,因此必須定義 OrderFactory 接口。然後,訂單模塊依次提供 OrderFactory 的實現(參見圖 8)。

可能還有相應的存儲庫接口。例如,如果客戶可能有數千個訂單,那麼我們可能會刪除其訂單集合。相反,客戶將使用 OrderRepository 根據需要定位其訂單(的一部分)。或者(如某些人所願),您可以通過將對存儲庫的調用移動到應用程序體系結構的更高層(例如域服務或應用程序服務)來避免從實體到存儲庫的顯式依賴性。

實際上,服務是我們需要探索的下一個話題。

域服務,基礎結構服務和應用程序服務(Domain services, Infrastructure services and Application services)

域服務(domain service)是在域層內定義的域服務,但實現可以是基礎結構層的一部分。存儲庫是域服務,其實現確實在基礎結構層中,而工廠也是域服務,其實現通常在域層內。特別是在適當的模塊中定義了存儲庫和工廠:CustomerRepository 位於客戶模塊中,依此類推。

更一般地說,域服務是任何不容易在實體中生存的業務邏輯。埃文斯建議在兩個銀行賬戶之間進行轉賬服務,但我不確定這是最好的例子(我會將轉賬本身建模爲一個實體)。但另一種域服務是一種充當其他有界上下文的代理。例如,我們可能希望與暴露開放主機服務的 General Ledger 系統集成。我們可以定義一個公開我們需要的功能的服務,以便我們的應用程序可以將條目發佈到總帳。這些服務有時會定義自己的實體,這些實體可能會持久化; 這些實體實際上影響了在另一個 BC 中遠程保存的顯着信息。

我們還可以獲得技術性更強的服務,例如發送電子郵件或 SMS 文本消息,或將 Correspondence 實體轉換爲 PDF,或使用條形碼標記生成的 PDF。接口在域層中定義,但實現在基礎架構層中非常明確。因爲這些非常技術性服務的接口通常是根據簡單的值類型(而不是實體)來定義的,所以我傾向於使用術語基礎結構服務(infrastructure service)而不是域服務。但是如果你想成爲一個 “電子郵件”BC 或 “SMS”BC 的橋樑,你可以想到它們。

雖然域服務既可以調用域實體也可以調用域實體,但應用服務(application service)位於域層之上,因此域層內的實體不能調用,只能反過來調用。換句話說,應用層(我們的分層架構)可以被認爲是一組(無狀態)應用服務。

如前所述,應用程序服務通常處理交叉和安全等交叉問題。他們還可以通過以下方式與表示層進行調解:解組入站請求; 使用域服務(存儲庫或工廠)獲取對與之交互的聚合根的引用; 在該聚合根上調用適當的操作; 並將結果編組回表示層。

我還應該指出,在某些體系結構中,應用程序服務調用基礎結構服務。因此,應用服務可以直接調用 PdfGenerationService,傳遞從實體中提取的信息,而不是實體調用 PdfGenerationService 將其自身轉換爲 PDF。這不是我的特別偏好,但它是一種常見的設計。我很快就會談到這一點。

好的,這完成了我們對主要 DDD 模式的概述。在 Evans 500 + 頁面書中還有更多內容 - 值得一讀 - 但我接下來要做的是突出顯示人們似乎很難應用 DDD 的一些領域。

問題和障礙

實施分層架構

這是第一件事:嚴格執行架構分層可能很困難。特別是,從域層到應用層的業務邏輯滲透可能特別隱蔽。

我已經在這裏挑出了 Java 的 EJB2 作爲罪魁禍首,但是模型 - 視圖 - 控制器模式的不良實現也可能導致這種情況發生。控制器(= 應用層)會發生什麼,承擔太多責任,讓模型(= 域層)變得貧血。事實上,有更新的 Web 框架(在 Java 世界中,Wicket [10] 是一個嶄露頭角的例子),出於這種原因明確地避免了 MVC 模式。

表示層模糊了域層

另一個問題是嘗試開發無處不在的語言。領域專家在屏幕方面談話是很自然的,因爲畢竟,這就是他們可以看到的系統。要求他們在屏幕後面查看並在域概念方面表達他們的問題可能非常困難。

表示層本身也可能存在問題,因爲自定義表示層可能無法準確反映(可能會扭曲)底層域概念,從而破壞我們無處不在的語言。即使不是這種情況,也只需要將用戶界面組合在一起所需的時間。使用敏捷術語,速度降低意味着每次迭代的進度較少,因此對整個域的深入瞭解較少。

存儲庫模式的實現

從更技術性的角度來看,新手有時似乎也會混淆將存儲庫(在域層中)與其實現(在基礎架構層中)的接口分離出來。我不確定爲什麼會這樣:畢竟,這是一個非常簡單的 OO 模式。我想這可能是因爲埃文斯的書並沒有達到這個細節水平,這讓一些人變得高高在上。但這也可能是因爲替換持久性實現(根據六邊形體系結構)的想法並不普遍,導致持久性實現滲透到域層的系統。

服務依賴項的實現

另一個技術問題 - 在 DDD 從業者之間可能存在分歧 - 就實體與域 / 基礎設施服務(包括存儲庫和工廠)之間的關係而言。有些人認爲實體根本不應該依賴域服務,但如果是這種情況,則外部應用程序服務與域服務交互並將結果傳遞給域實體。根據我的思維方式,這使我們走向了一個貧血的領域模型。

稍微柔和的觀點是實體可以依賴於域服務,但應用程序服務應該根據需要傳遞它們,例如作爲操作的參數。我也不喜歡這個:對我而言,它將實現細節暴露給應用層(“這個實體需要這樣一個服務才能完成這個操作”)。但是許多從業者對這種方法感到滿意。

我自己的首選方案是使用依賴注入將服務注入實體。實體可以聲明它們的依賴關係,然後基礎結構層(例如 Hibernate,Spring 或其他一些框架)可以將服務注入實體:

public class Customer {
…
 private OrderFactory orderFactory;
 public void setOrderFactory(OrderFactory orderFactory) {
 this.orderFactory = orderFactory;
 }
 …
 public Order placeOrder() {
 Order order = orderFactory.createOrder();return order;
 }
 }

一種替代方法是使用服務定位器模式。例如,將所有服務註冊到 JNDI 中,然後每個域對象查找它所需的服務。在我看來,這引入了對運行時環境的依賴。但是,與依賴注入相比,它對實體的內存需求較低,這可能是一個決定性因素。

不合適的模塊化

正如我們已經確定的那樣,DDD 在實體之上區分了幾種不同的粒度級別,即聚合,模塊和 BC。獲得正確的模塊化水平需要一些練習。正如 RDBMS 模式可能被非規範化一樣,系統也沒有模塊化(成爲泥漿的大球)。但是,過度規範化的 RDBMS 模式(其中單個實體在多個表上被分解)也可能是有害的,過模塊化系統也是如此,因爲它變得難以理解系統如何作爲整體工作。

我們首先考慮模塊和 BC。記住,模塊類似於 Java 包或. NET 命名空間。我們希望兩個模塊之間的依賴關係是非循環的,但是如果我們確定(比如說)客戶依賴於訂單,那麼我們不需要做任何額外的事情:客戶可以簡單地導入 Order 包 / 命名空間並使用它接口和類根據需要。

但是,如果我們將客戶和訂單放入單獨的 BC 中,那麼我們還有更多的工作要做,因爲我們必須將客戶 BC 中的概念映射到 BC 訂單的概念。在實踐中,這還意味着在客戶 BC 中具有訂單實體的表示(根據前面給出的總分類帳示例),以及通過消息總線或其他東西實際協作的機制。請記住:擁有兩個 BC 的原因是當有不同的最終用戶和 / 或利益相關者時,我們無法保證不同 BC 中的相關概念將朝着相同的方向發展。

另一個可能存在混淆的領域是將實體與聚合區分開來。每個聚合都有一個實體作爲其聚合根,對於很多很多實體,聚合將只包含這個實體(“瑣碎”的情況,正如數學家所說的那樣)。但我看到開發人員認爲整個世界必須存在於一個聚合中。因此,例如,訂單包含引用產品的 OrderItems(到目前爲止一直很好),因此開發人員得出結論,產品也在聚合中(不!)更糟糕的是,開發人員會觀察到客戶有訂單,所以想想這個意味着我們必須擁有 Customer / Order / OrderItem / Product 的巨型聚合(不,不,不!)。關鍵是 “客戶有訂單” 並不意味着暗示彙總; 客戶,訂單和產品都是集合的根源。

實際上,一個典型的模塊(這是非常粗糙和準備好的)可能包含六個聚合,每個聚合可能包含一個實體和幾個實體之間。在這六個中,一個好的數字可能是不可變的 “參考數據” 類。還要記住,我們模塊化的原因是我們可以理解一件事(在一定的粒度級別)。所以要記住,典型的人一次只能保持在 5 到 9 個之間[11]。

入門

正如我在開始時所說,你可能在 DDD 之前遇到過很多想法。事實上,我所說過的每一個 Smalltalker(我不是一個,我不敢說)似乎很高興能夠在 EJB2 等人的荒野歲月之後迴歸域驅動的方法。

另一方面,如果這些東西是新的怎麼辦?有這麼多不同的方式來絆倒,有沒有辦法可靠地開始使用 DDD?

如果你環顧一下 Java 領域(對. NET 來說並不那麼糟糕),實際上有數百個用於構建 Web 應用程序的框架(JSP,Struts,JSF,Spring MVC,Seam,Wicket,Tapestry 等)。從持久性角度(JDO,JPA,Hibernate,iBatis,TopLink,JCloud 等)或其他問題(RestEasy,Camel,ServiceMix,Mule 等),有很多針對基礎架構層的框架。但是很少有框架或工具來幫助 DDD 所說的最重要的層,即域層。

自 2002 年以來,我一直參與(現在是一個提交者)一個名爲 Naked Objects 的項目,Java 上的開源 [12] 和. NET 上的商業[13]。雖然 Naked Objects 沒有明確地開始考慮領域驅動的設計 - 事實上它早於 Evans 的書 - 它與 DDD 的原理非常相似。它還可以輕鬆克服前面提到的障礙。

您可以將 Naked Objects 視爲與 Hibernate 等 ORM 類似。ORM 構建域對象的元模型並使用它來自動將域對象持久保存到 RDBMS,而 Naked Objects 構建元模型並使用它在面向對象的用戶界面中自動呈現這些域對象。

開箱即用的 Naked Objects 支持兩個用戶界面,一個富客戶端查看器(參見圖 9)和一個 HTML 查看器(參見圖 10)。這些都是功能完備的應用程序,需要開發人員只編寫要運行的域層(實體,值,存儲庫,工廠,服務)。

Figure 9: Naked Objects Drag-n-Drop Viewer

我們來看看 Claim 類的(Java)代碼(如屏幕截圖所示)。首先,這些類基本上是 pojos,儘管我們通常從便捷類 AbstractDomainObject 繼承,只是爲了分解注入通用存儲庫並提供一些幫助方法:

public class Claim extends AbstractDomainObject {
...
}
Next, we have some value properties:
// {{ Description
private String description;
@MemberOrder(sequence = "1")
public String getDescription() { return description; }
public void setDescription(String d) { description = d; }
// }}

// {{ Date
private Date date;
@MemberOrder(sequence="2")
public Date getDate() { return date; }
public void setDate(Date d) { date = d; }
// }}

// {{ Status
private String status;
@Disabled
@MemberOrder(sequence = "3")
public String getStatus() { return status; }
public void setStatus(String s) { status = s; }
// }}

這些是簡單的 getter / setter,返回類型爲 String,日期,整數等(儘管 Naked Objects 也支持自定義值類型)。接下來,我們有一些參考屬性:

// {{ Claimant
private Claimant claimant;
@Disabled
@MemberOrder(sequence = "4")
public Claimant getClaimant() { return claimant; }
public void setClaimant(Claimant c) { claimant = c; }
// }}

// {{ Approver
private Approver approver;
@Disabled
@MemberOrder(sequence = "5")
public Approver getApprover() { return approver; }
public void setApprover(Approver a) { approver = a; }
// }}

這裏我們的 Claim 實體引用其他實體。實際上,Claimant 和 Approver 是接口,因此這允許我們將域模型分解爲模塊,如前所述。

實體也可以擁有實體集合。在我們的案例中,Claim 有一個 ClaimItems 的集合:

// {{ Items
private List<ClaimItem> items = new
ArrayList<ClaimItem>();
@MemberOrder(sequence = "6")
public List<ClaimItem> getItems() { return items; }
public void addToItems(ClaimItem item) {
items.add(item);
}
// }}

我們還有(Naked Objects 調用的)動作,即 submit 和 addItem:這些都是不代表屬性和集合的公共方法:

// {{ action: addItem
public void addItem(
@Named("Days since")
int days,
@Named("Amount")
double amount,
@Named("Description")
String description) {
ClaimItem claimItem = newTransientInstance(ClaimItem.class);
Date date = new Date();
date = date.add(0,0, days);
claimItem.setDateIncurred(date);
claimItem.setDescription(description);
claimItem.setAmount(new Money(amount, "USD"));
persist(claimItem);
addToItems(claimItem);
}
public String disableAddItem() {
return "Submitted".equals(getStatus()) ? "Already
submitted" : null;
}
// }}
// {{ action: Submit
public void submit(Approver approver) {
setStatus("Submitted");
setApprover(approver);
}
public String disableSubmit() {
return getStatus().equals("New")?
null : "Claim has already been submitted";
}
public Object[] defaultSubmit() {
return new Object[] { getClaimant().getApprover() };
}

// }}

這些操作會在 Naked Objects 查看器中自動呈現爲菜單項或鏈接。而這些行動的存在意味着 Naked Objects 應用程序不僅僅是 CRUD 風格的應用程序。

最後,有一些支持方法可以顯示標籤(或標題)並掛鉤持久性生命週期:

// {{ Title
public String title() {
return getStatus() + " - " + getDate();
}
// }}

// {{ Lifecycle
public void created() {
status = "New";
date = new Date();
}
// }}

之前我將 Naked Objects 域對象描述爲 pojos,但您會注意到我們使用註釋(例如 @Disabled)以及命令式幫助器方法(例如 disableSubmit())來強制執行業務約束。Naked Objects 查看器通過查詢啓動時構建的元模型來尊重這些語義。如果您不喜歡這些編程約定,則可以更改它們。

典型的 Naked Objects 應用程序由一組域類組成,例如上面的 Claim 類,以及存儲庫,工廠和域 / 基礎結構服務的接口和實現。特別是,沒有表示層或應用層代碼。那麼 Naked Objects 如何幫助解決我們已經確定的一些障礙?

下一步

領域驅動的設計彙集了一組用於開發複雜企業應用程序的最佳實踐模式。一些開發人員多年來一直在應用這些模式,對於這些人來說,DDD 可能只是對他們現有實踐的肯定。但對於其他人來說,應用這些模式可能是一個真正的挑戰。

Naked Objects 爲 Java 和. NET 提供了一個框架,通過處理其他層,團隊可以專注於重要的部分,即域模型。通過直接在 UI 中公開域對象,Naked Objects 允許團隊非常自然地構建一個明確無處不在的語言。隨着域層的建立,團隊可以根據需要開發更加量身定製的表示層。

那麼,下一步呢?

嗯,DDD 本身的聖經是埃裏克埃文斯的原着,“領域驅動設計”[1],建議閱讀所有人。雅虎新聞組 DDD [19] 也是一個非常好的資源。如果你有興趣瞭解 Naked Objects 的更多信息,你可以搜索 “使用 Naked Objects 的域驅動設計”[20],或者博客 [21](NO for Java)或 Naked Objects 網站 [13 ](對於. NET 而言)。快樂 DDD'ing!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/P2KVPl2eJZaWH8VuByNy4g