今年,Eric Evans 具有開創性的軟件設計圖書“Domain-Driven Design: Tackling Complexity in the Heart of Software”(領域驅動設計:軟件核心復雜性應對之道) (Addison-Wesley Professional,2003 年,amzn.to/ffL1k)迎來了其出版十周年的紀念日。Evans 在本書中分享了其指導大型企業完成構建軟件的過程的多年經驗。他隨後花了更長時間考慮 如何概括幫助這些項目獲得成功的模式 - 與客戶交互、分析要解決的企業問題、組建團隊和 設計軟件的架構。這些模式的焦點是企業的領域,它們共同組成領域驅動設計 (DDD)。利用 DDD,您可以為有問題的領域建模。這些模式是通過抽象化您對領域的知識產生的。即使在今 天,重讀 Martin Fowler 的前言和 Evans 的序言仍能獲得對 DDD 本質的豐富概覽。
在本專欄以及接下來的兩個專欄中,我將分享一些指導原則,在我著手讓我的代碼從 某些 DDD 技術模式獲益時,這些原則幫助我那注重數據的 Entity Framework 大腦保持清晰 的思維。
我為什麼關注 DDD?
我對 DDD 的介紹摘自發布於 InfoQ.com 上的對 Jimmy Nilsson 的簡短視頻訪談。Jimmy Nilsson 是 .NET 社區(以及其他地方)一位受人尊敬的架構師,他在訪談中談到了 LINQ to SQL 和 Entity Framework (bit.ly/11DdZue)。在訪談的最後,Nilsson 被要求列 舉他最喜歡的技術書籍。他回答道: “我最喜歡的計算機書籍是 Eric Evans 編著的 “Domain-Driven Design”(領域驅動設計)。我覺得它就像詩一樣美妙。它不僅內容精彩 ,還有詩一般的韻味,讓人百讀不厭。”詩一般的美妙!當時,我正在寫我的第一本技術書 籍“Programming Entity Framework”(實體框架編程)(O’Reilly Media,2009 年), 這一描述引發了我的興趣。因此,我去讀了一點 Evans 的書,想看看到底寫得如何。Evans 是一個行文優美而流暢的作者,再加上他在軟件開發領域具有敏銳而自然的見解,讀者在閱 讀本書時體驗到了充分的樂趣。不過,使我感到震撼的還有我讀到的內容。Evans 不僅文筆 極佳,而且書中的內容也非常吸引我。他提到與客戶建立關系並真正了解其業務和業務問題 (與有問題的軟件有關),而不只是苦苦編寫代碼。在我 25 年的軟件開發生涯中,這個觀 點起到了舉足輕重的作用。我想要實現更多。
我在 DDD 領域的邊緣又徘徊了很多年 ,然後開始了解更多內容 - 我在一次會議上見到了 Evans,隨後參加了他為期四天的浸入式 研討會。雖然我遠遠不是 DDD 方面的專家,但我發現在試圖將自己的軟件創建過程向更有條 理且更易於管理的結構轉變時,可立即使用“界定的上下文”模式。您可以閱讀我的 2013 年 1 月專欄中的主題“使用 DDD 界定的上下文收縮 EF 模型”(msdn.microsoft.com/magazine/jj 883952)。
從那時起,我對 DDD 的研究又深入了一步。我對 DDD 感到深深著迷 ,並從中獲得了很多靈感,但我為從數據驅動的角度理解一些可推動成功的技術模式而糾結 。似乎有很多開發人員也遇到了相同的問題,因此我想分享在 Evans 和很多其他 DDD 實踐 者和教師(包括 Paul Rayner、Vaughn Vernon、Greg Young、Cesar de la Torre 和 Yves Reynhout)的慷慨幫助和關心之下吸取的經驗教訓。
在為域建模時忘記持久性
為域建模就是重點關注企業的任務。在設計類型及其屬性和行為時,我非常傾向於 考慮關系如何在數據庫中發揮作用以及我選擇的對象關系映射 (ORM) 框架(即 Entity Framework)將如何處理我構建的屬性、關系和繼承層次結構。除非您為從事數據存儲和檢索 業務的公司(像 Dropbox 一樣)構建軟件,否則數據持久性僅在您的應用程序中起到支持作 用。這非常像調用天氣源的 API 以便向用戶顯示當前溫度,或者,從您的應用程序向外部服 務發送數據(可能是 Meetup.com 上的注冊信息)。當然,您的數據可能更加復雜,但利用 DDD 的上下文界定方法,通過關注行為和在構建類型時遵循 DDD 指導,持久性可能比您在今 天構建的系統簡單得多。
如果您已仔細研究 ORM(例如,了解如何使用 Entity Framework Fluent API 配置數據庫映射),則應能根據需要實現持久性。最糟糕的情況是, 您可能需要對您的類做一些調整。在極端情況下(例如,使用舊數據庫時),您甚至可以添 加專為數據庫映射設計的持久性模型,然後使用 AutoMapper 等工具解析域模型和持久性模 型之間的映射。
但是,這些關注點與您的軟件用於解決的業務問題無關,因此持久性 不應影響域設計。這對我來說是一個難題,因為我在設計實體時會忍不住考慮 EF 將如何推 斷其數據庫映射。因此,我試圖解決這件煩心事。
私有 Setter 和公共方法
另一個經驗法則是將屬性 setter 設為私有。您應使用修改屬性的方法控制與 DDD 對象及其相關數據的交互,而不是允許通過調用代碼來隨機設置各種屬性。不,我不是指 SetFirstName 和 SetLastName 之類的方法。例如,您在創建新客戶前需要考慮一些規則, 而不是實例化新的 Customer 類型並設置其各個屬性。您可以將這些規則內置於 Customer 的構造函數中,使用 Factory Pattern 方法,甚至在 Customer 類型中包含 Create 方法。 圖 1 顯示按照聚合根(即對象圖的“父級”,也稱為 DDD 中的“根實體”)的 DDD 模式定 義的 Customer 類型。客戶屬性具有私有 setter,以便僅 Customer 類的其他成員能夠直接 影響這些屬性。該類公開了構造函數以控制其實例化方式,並將無參數構造函數(Entity Framework 需要)隱藏為內部構造函數。
圖 1 充當聚合根的類型的屬性和方法
public class Customer : Contact { public Customer(string firstName,string lastName, string email) { ... } internal Customer(){ ... } public void CopyBillingAddressToShippingAddress(){ ... } public void CreateNewShippingAddress( string street, string city, string zip) { ... } public void CreateBillingInformation( string street, string city, string zip, string creditcardNumber, string bankName){ ... } public void SetCustomerContactDetails( string email, string phone, string companyName){ ... } public string SalesPersonId { get; private set; } public CustomerStatus Status{get;private set;} public Address ShippingAddress { get; private set; } public Address BillingAddress { get;private set; } public CustomerCreditCard CreditCard { get; private set; } }
Customer 類型通過以下方式控制和保護聚合中的其他實體(一些地址和信用卡類 型):公開將用於創建和操作這些對象的特定方法(如 CopyBillingAddressToShippingAddress)。聚合根必須確保通過在這些方法中實現的域邏輯 和行為應用定義聚合中的每個實體的規則。最重要的一點是,聚合根負責確保聚合中的固定 條件邏輯和一致性。我將在下一個專欄中詳細討論固定條件,同時,我建議您閱讀 Jimmy Bogard 的博客文章“Strengthening Your Domain: Aggregate Construction”(增強您的 域:聚合結構)(網址為 bit.ly/ewNZ52),該文很好地解釋了聚合中的固定條件 。
最後,Customer 公開的內容是行為而不是屬性: CopyBillingAddressToShippingAddress、CreateNewShippingAddress、 CreateBillingInformation 和 SetCustomerContactDetails。
請注意,從中派生 Customer 的 Contact 類型位於名為“Common”的另一個程序集中, 因為其他類可能需要該類型。我需要隱藏 Contact 的屬性,但它們不能是私有屬性,否則 Customer 將無法訪問它們。相反,應將其范圍設為“受保護”:
public class Contact: Identity { public string CompanyName { get; protected set; } public string EmailAddress { get; protected set; } public string Phone { get; protected set; } }
關於標識的附注: Customer 和 Contact 可能看起來像 DDD 值對象,因為它們 沒有鍵值。但在我的解決方案中,鍵值由從中派生 Contact 的 Identity 類提供。這些類型 都不是固定不變的,因此任何情況下都不能將它們視為值對象。
由於 Customer 繼承自 Contact,因此 Customer 能夠訪問和設置這些受保護的屬性,如 以下 SetCustomerContactDetails 方法中所示:
public void SetCustomerContactDetails (string email, string phone, string companyName) { EmailAddress = email; Phone = phone; CompanyName = companyName; }
有時您只需 CRUD 就夠了
在您的應用程序中,並非所有內容都需要使用 DDD 來創建。DDD 用來幫助處理復雜的行 為。如果您只需執行一些粗略而隨意的編輯或查詢,則一個簡單類(或一組類)就足夠了。 此簡單類的定義方式與您平時使用 EF Code First(使用屬性和關系)時一樣,它還與插入 、更新和刪除方法(通過存儲庫或只是 DbContext)結合使用。因此,若要完成創建訂單及 其明細項目之類的操作,您可能需要 DDD 來幫助執行特殊的業務規則和行為。例如,下訂單 的是否是此金星級客戶?在這種情況下,您需要獲取一些客戶詳細信息來確定答案是否為“ 是”,如果是這樣,則對要添加到訂單的每個項目應用 10% 的折扣。用戶是否已提供其信用 卡信息?隨後,您可能需要調用驗證服務來確保該信用卡有效。
DDD 的關鍵在於將域邏輯作為方法包含在域的實體類中,並在無狀態業務對象中利用 OOP 而不是實現“事務腳本”,這是典型演示件 Code First 類的外觀。
不過,有時您執行的所有操作都是非常標准的,例如創建聯系人記錄 (姓名、地址、推 薦人等)並保存該信息。這就是所謂的創建、閱讀、更新和刪除 (CRUD)。您不需要創建聚合 、根和行為即可滿足該要求。
您的應用程序很可能包含復雜行為和簡單 CRUD 的組合。花時間來闡明行為吧,不要把時 間、精力和金錢浪費在過度設計事實上非常簡單的應用程序部件的架構上。在這些情況下, 標識不同子系統或界定的上下文之間的邊界很重要。界定的上下文可能在很大程度上是由數 據驅動的(即 CRUD),而另一方面,以核心領域界定的關鍵上下文應按 DDD 方法進行設計 。
查看本欄目
共享數據在復雜系統中可能是個大麻煩
我遇到了另一個問題,當其他人滿懷好意地試圖進一步說明時,我抱怨不已,對跨子系統 共享類型和數據充滿擔心。很明顯,我不可能魚與熊掌兼得,因此我必須重新考慮我的假設 :我絕對必須積極地跨系統共享類型,並讓所有這些類型與同一數據庫中的同一表進行交互 。
我正在學習實際考慮需在何處共享數據,然後迎接挑戰。有些事情可能不值得嘗試,例如 ,從不同的上下文映射到單個表,甚至單個數據庫。最常見的示例是共享嘗試滿足系統中所 有人的需求的 Contact。您如何為大量系統中可能需要的 Contact 類型協調和利用源代碼管 理?如果有系統需要修改該 Contact 類型的定義,該怎麼辦?對於 ORM,您如何將在多個系 統中使用的 Contact 映射到單個數據庫中的單個表?
通過說明您並不總是需要指向單個數據庫中的同一個人員表,DDD 可指導您避免共享域模 型和數據。
我反對此行為的最大理由源於這 25 年來我對重用(重用代碼和重用數據)的好處的關注 。因此,我對以下觀點既感到費解又很有興趣: 復制數據並不是犯罪行為。當然,並非所有 數據都適合此新模式(對我而言)。不過,對於輕量型內容(如人員姓名),情況又如何呢 ?如果您將人員的名字和姓氏復制到多個表中,甚至復制到專用於軟件解決方案的各個子系 統的多個數據庫中,會怎麼樣呢?從長遠來看,通過消除共享數據的復雜性,可大大簡化構 建系統這一工作。在任何情況下,您必須始終最大程度地減少在不同的界定上下文中復制數 據和屬性的工作。有時,您只需要客戶的 ID 和狀態便能計算 Pricing 界定的上下文中的折 扣。可能只在 Contact Management 界定的上下文中需要此客戶的名字和姓氏。
但是,仍然有如此多的信息需要在系統之間進行共享。您可以利用 DDD 稱之為“反損壞 層”(可以像服務或消息隊列一樣簡單)的工具來確保當有人在一個系統中創建了新聯系人 時,您可以確定該人員在其他位置是否已存在,或確保人員及常用標識鍵已在其他子系統中 創建。
下個月前要考慮的大量問題
當我學習和理解領域驅動型設計的技術方面、努力協調舊習慣和新思想以及無數次覺得茅 塞頓開時,我在這裡討論的心得真正起到了撥開雲霧見青天的效果。有時,這只是一種觀點 ,我在這裡表達這些心得的方式反映了幫助我更清晰地了解事物的觀點。
我將在下一個專欄中分享我的得意時刻,並將在其中討論您可能聽到過的表現優劣的用詞 : “貧乏的域模型”及其 DDD 同類“豐富的域模型”。我還將討論不定向關系,並討論在 您使用 Entity Framework 的情況下,在需要添加數據持久性時要考慮的內容。我還將探討 更多的 DDD 主題,這些主題為我縮短您自己的學習曲線的工作帶來了大量困難。
到那時,為何不更詳細地觀察您自己的類並了解如何更傾向於做一個控制狂,隱藏這些屬 性 setter 並公開更多描述性和顯式方法。還要記住: 不允許使用“SetLastName”方法。 開個玩笑!
下載代碼示例