主頁 > 軟體設計 > 《領域驅動設計》:從領域視角深入倉儲(Repository)的設計和實作

《領域驅動設計》:從領域視角深入倉儲(Repository)的設計和實作

2023-02-04 07:27:37 軟體設計

一、前言
“ DDD設計的目標是關注領域模型而并非技術來創建更好的軟體,假設開發人員構建了一個SQL,并將它傳遞給基礎設施層中的某個查詢服務然后根據表資料的結構集取出所需資訊,最后將這些資訊提供給建構式或者Factory,開發人員在做這一切的時候早已不把模型看做重點了,這個整個程序就變成了資料處理的風格 ”——摘 Eric Evans《領域驅動設計》
《領域驅動設計》中的Repository(下面將用倉儲表示)層實際上是極具有挑戰性的,對于它的理解,也十分重要,本文大部分內容都在眾多前輩理論基礎上,從一個嶄新的領域視覺開始探索,并結合自己的實踐感悟進行細致的決議,同時本文不僅僅是DDD前輩的搬運工,也創新提出了倉儲物體轉移的概念,可以提供給讀者思考是否在自己場景中可以用到這種模式,即使讀者也對倉儲有很深的了解,我也覺得本文會對你有新的閱讀體驗,
導讀:
  • 本文首先從聚合根的生命周期和生存環境出發,引出了Repository概念,并說明其本質是管理中間程序的集合容器(2.1節);

  • 根據集合容器的概念,在領域角度去挖掘出Repository的職責,并提出了倉儲物體轉移模式用作對不同倉儲實作的對比標準(2.2節);

  • 然后從實作例子出發,介紹了一種純記憶體實作的倉儲,用作體現倉儲最佳實作(3.1節);

  • 繼續從實作例子出發,介紹了關系型資料庫下的倉儲特點,并描述面向持久化的倉儲的特點(3.4節);

 

二、概念剖析
DDD作者在介紹倉儲模式的時候,談到了大部分技術的程序會入侵領域模型,讓開發人員迷失,本文反其道行之,讀者可以先假設記憶體是無限大的,便于我們先關注模型再討論技術實作,然后我們先從DDD中的重要概念 聚合物體 的領域模型使用出發,挖掘出倉儲的本質特征和與之相關領域概念,然后再從本質特征,指導如何實作倉儲,

2.1 聚合物體

服務于物體的集合容器:說到倉儲我們必須要先討論聚合(聚合是由物體和值物件組成,其中有一個物體為聚合根,后面提到聚合物體即聚合根),倉儲必然是為聚合物體服務的,值物件則不必要,那我們的物體為何需要倉儲呢?這得從物體的整個生命周期說起,我們先總結一下DDD中聚合物體的特點:
  • 標識:物體具有唯一標識,這個唯一標識使得物體和值物件區分開來;

  • 狀態:物體是具有可以被改變的狀態,因此聚合物體無法被靜態描述;

  • 生命周期:物體擁有生命周期,從物體的創建,到物體的狀態的終態;

  • 生存環境:物體的活動存在于各個背景關系中的領域服務或者應用服務中,其中分用例程序和中間程序;

  • 用例程序:只要在執行用例程序的時候才需要物體的存在,其他時候,物體生命周期并沒有結束,而是處于中間狀態;

  • 中間程序:當沒有任何用例在處理一個物體的時候,物體消失了嗎?沒有,它仍然存在生命周期內,這個時候我們認為物體正處在一種中間程序,

其中最重要的就是物體會存在于各個背景關系中的用例運行程序中,之外的都會存在于一個中間程序中,我們用圖示來進行中間程序的描述,
圖片
檢索聚合根:在解決空間的運行態中,用例調度者(執行者、執行緒)要么新建聚合物體,要么獲取中間程序的聚合物體,創建新物體好說,但是中間程序的物體是如何獲取到的呢?其實中間程序的物體,只能是經過查找到得到的,這是一個檢索的程序,其中檢索包括全體遍歷(包括索引)和關聯遍歷,不管何種檢索渠道,我們都要讓Domain感覺到,檢索回來的物體還是原來那個物體,
統一語言:中間程序、用例程序,這些詞領域專家、業務人員是聽不懂的,中間程序也不在模型關注點上,但又是與模型有關聯,所以我們在領域角度、統一語言角度,封裝角度,這個中間程序都應該提出一個統一的領域概念抽象,屏蔽掉中間程序的細節,讓領域專家能明白我們的意思,倉儲(倉庫,貯藏室,Repository),這個詞就很適合,它類似一個幫你暫存物品的倉庫,然后你可以在倉庫中找回你要的物品,
但這個詞本身不重要,重要的是領域專家能聽懂倉儲這個詞的語意,并和技術人員統一,搭建一個溝通的橋梁,有關倉儲的統一語言應該有以下幾點:
  • 放置:建立一個新的聚合物體,這是一個聚合物體生命的開始,在用例程序結束后,把聚合物體放到倉儲中;

  • 查找:把已經存在的聚合物體找出來,這是一個聚合物體的中間程序到用例程序的行為;

  • 管理:它負責聚合物體的中間程序管理,并屏蔽掉中間程序的細節,向領域層提供統一的能力抽象,一些資料統計類的也可以在該范疇內;

