領域驅動設計 ——一種將概念模型化的方式

原文發佈於:http://www.gufeng.tech/  穀風的個人主頁

1.引子

      2004年Eric Evans 發表了一本書:《Domain-Driven Design: Tackling Complexity in the Heart of Software》(中文名:《領域驅動設計:軟件核心複雜性應對之道》),在這本書中作者提出了領域驅動設計(DDD)的概念,到現在已經10多年的時間了。

1.1 面向對象與面嚮對象語言

      面向對象思想已經存在相當長的歷史了(相對於軟件的歷史),我而們使用的語言,很多也都是面向對象的,但是我們使用面向對象的語言就一定能寫出來面向對象的程序嗎?顯然是不可能的。業務邏輯代碼的堆積、缺乏良好設計的系統或模塊亦或是功能,這樣是不能保證代碼的複用性、擴展性的。

1.2  領域模型

      領域驅動設計的出現,就是爲了解決這一問題的。領域驅動設計是以建立正確的領域模型爲核心,以構建清晰的分層架構基礎,從而使面向對象的開發進入到了一個新的階段。

      領域驅動設計的前提是有一種能夠在領域專家(業務專家)、設計人員、開發人員(爲什麼會有開發人員,我們會在後面介紹原因)三類參與者通用的溝通語言,在三類參與者的不斷交流、溝通中發現領域概念(業務概念),再將概念固化成模型,最後由領域模型驅動設計並實現。

      說到這裏,看上去領域模型並沒有什麼特別的地方,與我們日常分析的方式沒什麼大的區別,我們首先來簡單介紹寫領域模型的兩個特點:

      1)業務邏輯集中在領域對象(類)上;

      2)每個領域對象是完整和獨立的,並具有自己的屬性和行爲。

      在接下來的內容中,我們一起來了解下如何實現領域驅動設計以及領域驅動設計的優點。

2.領域驅動設計

      領域驅動設計涵蓋了領域模型、領域語言、架構設計、實現幾部分內容,下面我們逐一瞭解一下。

2.1 領域模型

      關於什麼是領域模型以及領域模型的特點,在前面內容中我們有了整體的瞭解,接下來我們就領域模型本身進行一下簡單的瞭解。

2.1.1 抽象模型

      領域模型是某個邊界內的領域的一個抽象,是客觀世界的模型,首先它使有邊界的,清晰的邊界是領域模型抽象是否完整的一個重要衡量指標。在該領域模型內,我們只關心領域內的內容。

      領域模型只是實際業務的一種反映,與具體實現技術無關。可以說領域模型建立的成功與否,直接關係到最終的實現、使用等等。領域模型確保任參與人在任何時間看到的內容都是一樣的,瞭解了模型,就能知道實現的步驟。

      領域模型對於提高軟件的維護性、複用性以及業務可理解性等方面都有很好的幫助。領域模型貫穿整個分析、設計、開發過程,前面提到的三類參與者使用一種大家都能理解的語言進行溝通,確保所有人對模型的理解是一致的,這樣最終開發出來的結果和最初的設計才能最大程度的吻合。

      要建立一個好的領域模型並不簡單,甚至可能是一路坎坷,需要領域專家、設計人員、開發人員通力配合、深入交流、共享信息和知識。最後,領域模型要通過文檔或圖形方式展現出來(推薦使用圖形分解整體結構,配以文字說明)。設計足夠好的領域模型,肯定是符合業務需求的,同時也能夠快速響應需求變化。

      與領域模型緊密相關的還有另外一組概念:聚合、聚合根。下面我們來簡單瞭解下這兩個概念。

      聚合:通過定義對象間的隸屬關係和邊界來實現領域模型的內聚,

      聚合根:聚合內的某個實體,外部調用聚合時,必須從聚合根開始調用,不能繞過。

      關於聚合的一些特點:

      1)每個聚合有一個根和邊界;

      2)內部對象可互相引用,但是外部對象訪問聚合時,必須從聚合根開始;

      3)除根外,其它對象在聚合內保持唯一即可;

      4)聚合內部對象可以保持對其它聚合根的引用;

      5)刪除聚合根時,必須同時刪除其它聚合內對象。

      所有具有獨立含義並且能夠被單獨訪問的內容是聚合。

