DDD項目總結

一、思維導圖

DDD(Domain-Driven Design 領域驅動設計)

二、技術設計方案

 

 三、基礎要點

1.實體(entity):

        根據eric evans的定義,”一個由它的標識定義的對象叫做實體”。通常實體具備唯一id,能夠被持久化,具有業務邏輯,對應現實世界業務對象。

         實體一般和主要的業務/領域對象有一個直接的關係。一個實體的基本概念是一個持續抽象的生命,可以變化不同的狀態和情形,但總是有相同的標識。

需要注意的是:

         一些開發人員將實體當成了orm意義上的實體,而不是業務所有和業務定義的領域對象。在一些實現中採用了transaction script風格的架構,使用貧血的領域模型。這種認識上的混亂,在領域驅動架構中,不願意在領域對象中加入業務邏輯而導致貧血的領域模型,同時還可能使混亂的服務對象激增。

2.值對象(value object)

        值對象的定義是:描述事物的對象;更準確的說,一個沒有概念上標識符描述一個領域方面的對象。這些對象是用來表示臨時的事物,或者可以認爲值對象是實體的屬性,這些屬性沒有特性標識但同時表達了領域中某類含義的概念。

        通常值對象不具有唯一id,由對象的屬性描述,可以用來傳遞參數或對實體進行補充描述。

        作爲實體屬性的描述時,值對象也會被存儲。在uml的類圖上顯現爲一對多或一對一的關係。在orm映射關係上需要採用較複雜的一對多或一對一關係映射。

        關於實體與值對象的一個例子:比如員工信息的屬性,如住址,電話號碼都可以改變;然而,同一個員工的實體的標識將保持不變。因此,一個實體的基本概念是一個持續抽象的生命,可以變化不同的狀態和情形,但總是有相同的標識。

實體與值對象的區別

         實體具有唯一標識,而值對象沒有唯一標識,這是實體和值對象間的最大不同。

        實體就是領域中需要唯一標識的領域概念。有兩個實體,如果唯一標識不一樣,那麼即便實體的其他所有屬性都一樣,也認爲是兩個不同的實體;一個實體的基本概念是一個持續抽象的生命,可以變化不同的狀態和情形,但總是有相同的標識。

        不應該給實體定義太多的屬性或行爲,而應該尋找關聯,發現其他一些實體或值對象,將屬性或行爲轉移到其他關聯的實體或值對象上。

        如果兩個對象的所有的屬性的值都相同,我們會認爲它們是同一個對象的話,那麼我們就可以把這種對象設計爲值對象。值對象在判斷是否是同一個對象時是通過它們的所有屬性是否相同,如果相同則認爲是同一個值對象;而實體是否爲同一個實體的區分,只是看實體的唯一標識是否相同,而不管實體的屬性是否相同。

        值對象另外一個明顯的特徵是不可變,即所有屬性都是隻讀的。因爲屬性是隻讀的,所以可以被安全的共享;當共享值對象時,一般有複製和共享兩種做法,具體採用哪種做法還要根據實際情況而定。

        箴言:如果值對象時可共享的,它們應該是不可變的。(值對象應該保持儘量的簡單)

         值對象的設計應儘量簡單,不要讓它引用很多其他的對象,因爲本質上講值對象只是代表一個值。

3.聚合及聚合根(aggregate、aggregate root):

        聚合是用來定義領域對象所有權和邊界的領域模式。聚合的作用是幫助簡化模型對象間的關係。聚合,它通過定義對象之間清晰的所屬關係和邊界來實現領域模型的內聚,並避免了錯綜複雜的難以維護的對象關係網的形成。聚合定義了一組具有內聚關係的相關對象的集合,我們把聚合看作是一個修改數據的單元。

        劃分aggregation是對領域模型的進一步深化,aggregation能闡釋領域模型內部對象之間的深層關聯.對aggregation的劃分會直接映射到程序結構上.比如:ddd推薦按aggregation設計model的子包.每個aggregation配備一個repository.aggregation內部的非root對象是通過導航獲得的.        

        一個聚合是一組相關的被視爲整體的對象。每個聚合都有一個根對象(聚合根實體),從外部訪問只能通過這個對象。根實體對象有組成聚合所有對象的引用,但是外部對象只能引用根對象實體。

         只有聚合根才能使用倉儲庫直接查詢,其它的只能通過相關的聚合訪問。如果根實體被刪除,聚合內部的其它對象也將被刪除。

         通常,我們把聚合組織到一個文件夾或一個包中。每一個聚集對應一個包,並且每個聚集成員包括實體、值對象,domain事件,倉儲接口和其它工廠對象。

 