集合容器:為了方便的把處于中間程序的物體找出來,我們的倉儲需要解決兩個問題,第一個是如何放置物體,第二個問題是如何檢索物體,
  • 如何放置物體:為了方便管理,我們通常會采用分治把同一種型別的物體放在一起成為一個集合,相同型別和集合給了我們一個指導就是:倉儲的設計應該是一個聚合物體型別對應一個倉儲物體,具有一一對應關系,所以倉儲物體應該是一個保存相同型別元素的集合容器;

  • 如何查找物體:我們知道物體具有唯一標識別,也具有其他特征屬性,所以為了查找物體,我們應該通過物體的唯一標識或者特征屬性去遍歷查找,倉儲應當提供這種功能,所以倉儲應該針對聚合物體欄位具有索引查找功能;

  • 如何查找倉儲:既然我們提到了需要用倉儲來查找物體,那么我們又是如何查到倉儲的呢?其實這個很簡單,如果一個聚合物體型別只具有一個倉儲型別,那么我們把倉儲設計為單例的就可以了,

圖片

我們從領域模型的生存環境角度,引申出了倉儲的必要性,并在統一語言的原則上,從它的必要性行為中挖掘出了倉儲的特征,關注領域模型的倉儲,應當讓客戶感覺模型就一直在記憶體中一樣,最后我們總結一下倉儲的本質:
  • 一個聚合型別(也就是一個聚合根),最好對應一個倉儲(這個不是絕對的);

  • 一個倉儲應該是單例的,便于先查到到倉儲,再查找到聚合物體(當然也不是絕對的);

  • 倉儲應該是一個集合的抽象概念,并且負責屏蔽中間程序,包括其中的實作細節,如持久化和重建,它最好能讓客戶感覺它似乎就一直在記憶體中一樣

  • 倉儲作為聚合物體的集合,應該具有檢索物體的功能,如果從技術角度看,那么將一直持有聚合物體參考;

 

2.2 倉儲職責

倉儲與統計:在我們關注領域服務的時候,會有部分統計的領域邏輯可以歸納到中間程序管理中,例如我要根據某個聚合根的個數進行更新另一個聚合,倉儲也應當封裝這部分邏輯,主要是考慮到以下幾點:
  • 我們的一個用例服務中很可能不需要使用聚合物體本身,而僅使用到符合某種條件的聚合的數量,因此我們沒必要查出聚合物體進行統計;

  • 具體的基礎設施資料庫實作,對統計性能有著顯著的性能優化,為了使用這些中間技術的優點,把統計這種細節的操作委托給倉儲是一個很好的選擇,

  • 統計和查詢有很多時候的應用場景是不修改聚合根狀態的,所以這種情況你可能沒必要使用倉儲完成這件事,CQRS的思想要求我們去分離查詢,建立查詢模型,所以建立一套查詢模型去做這件事是一個好的解耦實踐,

倉儲與規格:上面提到倉儲應當具有檢索功能,檢索必然需要一些聚合物體的狀態欄位作為入參,最好的直接檢索是通過物體的唯一標識別進行,但如果我們有大量不同的欄位檢索需求,為每一個需求在倉儲建立一個這樣的方法介面,必然讓倉儲變得臃腫,規格這個概念可以消除這種臃腫變得可能,我們抽象一個規格物體,然后把規格作為一個引數傳給倉儲,讓倉儲根據規格獲取聚合物體,便可統一檢索功能,對該模式敢興趣的可以參考Eric Evans的《領域驅動設計》第9章:
  • 規格是一個謂詞,封裝了業務規則,可以明確表達一個特定物體是否滿足該規格標準;

  • 規則是值物件,可以組合使用,其組合實作與SQL的拼湊非常契合,使得其十分適合應用在倉儲;

  • 規格的概念引入,使得我們對物體多種檢索的需求程序做到了通用化;

  • 好的規格實作,鏈式 API 呼叫,可以使得編程變得靈活,表達能力強流暢;