2.1.2 領域通用語言

      設想一下,領域專家滿口的專業術語,設計人員滿口的設計理論,開發人員滿口的開發語言及算法,這樣的團隊怎麼溝通!當然可以引入“翻譯”,但是“翻譯”的結果以及對結果的理解會造成多大程度上的信息丟失,誰也不確定。

      基於以上的原因,迫切需要一種大家都能夠表達出來和理解的語言——這就是領域通用語言。領域通用語言是領域驅動設計的基礎和前提。在三類參與者的各種形式溝通中,都要使用領域通用語言,確保自己的信息能夠被其他人完整、快速的理解。

2.1.3 模型到實現

      假設我們已經擁有了一個非常正確且嚴謹的模型,那麼是否能將這個模型直接轉換成代碼嗎?肯定是不行的。所以要求我們在領域建模和設計時,就要考慮最終的代碼實現,將領域模型與實現緊密關聯起來,這就是爲什麼要有開發人員參與的原因。

      這樣的結構(開發人員參與模型建立、結構設計)有利於儘早發現那些不適合在軟件中實現的模型部分並要求修正,這樣也避免了在最後實現時發現問題、修正設計所帶來的巨大時間損失。同時,因爲開發人員參與了模型設計,所以在編碼實現時,都會盡力保護模型不被破壞(因爲這是大家共同努力的結果),同時當開發人員發現編碼實現有不滿足模型或者不完善的地方,也會去完善它,進行代碼重構,這樣能夠在很大程度上提升軟件的可靠性,也便於其他人員在接手時能快速瞭解模型、掌握實現。

2.2 領域驅動設計的架構分層

      我們先來看一張Eric Evans 在他的《Domain-Driven Design: Tackling Complexity in the Heart of Software》一書中提到的分層圖:

     關於這張圖可能都不陌生,但是每一層在領域驅動設計中的職責是什麼?完成什麼樣的功能?層與層之間的協作關係是什麼樣的?這些問題會在後面一一解釋。

2.2.1 分層

      1)用戶界面

      人機交互部分,沒有特殊內容。

      2)應用

       此應用非彼應用,這裏的應用只是很薄的一層,用於給User Interface提供功能接口,並調用Domain完成功能邏輯,看到這裏應該有了比較明確的認識了,Application不包括任何業務,只是將根據User Interface的需要提供接口,並完成對一個或者多個Domain的調用。

       本層包含了所有軟件系統要完成的任務,通過本層就能瞭解整體功能,User Interface僅僅是一種展現方式。

      3)領域

       這一層是整個系統的核心部分,包括了全部的業務邏輯、業務規則等全部業務相關內容。

      4)基礎設施

       這裏的基礎設施指的是基礎技術組件,包括消息通信、持久化、緩存等等所有的基礎技術組件。