聚合有以下一些特點:

  1. 每個聚合有一個根和一個邊界,邊界定義了一個聚合內部有哪些實體或值對象,根是聚合內的某個實體;

  2. 聚合內部的對象之間可以相互引用,但是聚合外部如果要訪問聚合內部的對象時,必須通過聚合根開始導航,絕對不能繞過聚合根直接訪問聚合內的對象,也就是說聚合根是外部可以保持對它的引用的唯一元素;

  3. 聚合內除根以外的其他實體的唯一標識都是本地標識,也就是隻要在聚合內部保持唯一即可,因爲它們總是從屬於這個聚合的;

  4. 聚合根負責與外部其他對象打交道並維護自己內部的業務規則

  5. 基於聚合的以上概念,我們可以推論出從數據庫查詢時的單元也是以聚合爲一個單元,也就是說我們不能直接查詢聚合內部的某個非根的對象;

  6. 聚合內部的對象可以保持對其他聚合根的引用;

  7. 刪除一個聚合根時必須同時刪除該聚合內的所有相關對象,因爲他們都同屬於一個聚合,是一個完整的概念。

如何識別聚合?

        聚合中的對象關係是內聚的,即這些對象之間必須保持一個固定規則,固定規則是指在數據變化時必須保持不變的一致性規則。

        當我們在修改一個聚合時,我們必須在事務級別確保整個聚合內的所有對象滿足這個固定規則。

        作爲一條建議,聚合儘量不要太大,否則即便能夠做到在事務級別保持聚合的業務規則完整性,也可能會帶來一定的性能問題。

        有分析報告顯示,通常在大部分領域模型中,有70%的聚合通常只有一個實體,即聚合根,該實體內部沒有包含其他實體,只包含一些值對象;另外30%的聚合中,基本上也只包含兩到三個實體。這意味着大部分的聚合都只是一個實體,該實體同時也是聚合根。

 

如何識別聚合根?

  如果一個聚合只有一個實體,那麼這個實體就是聚合根;如果有多個實體,可以思考聚合內哪個對象有獨立存在的意義並且可以和外部直接進行交互。

       並不是所有的實體都是聚集根,但只有實體才能成爲聚集根。

4.工廠(factories):

       工廠用來封裝創建一個複雜對象尤其是聚合時所需的知識,作用是將創建對象的細節隱藏起來。客戶傳遞給工廠一些簡單的參數,然後工廠可以在內部創建出一個複雜的領域對象然後返回給客戶。當創建 實體和值對象複雜時建議使用工廠模式。

       不意味着我們一定要使用工廠模式。如果創建對象很簡單,使用構造器或者控制反轉/依賴注入容器足夠創建對象的依賴。此時,我們就不需要通用工廠模式來創建實體或值對象。

       良好工廠的要求:

       每個創建方法都是原子的。一個工廠應該只能生產透明狀態的對象。對於實體,意味着創建整個聚合時滿足所有的不變量。

      一個單獨的工廠通常生產整個聚合,傳出一個根實體的引用,確保聚合的不變量都有。如果對象的內部聚合需要工廠,通常工廠方法的邏輯放在在聚合根上。這樣對外部隱藏了聚合內聚的實現,同時賦予了根確保聚合完整的職責。如果聚合根不是子實體工廠的合適的家,那麼繼續創建一個單獨的工廠。

 

5.倉儲(repositories):

        倉儲是用來管理實體的集合。

         倉儲裏面存放的對象一定是聚合,原因是domain是以聚合的概念來劃分邊界的;聚合作爲一個整體概念,要麼一起被取出來,要麼一起被刪除。外部訪問不會單獨對某個聚合內的子對象進行單獨操作。因此,我們只對聚合設計倉儲。

         倉儲還有一個重要的特徵就是分爲倉儲定義部分和倉儲實現部分,我們在領域模型中定義倉儲的接口,而在基礎設施層實現具體的倉儲。也符合按照接口分離模式在領域層定義倉儲庫接口的原則。

        注意:repositories本身是一種領域組件,但repositories的實現卻不是領域層中的。