倉儲與唯一標識:上面提到,聚合物體具有唯一標識,其中唯一標識的生產方法也有很多種(如用戶輸入生成、分布式ID生成、資料庫持久化時候生成),生成時機也可以在執行用例步驟之初,也可以在事務持久化的時候,在用例執行之初的情況下,我們其實可以讓倉儲封裝這種生成唯一標識,或者直接讓倉儲提供新聚合的工廠方法,這種表達會更自然,
  • 倉儲生成唯一標識別:在利用資料庫能力生成唯一ID的時候(例如TDDL的Sequence),因為倉儲本身封裝資料庫細節,所以倉儲可以單獨提供這種功能,例如 DomainRepository.getInstance().newEntityId() 方法,回傳一個由資料庫管理的唯一ID,

  • 倉儲提供工廠方法:聚合物體的創建,不一定是由領域服務完成的,如果我們的聚合物體具有創建模板,那么我們可以假設倉儲本身具有大量的新物件池待使用,所以可以這樣創建物體:DomainRepository.getInstance().newXXEntity() 回傳聚合物體(該方式Evric不推薦);

倉儲與Resource:Repository通常被翻譯為資源庫,個人認為對比倉儲,資源庫的描述可能會讓我們更多的把聚合物體看作為一種網路中可以唯一定位的資源(Resource)抽象,我們通常在網路術語中看到資源的概念,如URL中的R即資源,如REST架構風格(表現層狀態轉移)也會把物件當初是資源,如果從資源角度看倉儲,就是實實在在的資源庫:
  • 作為Resource,我們通常會給它定一個URI(統一資源識別),用作全網唯一識別,但很少資源庫會定義URI,因為物體唯一標識已經足夠;

  • 作為Resource,倉儲一但持有了資源,那么就一直持有并跟蹤資源,直到資源被洗掉;

  • 作為Resource,倉儲有時會被當作是對遠程服務行程封裝的機制,這個時候倉儲有點像防腐層,但我不建議這樣做(國內部分書籍有這種介紹);

介紹這種角度,只是想讓讀者了解各種一些方案背后的設計理念,后面介紹面向集合的倉儲的時候,或者需要結合DDD和REST架構風格的時候,讀者可以自行體會聚合物體作為Resource的意義,
倉儲物體轉移(創新):現在我們討論一個問題,當我們從倉儲中獲取到聚合物體之后,倉儲是否還應該擁有該聚合物體?如果我們拋開計算機和技術概念,完全從問題空間出發,那么倉儲是不再擁有聚合物體的:想象一下,一個倉庫管理人員需要處理一個商品,當他從倉庫獲取到該商品后后,另一個人在倉庫中還能找到這個商品嗎?按照這種思維對倉儲進行建模,倉儲和聚合的關系可以明確為:
  • 聚合物體一個時刻只能存在于一個用例程序或者一個倉儲實體中;

  • 聚合物體無法同時存在在倉儲中和用例程序中;

  • 聚合物體也無法同時存在于兩個用例程序中;

如果我們在解空間中對這個程序進行建模,可以描述為下圖:
圖片
有人或者會覺得我對這個倉儲的建模太較真了,因為我完全從問題空間角度看這個問題,但我提出這個的目的,只是想為后面的實踐方案提供一個以問題空間為主的參考標準,突出在倉儲選擇不同實作的時候不得不屈服于技術的特性從而使得倉儲的特性產生的差異,我會在每個實作中提出如果要抹平差異要怎么做,并給出可以應用的場景,讀者理解這些差異后會對倉儲有更深的了解,其中《實作領域驅動設計》中Vaughn Vernon提出的一種實作為面向持久化的資源庫和這種問題空間角度其實是相通的,而Vaughn Vernon提出的另一種實作為面向集合的資源庫和解空間看的角度是想通的,我暫且將倉儲物體轉移描述為一種模式(后面統一為倉儲物體轉移模式),在該模式下,倉儲領域本質上,應該只有兩種操作:
  • 放置(put或save):把聚合物體從用例程序,放置到倉儲中,狀態變為中間程序,用例程序中不再擁有物體;

  • 獲取(Take):用例程序運行中,需要把物體從中間程序,轉移到用例程序,完成這個操作后,倉儲將不再擁有物體,我特別用take而不是find表達了這種思想,