2.2.2 幾種輔助模式

      1)實體(Entity)

      具有跨越系統的生命週期甚至能超越軟件系統的一系列的延續性和標識符的對象成爲實體。簡單說就是具有絕對唯一標識的對象。比如銀行賬戶的ID是唯一的標識,那麼一個銀行賬戶就是一個實體。實體擁有自己的屬性,管理自己的內部狀態並對外暴露行爲。

      2)值對象(Value Object)

      當我們關心對象的唯一標識而只關心其屬性值的時候,這個對象就是一個值對象。對於值對象,理論上可以被輕易的創建以丟掉。如果是可共享值對象,那應該確保它的值是不可變的。“值對象應該保持儘量的簡單。當其他當事人需要一個值對象時,可以簡單地傳遞值,或者創建一個副本。”

      3)服務(Service)

      是不是所有的領域都能映射成對象呢?顯然是不可能的,那麼如何處理不能夠映射成對象的領域呢?這時候就需要服務這個東西了。服務通常對應領域的動作,代表領域中得一些重要行爲,而這些行爲又不屬於任何一個實體或者值對象。這些行爲可以定義爲服務對象。

      服務可能存在於領域層、基礎設施層等,所以要區分服務,不要濫用服務。

      服務對象不包含內部狀態,只有行爲,所以它提供的主要是行爲,作爲操作接口存在。我們需要注意的是,不需要對每一個操作創建服務。我們一起來一下服務的幾個特徵:

    (1)服務執行的操作涉及一個領域概念,這個領域概念通常不屬於一個實體或者值對象;

    (2)被執行的操作涉及到領域中的其他的對象;

    (3)操作是無狀態的。

      4)模塊(Module)

      當模型巨大,難以整體討論時,需要把這個大得模型拆成幾個關聯的模塊。

      5)聚合&聚合根

      聚合是針對數據變化可以考慮成一個單元的一組相關的對象。聚合使用邊界將內部和外部的對象劃分開來。每個聚合有一個根,是一個實體,並且它是外部可以訪問的唯一的對象。

      關於聚合與聚合根的內容,可參考2.1.1節。

       6)Factory(工廠)

      引入工廠模式,是因爲領域模型本身的複雜性決定的,創建領域對象要遠遠比創建pojo對象複雜得多,尤其是聚合會更加複雜。此時引入工廠模式,可以將複雜的實現放在工廠內,外部調用工廠方法即可得到相應領域對象,同時也隱藏了創建邏輯(主要是提供給Application和Infrastructure使用的)。

       7)Repository(倉儲、資源庫)

      倉儲最初的設計目的是用來管理內存中的對象,但我們可以擴展使用,對於需要持久化的領域對象,使用Repository將其持久化到數據庫(或其它持久化存儲)中,再次需要時,可以通過Repository將對象從數據庫中恢復。通常情況下,一個聚合對應一個倉儲。

      那麼對於那些不能夠通過單一Repository查詢出來的結果(比如界面中需要展現的數據來源於多個Repository的情況)我們該怎麼辦呢?當然可以通過調用多個Repository查詢出結果,但更好的方式是通過CQRS架構來實現,也就是說對於查詢可繞過Domain,直接由Application發起調用另外的架構或者層來實現。

       8)CQRS(Command Query Responsibility Segregation,命令查詢職責分離)

      從字面理解,就是命令和查詢要分離開,那麼什麼事命令呢?非查詢的操作即命令。結合領域驅動設計,我們可以理解成命令可以通過領域驅動設計完成,查詢則可使用簡單、直接的方式完成(如直接寫SQL)。

      由於是分離的,所以兩部分可以採用相同甚至完全不同的架構來實現,由此引申,是不是數據庫也可以分開設計呢?當然是可以的。

3.結束

      本文中我們粗略的瞭解了領域驅動設計的一些基本概念、原則和一些所謂的“模式”,在實際使用或者叫“領域驅動設計落地”的過程中,除了一些必須遵守的原則外,我們可以根據自己的業務特點、團隊優勢進行裁剪。

      沒有任何一種語言是具有絕對優勢的,同樣也沒有任何一種設計方法是絕對正確的。找準我們自己的方向,找出適合我們業務特點、團隊特點的方法,並對該方法進行落地裁剪,使之更具生命力、能夠解決我們的實際問題。

      最後,不要迷信、迷戀任何一種或幾種方法、模式,所有的方法都是人根據經驗總結出來,方法、模式可以參考並綜合使用,最終達到擁有自己的方法、自己的模式,這樣才能更好的服務於自己的業務,創造技術體系。

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