respositories和dao:

         dao和repository在領域驅動設計中都很重要。dao是面向數據訪問的,是關係型數據庫和應用之間的契約。

        repository:位於領域層,面向aggregation root。repository是一個獨立的抽象,使用領域的通用語言,它與dao進行交互,並使用領域理解的語言提供對領域模型的數據訪問服務的“業務接口”。

  dao方法是細粒度的,更接近數據庫,而repository方法的粒度粗一些,而且更接近領域。領域對象應該只依賴於repository接口。客戶端應該始終調用領域對象,領域對象再調用dao將數據持久化到數據 存儲中。

  處理領域對象之間的依賴關係(比如實體及其repository之間的依賴關係)是開發人員經常遇到的典型問題。解決這個問題通 常的設計方案是讓服務類或外觀類直接調用repository,在調用repository的時候返回實體對象給客戶端。

6.服務(services):

         服務這個詞在服務模式中是這麼定義的:服務提供的操作是它提供給使用它的客戶端,並突出領域對象的關係。

         所有的service只負責協調並委派業務邏輯給領域對象進行處理,其本身並真正實現業務邏輯,絕大部分的業務邏輯都由領域對象承載和實現了。

         service可與多種組件進行交互,這些組件包括:其他的service、領域對象和repository 或 dao。

         通常,應用中一般包括:domain模型服務和應用層服務:

        *  domain services encapsulate domain concepts that just are not naturally modeled as things.

        *  application services constitute the application, or service, layer.

        當一個領域操作被視爲一個重要的領域概念,一般就應該作爲領域服務。 服務應該是無狀態的。

        設計實現領域服務來協調業務邏輯,只在領域服務中實現領域邏輯的調用。

        領域服務邏輯須以非常乾淨簡潔的代碼實現。因此,我們必須實現對領域低層組件的調用。通常應用的調用,例如倉儲庫的調用,創建事務等,不應該在這裏實現。這些操作應該在應用層實現。

          通常服務對象名稱中都應包含一個動詞。 service接口的傳入傳出參數也都應該是dto,可能包含的工作有領域對象和dto的互轉換以及事務。

      服務的3個特徵:

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

  b. 被執行的操作涉及到領域中其它的對象

  c. 操作時無狀態的

推薦:最好顯式聲明服務,因爲它創建了領域中一個清晰的特性,封裝了一個概念領域層服務和基礎設施層服務:均建立在領域實體和值對象的上層,以便直接爲這些相關的對象提供所需的服務;

 

領域服務與domain對象的區別

        一般的領域對象都是有狀態和行爲的,而領域服務沒有狀態只有行爲。需要強調的是領域服務是無狀態的,它存在的意義就是協調領域對象共同完成某個操作,所有的狀態還是都保存在相應的領域對象中。

        通常,對開發人員來說創建不應該存在的服務相當容易;要麼在服務中包含了本應存在於領域對象中的領域邏輯,要麼扮演了缺失的領域對象角色,而這些領域對象並沒有作爲模型的一部分去創建。

 

7.domain事件

        domain event模式最初由udi dahan提出,發表在自己的博客上:http://www.udidahan.com/2009/06/14/domain-events-salvation/

        企業級應用程序事件大致可以分爲三類:系統事件、應用事件和領域事件。領域事件的觸發點在領域模型(domain model)中。它的作用是將領域對象從對repository或service的依賴中解脫出來,避免讓領域對象對這些設施產生直接依賴。它的做法就是當領域對象的業務方法需要依賴到這些對象時就發出一個事件,這個事件會被相應的對象監聽到並做出處理。

        通過使用領域事件,我們可以實現領域模型對象狀態的異步更新、外部系統接口的委託調用,以及通過事件派發機制實現系統集成。另外,領域事件本身具有自描述性。它不僅能夠表述系統發生了什麼事情,而且還能夠描述發生事件的動機。

         domain事件也用表進行存儲。