大家可以對比資料庫的操作更新洗掉,這兩個操作是帶著資料建模的思想,我將會在下面關系型資料倉儲中提及,讓大家衡量要不要倉儲增加這兩種行為,同時也會介紹在關系型資料倉儲實作和記憶體倉儲實作如何改進為倉儲物體轉移模式,達到對比的目的,
作為開發人員,我們在應用DDD,關注模型的時候隔離了中間程序,確實得到了以模型為關注點的概念設計,但我們還需要兼顧技術的實作難度以及可行性,其實整個倉儲的解決方案在細節中并沒有那么簡單,下面我們開始沿著領域模型分析的結論,開始看技術實作的鴻溝,
三、實作剖析
如果有無限大的記憶體,或者無需持久化的業務,DAO層必然不存在,但倉儲(集合容器+檢索的資料結構)是仍然存在的,這就是為什么我認為,理解倉儲的本質,不應該從技術角度思考,而是從領域角度思,即使我們對倉儲在領域上有幾乎固定的職責和功能,具體實作的倉儲都很難滿足其領域模型角度的功能,在《實作領域驅動設計》一書中,Vaughn Vernon提出2種倉儲的實作模式:
  1. 面向集合的資源庫:面向集合的倉儲提出的是完全按照集合的理念去設計倉儲,就似乎它就是Set資料結構一樣,所以他能自動去跟蹤聚合物體的變化

  2. 面向持久化的資源庫:面向持久化的倉儲,核心點是合并了插入和更新這兩種操作,統一用 save() 操作完全取代倉儲舊物體使得倉儲的功能更統一,這種資料存盤(如MongoDB等檔案資料庫)通常稱之為:面向聚合的資料庫(Aggregation-Oriented DataBase)或聚合存盤(Aggregation Store),

以上兩種模式對倉儲來說都沒有統一,他們各有不同特點,面向集合模式強調倉儲一直保持跟蹤(參考),而面向持久化則強調采用 save()或者 put() 操作全量覆寫,本文的實作介紹角度不同,但效果差異不大,本文只對記憶體實作和關系型資料庫實作做區分,并希望在統一的角度做了一些解讀給讀者參考,但我認為讀者可以根據自己理解去側重選擇自己的實作,

3.1 記憶體倉儲

在《實作領域驅動設計》一書中,作者Vaughn Vernon提出一種面向集合的倉儲,我認為這其實就是一種完全面向記憶體實作的倉儲方式,在這種方式中,我們利用倉儲管理聚合物體的生命周期中間程序其實和使用框架集合(Collection)是一樣的,我把書中的例子稍改動展現如下:

public class CalendarRepository extends HashMap{
  
  private Map<CalendarId,Calendar> calendars;
  
  public CalendarRepository(){
    this.calendars = new HashMap<CalendarId,Calendars>();
  }
  
  public void add(Calendar aCalendar){
    this.calendars.put(aCalendar.getId,aCalendar);
  }
  
  public Calendar findCalendars(CalendarId calendarId){
    return this.calendars.get(calendarId);
  }
  
}
熟悉編程的人員很簡單就知道這是怎么一回事了,這個實作也很能表達從領域模型的角度看倉儲應該是怎么樣子的,我總結了該實作特點如下:
  • 倉儲應該是一個集合實體,而且無法對倉儲進行重復的放置;

  • 從倉儲獲取的聚合實體,應當和放置倉儲的實體具有完全一樣的狀態,在這里是原物件;

  • 如果在倉儲之外對聚合實體進行了修改,無需“重新保存”聚合實體;

  • 這種倉儲下的聚合物體,看起來更加像資源Resource;

抹去參考的創新改進:Vaughn Vernon的這個例子完全決議了倉儲應有的樣子,但即使純記憶體實作也不得不融入了實作的特性——倉儲完全持有集合,這種持有參考特性幾乎對領域無影響,但我還想試圖把這種實作特性抹掉,對比 2.2中間程序的倉儲物體轉移一小節中,當取出資源后,集合不應該再擁有聚合物體,所以按照這種思路進行,findCalendars方法還應該加上邏輯移除Calendar聚合的實作,如下面代碼所示,但這樣完全模擬有什么好處呢?這是一個好問題,因為我們的選擇必須要權衡其中得失,繼續往下看一下不這樣做引起的并發沖突問題......
public class CalendarRepository extends HashMap{
  //存聚合物體
  private Map<CalendarId,Calendar> calendars;
  //標記物體被邏輯移除
  private Map<CalendarId,Thread> calendarsTakenAway;
  
  public CalendarRepository(){
    this.calendars = new HashMap<CalendarId,Calendars>();
  }
  
  public synchronized void add(Calendar aCalendar){
    this.calendars.put(aCalendar.getId,aCalendar);
    //移除邏輯洗掉
    calendarsTakenAway.remove(aCalendar.getId)
  }
  
