「領域驅動設計」這個詞是在 Eric Evans 在他的著作《領域驅動設計:解決軟體核心的複雜性(Domain-Driven Design: Tackling complexity in the Heart of Software, 2003)》中所提出的,書中正式地提出了許多軟體開發的概念。
我無法用一篇文章就概括 DDD,與 DDD 相關的重要概念實在太多了。以下列出我認為一些重要的 DDD 概成,包含:
- Ubiquitous Language
- Layers
- Bounded Contexts
- Anti-Corruption Layer
- Shared Kernel
- Generic Subdomain
通用語言 Ubiquitous Language
在軟體開發中,一個常見的問題是怎麼理解程式碼,它是什麼,它能做什麼,它如何做,它為什麼這樣做…如果程式碼使用的術語與領域專家使用的術語不同,理解程式碼便變得更加複雜。例如,如果領域專家談論的是 elder users,而程式碼中提到的是 supervisors,那麼名詞可能就會造成在討論應用程式時造成混淆。然而,大部分的模糊性可以透過適當地命名類別和方法來解決,讓類別的命名明確地表達出物件是什麼,讓方法的命名明確地表達出方法在領域上下文中做了什麼事。
使用通用語言的主要概念是將應用程式與商業邏輯對齊,這是通過在程式碼中採用業務與技術之間的共同語言所實現的。該語言的來源是公司的業務部門,它們擁有需要實施的概念,但術語則與公司的技術部門協商(這意味著業務部門並不總是選擇最佳命名)以創建一種業務與程式開發人員共通且不會發生歧義的共同語言。包含程式碼、類別、方法、屬性和模組的命名都最重與通用語言對齊。
層 Layers
我在之前的文章中已經談過分層的概念,但我認為此刻重提由DDD所識別的各層是很重要的
User Interface 使用者介面
負責繪製用戶與應用程式互動的螢幕,並將用戶的輸入轉換為應用程式命令。值得注意的是,「用戶」可以是人類,但也可以是連接到我們API的其他應用程式,這完全對應於EBI架構中的邊界對象。
Application Layer 應用層
協調領域對象以執行用戶所需的任務:用例。它不包含業務邏輯。這與EBI架構中的互動者相關,只是互動者是與UI或實體無關的任何對象,而在這種情況下,應用層只包含與用例相關的對象。這一層是應用服務所屬的地方,因為它們是用例協調發生的容器,使用存儲庫、領域服務、實體、價值對象或任何其他領域對象。
Domain Layer 領域層
這是包含所有業務邏輯的層,包括領域服務、實體、事件以及任何其他包含業務邏輯的對象類型。顯然,它與EBI的實體對象類型有關。這是系統的核心。領域服務將包含不完全適合於實體的領域邏輯,通常在完成某些領域動作時協調多個實體。
Infrastructure 基礎建設
支援上層的技術能力,即持久性或訊息傳遞。
有界上下文
在企業應用中,模型可能會大幅增長,同時進行程式碼開發的團隊規模也可能會擴大。這帶來了兩個問題:
- 開發人員必須處理的程式碼庫越大,認知負荷就越大,理解程式碼的難度也就越高,因此可能會引入更多的錯誤和判斷失誤,
- 越多的開發人員在同一個程式碼庫上工作,就愈難協調對應用程式的共同技術與領域視野。
換句話說,手頭的問題變得過於龐大。
對於大問題的常見解決方案是將其分解成較小的部分,這正是「有界上下文」發揮作用的地方。
Two subsystems commonly serve very different user communities - Eric Evans 2014, Domain-Driven Design Reference
兩個子系統通常服務於截然不同的用戶群體 - Eric Evans 2014,領域驅動設計參考
有界上下文定義了模型的一個獨立部分適用的範疇。這種隔離可以通過解耦技術邏輯、程式碼庫分離、數據庫模式分離,以及在團隊組織方面來實現。我們隔離有界上下文的程度,如同往常一樣,取決於實際情況:我們所需和可能的情況。
有趣的是,這並非一個全新的概念。早在1992年,也就是Eric Evans之前的十一年,Ivar Jacobson就在他的書中寫到了子系統(subsystems)的概念!
早在那時,他對這個主題已經有了相當多非常具體的想法:
- 因此,該系統由多個子系統組成,這些子系統可以包含自身的子系統。在這種階層結構的底部是分析對象。因此,子系統是為了進一步開發和維護系統的一種結構方式。
- 子系統的任務是將物件打包,以降低複雜性。
- 所有與特定功能部分相關的對象將被放置在同一個子系統中
- 目標是在子系統內擁有強大的功能性耦合,並在子系統之間擁有弱耦合(現今被稱為低耦合和高內聚)
- [一個子系統]因此最好只與一個行動者連接,因為變化通常由行動者引起
- […] 首先將控制對象放入一個子系統中,然後將強烈耦合的實體對象和介面對象放在同一個子系統中
- 所有具有強烈相互功能聯繫的物件將被放置在同一個子系統中[…]
- 一個物件的變化會導致另一個物件的變化嗎?(這現在被稱為共同封閉原則 - 一起變化的類別會被一起打包 - 由羅伯特·C·馬丁在他的論文“Granularity粒度”中發表,該論文於1996年發表,比伊瓦·雅各布森的書晚了4年)
- 他們是否與同一個 actor 進行溝通?
- 他們兩者是否都依賴於第三個物件,例如介面物件或實體物件?
- 一個物件是否對另一個物件執行多個操作?(這現在被稱為共享重用原則 - 一起使用的類別將一起打包 - 由Robert C. Martin在他的論文“粒度”中提出,該論文於1996年發表,比Ivar Jacobson的書晚了4年)
- 另一個劃分的準則是,不同子系統之間的溝通應盡可能少(低耦合)
- 對於大型項目,因此可能有其他的子系統劃分標準,例如:
- 不同的開發團隊擁有不同的能力或資源,因此可能需要相應地分配開發工作(這些團隊也可能在地理上相互分隔)
- 在分散式環境中,每個邏輯節點(SOA,網路服務和微服務)可能都需要一個子系統
- 如果現有的產品可以在此系統中使用,則可能被視為一個子系統(我們的系統依賴的庫,即一個 ORM)。
Anti-Corruption Layer 反腐層
反腐層基本上是兩個子系統之間的中介軟體。它被用來隔離兩個子系統,使它們依賴於反腐層,而不是直接依賴於彼此。這樣,如果我們重構或完全替換其中一個子系統,我們只需要更新反腐層,而不需要觸及另一個子系統。
這尤其在我們需要將一個新系統與舊有系統整合時非常有用。為了不讓舊有結構主導我們設計新系統的方式,我們會創建一個防腐層,該層將適應舊有子系統的API以滿足新子系統的需求。
它主要有三個關注點:
- 將子系統的API調整以符合客戶子系統的需求;
- 在子系統之間轉譯數據和命令;
- 根據需要,建立一個或多個方向的溝通
這是一種技術,當我們無法控制一個或所有子系統時,使用它更具邏輯性,但即使我們控制所有涉及的子系統,即使它們設計得很好,但模型卻大相徑庭,我們希望防止一個模型對另一個模型的影響(改變一個子系統以符合另一個子系統的需求),使用它也可能是有意義的。
共享核心 Shared Kernel
在某些情況下,儘管我們希望完全隔離和解耦的組件,但對於一些領域程式碼來說,被多個組件共享是有意義的。
這將讓各個組件能夠保持彼此的解耦,儘管它們都與同一個共享程式碼,也就是共享核心,有所連結。
例如,這就是一個情況,一個組件觸發的事件被另一個或多個組件監聽。但這也可能是服務介面甚至實體的情況。
儘管如此,我們應該保持共享核心的規模小,並在更改它時要非常小心,以免無意間破壞其他使用它的程式碼。確保在未經其他使用該共享核心的開發團隊諮詢的情況下,不更改共享核心中的代碼是非常重要的。
通用子網域 Generic Subdomain
子網域是網域中非常獨立的一部分。一個通用子網域並非專為我們的應用程式設計,它可以用於任何類似的應用程式。
所以,如果我們有一個應用程式,其中一部分涉及到金融,或許我們可以在我們的應用程式中使用現有的金融庫。但無論如何,即使我們不能使用現有的庫並需要自己建立,如果它是一個通用的子領域,那麼它並不是我們的核心業務,我們應該將其視為必要但不是關鍵的。它並不是我們應用程式的最重要部分,所以我們最好的專家不應該專注於此,甚至應該清楚地將其置於主源代碼之外,可能需要使用依賴性管理工具進行安裝。
Conclusion 結論
我選擇在此處談論的DDD概念,再次,主要是關於單一職責,低耦合,高內聚,隔離邏輯,使我們的應用程序變得更加一致,更容易且更快地改變和適應業務需求。