8.DTO

       dto- datatransfer object(數據傳輸對象):dto在設計之初的主要考量是以粗粒度的數據結構減少網絡通信並簡化調用接口。user interface:

        該層包含與其他系統/客戶進行交互的接口與通信設施,在多數應用裏,該層可能提供包括web services、rmi或rest等在內的一種或多種通信接口。該層主要由facade、dto和assembler三類組件構成,三類組件均是典型的j2ee模式。

        dto的作用最初主要是以粗粒度的數據結構減少網絡通信並簡化調用接口。在領域驅動設計中,採用dto模型,可以起到隱藏領域細節,幫助實現獨立封閉的領域模型的作用。

        dto與領域對象之間的相互轉換工作多由assembler承擔,也有一些系統使用反射機制自動實現dto與領域對象之間的相互轉換,如apache common beanutils。

        facade的用意在於爲遠程客戶端提供粗粒度的調用接口。facade本身不處理任何的業務邏輯,它的主要工作就是將一個用戶請求委派給一個或多個service進行處理,同時藉助assembler將service傳入或傳出的領域對象轉化爲dto進行傳輸。

 

application:

         application層中主要組件就是service。這裏需要注意的是,service的組織粒度和接口設計依據與傳統transaction script風格的service是一致的,但是兩者的實現卻有質的區別。

  transaction script(事務腳本)的核心是過程,通過過程的調用來組織業務邏輯,業務邏輯在服務(service)層進行處理。大部分業務應用都可以被看成一系列事務。

         transaction script的特點是簡單容易理解,面向過程設計。  如果應用相對簡單,在應用的生命週期裏不會有基礎設施技術的改變,尤其是業務邏輯很少會變動,採用transaction script風格簡單自然,性能良好,容易理解。

        transaction script的缺點在於,對於複雜的業務邏輯難以保持良好的設計,事務之間的冗餘代碼不斷增多。應用架構容易出現“胖服務層”和“貧血的領域模型”。同時,service層積聚越來越多的業務邏輯,導致可維護性和擴展性變差

  領域模型屬於面向對象設計,領域模型具備自己的屬性行爲和狀態,領域對象元素之間通過聚合配合解決實際業務應用。可複用,可維護,易擴展,可以採用合適的設計模型進行詳細設計。缺點是相對複雜,要求設計人員有良好的抽象能力。

        transactionscript風格業務邏輯主要在service中實現,而在領域驅動設計的架構裏,service只負責協調並委派業務邏輯給領域對象進行處理。因此,我們可以考察這一點來識別系統是transaction script架構還是domain model架構。在實踐中,設計良好的領域設計架構在開發過程中也容易向transaction script架構演變。

 

domain:

        domain層是整個系統的核心層,該層維護一個使用面向對象技術實現的領域模型,幾乎全部的業務邏輯會在該層實現。domain層包含entity(實體)、valueobject(值對象)、domain event(領域事件)和repository(倉儲)等多種重要的領域組件。

infrastructure:

        infrastructure(基礎設施層)爲interfaces、application和domain三層提供支撐。所有與具體平臺、框架相關的實現會在infrastructure中提供,避免三層特別是domain層摻雜進這些實現,從而“污染”領域模型。infrastructure中最常見的一類設施是對象持久化的具體實現。

設計領域模型的一般步驟:

       1.   根據需求建立一個初步的領域模型,識別出一些明顯的領域概念以及它們的關聯,關聯可以暫時沒有方向但需要有(1:1,1:n,m:n)這些關係;可以用文字精確的沒有歧義的描述出每個領域概念的涵義以及包含的主要信息;

       2.   分析主要的軟件應用程序功能,識別出主要的應用層的類;這樣有助於及早發現哪些是應用層的職責,哪些是領域層的職責;

       3.   進一步分析領域模型,識別出哪些是實體,哪些是值對象,哪些是領域服務;

       4.   分析關聯,通過對業務的更深入分析以及各種軟件設計原則及性能方面的權衡,明確關聯的方向或者去掉一些不需要的關聯;

       5.   找出聚合邊界及聚合根,這是一件很有難度的事情;因爲你在分析的過程中往往會碰到很多模棱兩可的難以清晰判斷的選擇問題,所以,需要我們平時一些分析經驗的積累才能找出正確的聚合根;

       6.   爲聚合根配備倉儲,一般情況下是爲一個聚合分配一個倉儲,此時只要設計好倉儲的接口即可;

       7.   走查場景,確定我們設計的領域模型能夠有效地解決業務需求

       8.   考慮如何創建領域實體或值對象,是通過工廠還是直接通過構造函數;

       9.   停下來重構模型。尋找模型中覺得有些疑問或者是蹩腳的地方,比如思考一些對象應該通過關聯導航得到還是應該從倉儲獲取?聚合設計的是否正確?考慮模型的性能怎樣,等等;

 

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