  //注意我們改了命名方法,變為了take,獲取,體現倉儲不再擁有物體
  public synchronized Calendar takeCalendars(CalendarId calendarId){
    //如果已經被取過,無法再取
    if(calendarsTakenAway.containsKey(calendarId)){
      return null;
    }
    Calendar calendar = this.calendars.get(calendarId);
    //邏輯洗掉
    calendarsTakenAway.put(calendarId,Thread.currentThread());
    return calendar;
  }
  
}
考慮并發:在領域角度,在同一個時刻沒有有兩個人可以同時在一個倉庫中獲取到同一件商品,但在計算機解空間中可以,所以計算機解空間會出現并發問題,為了解決并發問題,我們可以使用以下方式
  • 悲觀鎖:在一個調度者(執行緒)使用該聚合物體前,先對聚合物體進行加鎖,其他調度者則無法獲取物體進行操作

    • 阻塞悲觀鎖:如果調度者發現聚合物體被鎖了之后,則停止調度直到等待得到物體鎖后繼續;

    • 非阻塞悲觀鎖:如果調度者發現聚合物體被鎖了之后,不等待鎖,立即回傳做其他用例;

  • 樂觀鎖:一個調度者認為沖突可能性不大,所以可以先獲取聚合物體進行事務操作,但是當它想把聚合持久化的時候,發現有人操作過這個聚合,則回滾自己所有的操作,

采用哪一種操作完全取決于軟體開發人員,這個時候要求我們對程式架構設計和運作方式有著充分的了解,但是我們可以看到,其實用到了倉儲物體轉移這種完全模擬真實的領域問題空間的實作,剛剛好就是非阻塞悲觀鎖,只要是findCalendars方法洗掉找到的Calendar物體是原子性的操作,其他執行緒則無法獲取到物體,那么我們便不需要考慮重新設計一個新的鎖方案,如果你不是為了性能等其他因素非要領域模型妥協或者你剛好選擇的就是非阻塞性悲觀鎖,那么這種實作將會大大簡化你的程式代碼重量,也能讓客戶了解你的模型運作機制,使得該程序也做到了統一語言,
即使是我們常用的樂觀鎖,在資料庫倉儲下倉儲物體轉移也非常適用,最后明確一下,做到統一語言,回歸領域本質的意義非常大,它是領域驅動設計應付軟體復雜之道的核心理論基礎,它要求我們抓住問題的本質復雜度,盡量排除因計算機技術方案引入的偶然復雜度,從而實作軟體的架構價值,獲取長遠的軟體效益,

 

3.2 關系型資料庫倉儲

DAO和倉儲思維差異:正如本文開篇中的第一段話所參考,我們程式員通常會在實作技術的程序中,把關注模型的想法早早拋之腦后,這是可以理解的,我們在入門該科學所接受的基礎學習讓我們的思維很大程度上固化為面向計算機技術的開發,卻往往沒注意到,軟體工程的設計建模更應該關注的是模型,DAO和倉儲正是這兩種差異的產物,本文不會決議DAO和DO之類的概念,因為讀到這里的讀者,對他們的了解應當是非常專業的,
DAO和倉儲實作差異:先引出一個例子:我們有一個主任務TaskA和兩個子任務subTaskB,subTaskC,這三個物體都有一個叫state的狀態欄位,我們有一個業務規則是:所有子任務物體的狀態都是FINISHED,那么就把TaskA物體的state設定為FINISHED,但是外部事件是一個一個子任務回傳回來的,我們接下來看不同思維的實作,
面向資料的開發思維,使用關系型資料庫實作倉儲的時候,我們對資料表有插入、更新、洗掉、查詢四種主要操作,而且在面向資料模型開發的時候,服務類本身明確知道自己是在做哪一步操作,所以面向資料模型的開發經常會寫這樣的代碼:
public class BusinessService {
  
  @Resource
  private TaskDao taskDao;
  
  @Resource
  private SubTaskDao subTaskDao;
  
  @Transactional
  public void onFinished(String subTaskId,String taskId){
    //查出所有子任務
    List<SubTask> subTasks = subTaskDao.getAllSubTask(taskId);
    //找出回傳的子任務
    SubTask callBackTask = subTasks.stream()
      .filter(e->subTaskId.equals(e.getSubTaskId)).findAny();
    //更新子任務狀態
    callBackTask.setFinished(true);
    //如果所有子任務完成,更新主任務狀態
    if(allFinished(subTasks)){
       taskDao.updateStateById(taskId,TaskStatusEnum.FINISHED);
    }
    //更新一個欄位
    subTaskDao.updateStateById(subTaskId,TaskStatusEnum.FINISHED);
  }
  
}
上面的代碼,用例服務知道自己要更新什么欄位,并自行去做了更新,但當我們關注模型思維用到倉儲的之后,針對以上的功能實作用例服務就不應該關注到更新哪一個欄位這個和持久化相關的操作,而是讓倉儲需要自行去對比,哪些欄位變化了,然后更新到資料庫中去,用例服務會是下面所示的樣子:
public class BusinessDomainService {
  
  public void onFinished(String subTaskId,String taskId){
    //獲取物體的時候記錄快照
    Task task = DomainRepository.getInstance().taskOf(taskId);
    //聚合物體負責業務邏輯
    task.subTaskFinished(subTaskId);
    //倉儲自己識別到底哪個欄位變化了,然后更新該欄位(簡稱diff)
    DomainRepository.getInstance().put(task);
  }
  
}

public class Task {
  
    private List<SubTask> subTasks;
  
    private TaskStatusEnum status;
    
    public void subTaskFinished(subTaskId){
      //找出回傳的子任務
      SubTask callBackTask = subTasks.stream()
        .filter(e->subTaskId.equals(e.getSubTaskId)).findAny();
      //更新子任務狀態
      callBackTask.setFinished(true);
      //如果所有子任務完成,更新主任務狀態
      if(allFinished(subTasks)){
         status = TaskStatusEnum.FINISHED;
      }
    }
}
以上就是面向資料開發和面向領域模型的倉儲開發的差別,那么這樣的例子應該選擇哪一種實作最好呢?這個問題不好回答,既然是DDD那只能選擇倉儲,這基本涉及的是系統如何設計的問題,簡單的系統選擇面向資料開發是簡單直接的,你應該在什么時候使用「領域驅動設計」這種倉儲設計思想,別忘記了它的作用:復雜性軟體應對之道,
復雜的聚合根物體:如果你的資料欄位是有限的,但是物體變化的規則是多種多樣的,那么實作自動更新模式將得到好處,假設我們一個物體有20個欄位,那么我們 diff 20個欄位的代碼必然比寫不知道多少個由這20個欄位組成的組合介面要強,另一方面,比較可怕的是,有可能用例程序本身根本不知道一個要更新的物體哪些欄位發生了變化,為了說明這些情況,我們不得不提一下聚合根的另外一些特點,
  • 聚合內部一致性:聚合根的存在,最主要是的封裝和管理聚合內部各種物體的關聯和耦合,包括代碼耦合和資料耦合,所以上面的task本身持有所有subTask的參考,而且負責subTask和task的state狀態業務規則一致,此時,這個事務處理程序,就無法感知Task封裝的一致性邏輯是否由subTask引起了Task物體自身的狀態變化成為FINISHED,所以diff的實作就很有必要,
圖片
  • 領域服務的純粹性:如上圖所示,因為設定Task的狀態規則是由聚合根負責,所以領域服務是不感知的,必須要靠diff,但是如果把diff這個邏輯寫在領域服務中,不如把邏輯寫在倉儲中,因為我們也不應該讓領域服務去關注一些技術上的邏輯,增加領域服務邏輯的復雜性,其實這樣做,剛好就是倉儲本身的職責,封裝diff后的倉儲讓領域服務感覺到聚合物體一直在記憶體中一樣,

  • 聚合根的重建工序:在DAO中,我們可以直接方便從ORM框架中回傳資料物件,但是聚合根卻不能,因為聚合根是由多個DO組成的,我們的持久化中間件(不管是MySQL關系型還是MongoDB檔案型)無法給我們回傳一個聚合根物體,所以倉儲還得老老實實的把ORM中獲取到的DO組裝為Entity和Value Object,且要保證查找到的物體是要和原來的物體一摸一樣的,這意味著需要“重建”物體的操作;

    • 拆建規則(Convertor):倉儲應當知道怎么拆,就應該怎么復原,所以它應該有一套拆解和重建規則,并根據此規則進行復原,Convertor是維護這種規則的一種工具,我建議采用這種命名類封裝拆建規則

    • 事件溯源(Event Sourcing):還有一種重建工廠的實作是利用物體的快照+物體的領域事件集合回放來恢復聚合物體,有興趣的同學可以了解一下事件溯源;

    • 聚合根與關聯單例:關聯單例是一種特殊的重建工序,我用一個領域事件監聽器來說明,例如我們的聚合根物體實作了觀察者模式,聚合根為主題,內部持有一些單例監聽器物件串列,其中一個監聽器用作監聽聚合根的狀態變化發送領域事件,那么這個監聽器也應該讓倉儲負責拆解和恢復,

以上的幾種特性,都意味著關系型資料庫倉儲的實作都會比較復雜,但這種復雜換來的是我們領域模型的干凈,當軟體系統的復雜度提升,面向資料的開發所帶來的偶然復雜度是指數級別的,所以這個時候我們就能感受到倉儲的復雜性付出是值得的,最后我們列舉一下對比DAO,倉儲的缺點:
  • 實作復雜:因為聚合的復雜性所以我們其實作起來也非常困難,其中最好模型能配合實作這種復雜性,

  • 犯錯成本:正如DAO的某個介面只對一個屬性更新,那么無論代碼有何種bug,最多只會寫錯一個欄位,但倉儲全量化更新后,我們在未知情況下手一抖,那么將可能覆寫其他本應安全欄位,所以這也提高了我們的犯錯成本,斷言是解決的一種較好方案

關系型倉儲實作方案:倉儲必須要讓客戶感覺它似乎就一直在記憶體中一樣;但上面提到的 Diff 邏輯讓倉儲的使用和實作變得困難,設計者需要在整個背景關系角度了解倉儲的原理細節,因為要追求性能和安全的實作,還要只針對已經變化的欄位更新,忽略無變化欄位,其中Vaughn Vernon在《實作領域驅動設計》里面提到了兩個方法,來解決這個問題:

  • 隱式讀時復制:在查找聚合物體的時候,記錄下聚合物體的所有狀態,然后在更新的時候,用新狀態diff舊的狀態,只對特定欄位進行更新;

  • 隱式寫時復制:在查找到集合物體的時候,倉儲把聚合物體的更新操作隱式委派給倉儲的某種機制進行,所以每次更新狀態物體狀態倉儲都能跟蹤到,并在這個時候對該值標記為臟資料,最后倉儲在事務結束的時候把臟資料給刷盤,

public interface TaskRepository{
  
    //相當于findTask,獲取到的Task會被隱式追蹤復制
    public Task taskOf(String taskId);
    
    public void addTask(Task task);
  
    public void removeTask(String taskId);
    //其他/統計/集合操作等
    //......
}
看上面代碼,在獲取方法 taskOf() 中,倉儲負責開始對物體進行跟蹤,因為外界呼叫方不感知倉儲在跟蹤物體,所以稱之為隱式,我們可以根據聚合的不同構成自行實作以上提供的兩種隱式跟蹤的方案的一種,如果是追求性能那么寫時復制比較好,如果是采用讀時復制,那么Javers開源框架會是一個比較好的選擇,但記得一定要做好單測,
以上兩種方案其實都是對物體進行狀態跟蹤,但要注意的是在介紹這兩種方案的時候,Vaughn是打算讓倉儲往面向集合倉儲的思路走的(該方法被他歸到面向集合一章),雖然以上兩種隱式方案是非常好的實踐,但我認為還是可以像在面向記憶體倉儲一節提到的一樣,繼續引入創新改進為倉儲物體轉移模型,現在我們看一下關系型資料庫倉儲該如何應對這種模型,
抹去跟蹤的創新改進:我們上面提到了,倉儲物體轉移模式下,倉儲實則只有兩種主要操作,一個是放置聚合物體,一個是獲取(Take)聚合物體,獲取到物體后,倉儲將不再擁有物體管理權限,在面向記憶體的倉儲實作中,我們只需在take方法中remove掉物體即可,但是持久化下的這種倉儲模式該如何實作、又有什么特點呢?
很簡單,只要我們在原來的基礎上,讓倉儲把插入更新(即上面的跟蹤)操作封裝為一個操作put(也可以用save),然后讓find操作不變,直接命名為take,讓領域服務認為倉儲實際上已經沒有物體即可完成倉儲物體轉移模式,決議如下:
  • 領域服務視覺:在獲取(take)到聚合物體后,領域服務可以認為倉儲中的聚合物體是不存在的(即使倉儲沒有洗掉聚合物體);

  • 合并插入和更新(全覆寫):倉儲沒有所謂的更新操作,只有直接放置聚合物體到倉儲中,可以讓倉儲判斷該插入還是全量更新(其實和用隱式跟蹤實作部分更新差別不大,隱式跟蹤更安全但多一個復制操作),或者我們直接一點,完全洗掉物體后再次插入或者全覆寫物體;

  • 洗掉:不管是否改進模型,當聚合物體生命周期結束都需要去真正的洗掉物體,這一點確實不好統一;

  • 樂觀鎖:我們可以在實作的時候在關系型倉儲中采用樂觀鎖保證一個聚合物體不會存在于不同的領域事務中,因為樂觀鎖只會讓其中一個成功;
圖片
在Vaughn的書中介紹,隱式讀時/寫時跟蹤是做成面向集合的Repository,而另外用面向聚合的資料庫(Aggregation-Oriented DataBase)來表達他的面向持久化Repository,不知道讀者是否能Get到其實關系型資料庫實作的倉儲物體轉移模式,正是關系型資料庫下的面向持久化的Repository,
  • 優點:所以它最大的優點就是無需跟蹤物體,而是以轉移的聚合物體為主;

  • 缺點:因為倉儲實作要全量覆寫整個聚合狀態,所以只適合用在類檔案資料庫,對于關系型資料庫則需要復雜的隱式讀/寫跟蹤了;

關系型倉儲總結:但確實不同的實作倉儲表現出了不同的特點,所以不管用何種實作,我們都需要了解倉儲的使用方法,不然是無法正確使用倉儲的,下面給一個圖大概描述一下關系型資料庫持久化倉儲的功能和內部結構:
  • 訪問物件DAO:可以封裝一層Mapper,或者其他ORM框架,提供DO以及其他統計資料;

  • Convertor:維護拆解規則和重建規則,同時復制聚合根監聽器的一些組裝;

  • DO:資料物件,一般和關系型資料表一一對應;

  • 隱式狀態跟蹤:實作一套隱式讀時復制和隱式寫時復制狀態跟蹤的邏輯;
圖片
當性能不是很重要而且代碼比較重視質量的時候,我比較推薦推薦在領域服務結束之前,都要把聚合物體回歸倉儲,然后用樂觀鎖把整個聚合物體替換掉倉儲實作中的聚合物體,在開發規范約束、統一語言倍訓的情況下,我們有了這條默契的規則,就不用擔心這種漏掉持久化實作的問題,也無需考慮我們到底是插入還是更新,

3.3 倉儲的架構

倉儲層(資源層):我們提到,中間程序是不歸領域模型關注的,我們屏蔽了中間程序提供了倉儲的領域概念,那么顯然倉儲是領域模型關注的,這就涉及一個耦合以及依賴的問題,其中最自然的依賴就是我們的領域服務,要依賴倉儲,而倉儲要依賴資料庫、記憶體等具體的實作工具去做真正的中間程序狀態維護(持久化),如下圖所示(圖中連線代表依賴關系):
圖片
如此,在代碼實作上,必然很容易讓領域模型對資料庫、記憶體等這里基礎設施的代碼產生依賴,從而讓基礎設施的概念入侵到領域模型變得容易,我們習慣于面向資料和程序的開發,當這類代碼和領域模型的代碼界限變得沒那么明顯的時候,聚焦于模型也容易被破壞,倒置依賴整潔架構分層給了我們解決這個問題很好的實踐,我們可以把倉儲的行為抽象為基本的介面,然后利用控制反轉,把實作該節點的倉儲注入領域模型的運行態中,實作了倒置依賴的依賴圖如下:
圖片
應用了依賴倒置,把所有的倉儲都在一個命名空間(模塊)中管理,就形成了我們熟知的倉儲層(也叫資源層),
四、結束語
對Repository的認知其實和對DDD思維的認識是統一的,他們都是從領域專家角度去對解決方案進行建模,倉儲為聚合根在領域知識和工程知識之間做了隔離,并為技術實作提供了統一的概念抽象,這樣的模式和例子在DDD中是經常有的,例如:防腐層也是其中的一種,他們都是為了保持領域模型的純粹性作出了自己的努力,最后由于篇幅問題簡要提一下倉儲的一些我還能想到的關注點:
倉儲與事務:聚合根是事務修改的基本單元;所以倉儲其實也是隱藏著一個事務原子化的能力,我們通常資料庫事務的實作要控制在應用層,但有時候會遇到大事務問題或者兩階段提交的問題,所以有極端情況下把事務用一個領域概念進入領域層,從而讓倉儲層的實作來反轉控制事務也不失為一個好選擇,這種打破原則的事情也要求我們理解原則,
倉儲與值物件:值物件可以很簡單,就一個數字,也可以很復雜,如一個完備的Domain Primitive概念,我們的物體擁有值物件,所以Repository也是要負責值物件的持久化,這點的處理也是非常值得大家去注意的點,讀者在實戰中處理的值物件的時候更需要豐富的經驗去取舍設計方案,
倉儲的設計和實作十分的復雜,我們很難在節奏比較快的開發迭代中去完成業務不關注的這種設計方式,這或許要求我們在每一次不同的迭代中去慢慢完成一個倉儲,這個時候代碼實作的倉儲有多丑陋不重要,或者重要的是你心中有一個成型的倉儲,它始侄訓跟著你的每一次改進被沉淀、演進,這就是為什么我們要去理解倉儲存在的意義和本質,開發者如何去看待一個系統的各個構件,最終系統就會被開發成什么樣子,

參考書籍:

《領域驅動設計》Eric Evans [著].趙俐[譯]2016.. 人民郵電出版社

《實作領域驅動設計》Vaughn Vernon[著].滕云[譯].2014.中國工信出版集團
作者|范燦華(少嵐)

本文來自博客園,作者:古道輕風,轉載請注明原文鏈接:https://www.cnblogs.com/88223100/p/DDD_in-depth-design-and-implementation-of-repository-from-a-domain-perspective.html

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/542972.html

標籤:其他

上一篇:函式式編程思維讀后總結與感想

下一篇:keycloak~JWT各欄位說明及擴展欄位的方法

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more