l

2020年8月18日 星期二

領域驅動設計學習筆記(10):實作Repository

August 18 10:00~13:35


背景介紹

在《Clean Architecture》書中建議使用Gateway來存取物件,透過依賴反轉原則,避免領域物件(Domain Model,在Clean Architecture中稱為Entity Layer)與使用案例層對資料存取層產生直接依賴。

Gateway是一個比較抽象的名詞,Teddy在實作Clean Architecture的同時也套用了領域驅動設計(Domain-Driven Design;DDD),因此採用領域驅動設計所建議的Repository設計模式來實作Gateway。

在DDD中,每一個Aggregate會對應到一個Repository負責物件持久化的工作。

接下來討論幾種實作Repository的方法,以及各自的優缺點。

***

Take 1,一對一

實作Repository最直接的方式就是先幫每一個Aggregate宣告一個相對應的Repository介面,然後實作這些介面。圖1為Clean Kanban系統的三個Repository(Teddy開發的看板系統)。

▲圖1:一個Aggregate對應一個Repository

Teddy幫每一個Repository提供兩個版本的實作物件:

  • InMemoryRepository:把Aggregarte儲存在記憶體中,方便測試使用,如圖2所示。
  • SerializableRepository:透過Java的serialization功能直接把物件儲存在檔案中,如圖3所示。

▲圖2:ImMemoryBoardRepository


▲圖3:SerializableBoardRepository

第一種方法最直接也很簡單,相信大家都看得懂。

***

Take 2,泛型

看到這裡鄉民們應該會想:「每一個Aggregate都宣告一個,Repository介面,這樣太麻煩了,怎麼不用泛型(generic programming)?」


▲圖4:採用泛型Repository介面


圖4為採用泛型的Repository介面,之後只要需一個InMemoryRepository實作便可讓所有的Aggregate使用,如圖5所示。

▲圖5:採用泛型的InMemoryRepository

***

Take 3,使用JPA

前兩種方式都需要自己撰寫實作Repository的程式碼,如果想偷懶使用Java 持久化 API (Java Persistence API;JPA),讓Hibernate自動產生Repository實作。


▲圖6:使用JPA讓SpringBoot透過Hibernate自動產生Repository實作

這個做法省去了自己撰寫操作資料庫程式的麻煩,但存在三個問題:

  • 有些資料庫操作沒有定義在標準的Repository介面裡面,需要自己撰寫Java持久化查詢語言 (Java Persistence Query Language;JPQL)客製化查詢條件。例如圖6的WorkflowRepository繼承自CrudRepository介面,而CrudRepository只有findById,可以透過workflow id 找到workflow。如果需要透過board id找到workflow,就需要自己增加一個getWorkflowsById的方法,然後用@Query annotation來撰寫自訂查詢條件。這原本也不是什麼大問題,但如果查詢結果無法完全用JPQL表達,而是需要在從資料庫回傳資料後再透過Repository處理,那就沒辦法了(因為WorkflowRepository只是一個interface,它的實作是程式執行時動態產生)。
  • 第二個問題來自於Clean Architecture,Repository儲存的對象是Aggregate,也就是圖6中的Workflow。但直接把位於Entity Layer的Workflow物件丟到最外層的DB & Drivers,違反了Clean Architecture的跨層原則。
  • 第三個問題也是因為套用Clean Architecture才會有的困擾。為了讓JPA自動產生資料庫對應程式碼,必須在Entity(domain model裡面的entity類別)貼上JPA的@Entity annotation,如圖7所示。雖然@Entity是JPA標準的annotation,但JDK並沒有內建這些annotation。為了使用它必須要加入對框架的相依性,例如要引用Hibernate。如此一來,便違反了Clean Architecture的相依性原則:「高層不可以相依於底層(原始碼的相依性只可由外往內,由底層往高層)」。此外,在domain model的entity類別身上貼了一堆「狗皮膏藥(annotation)」,對於有潔癖的人來講,也是無法接受的一件事XD。


▲圖7:貼上@Entity讓JPA知道這是一個需要持久化的物件

***

Take 4,Bridge設計模式

讓JPA直接產生Repository實作雖然方便,但卻少了一些自行撰寫程式的彈性,請參考圖8。

▲圖8:Client直接相依於Repository介面


這個問題的根本原因是我們把JPA所產生的Repository直接丟給客戶端使用,如果可以在客戶端和Repository之間增加一個間接層,就可以透過這個間接層手動增加一些彈性的操作,請參考圖9。這個設計,就是Bridge設計模式。


▲圖9:在Client與JPA產生的Repository實作之間增加一層。原本的JPA Repository改名為RepositoryPeer,客戶端對應的是新增加的Repository介面。

***

最後實作程式碼如圖10所示。

▲圖10:套用Bridge設計模式之後的Repository。


眼尖的鄉民可能注意到圖10中的一些細節:

  • WorkflowRepository所儲存的物件類別是Workflow,但透過WorkflowRepositoryPeer儲存到資料庫裡面的是WorkflowData。這是為了不要將Entity層的Workflow直接傳到第四層的Database & Driver。
  • 為了把Workflow轉成WorkflowData,以及反之把WorkflowData轉成Workflow,還套用了Mapper設計模式。這個資料傳換由WorkflowRepositoryImpl所負責。這也是套用Bridge的好處,在呼叫peer之前以及之後,可以插入一些程式邏輯。
  • 因為最後儲存到資料庫的是WorkflowData類別,因此只要在WorkflowData類別貼JPA annotation即可(參考圖11),位於entity 層的Workflow保持乾淨的原貌。


▲圖10:WorkflowData類別

***

友藏內心獨白:終於寫完了。

3 則留言:

  1. 我覺得書中的跨層原則其實有點模糊空間,確實在 p. 207 中有這句話

    "We don't want to cheat and pass Entity objects or database rows."

    但我覺得重點是下一句

    "We don't want the data structure to have any kind of dependency that violates the Dependency Rule."

    也就是 p. 203 的

    "Source code dependencies must point only inward, toward higher-level policies"

    因此 database rows 確實不該傳入內層,但我覺得外層看到內層資料並不一定算是 dependency rule,方向依然像內,例如 entity 定義在最內層 (第一層),repository interface 定義在 use case 層 (第二層),repository 的實作則在第三層,其實 entity 也跨過第二層來到第三層。

    Entity 會到第四層,我覺得是 JPA/ORM 引起的,如沒有 JPA/ORM,資料庫的表格結構本來就和 Entity 是獨立的,至少我現在寫 JavaScript 沒有用任何 ORM,資料庫表格定義完全是靠 SQL migration script 建立,repository 就是負責將 entity 轉成 row,或是相反轉換。但說真的,在實務上,除了 naming convention 可能不同外,大多數的情況下 entity 的屬性都會在 database 找到對應的欄位。

    為了滿足跨層原則,像 p. 208 那樣針對 input/output 定義資料結構,但只要最核心的 entity 因為商業需求而變動,很難不會連帶影響到,最後就是 entity/input data/output data 都要改動,連鎖效應蠻大的,所以過去我通常只會刻意設計 input data,因為比較常和 entity 有差別 (像是少 id 欄位),然後直接把 entity 當成 output data 往外丟,省去一個 entity 轉 output data 的程式。

    最近想法有點改變,為了產生 OpenAPI 需要的 annotation,會定義 output data 的資料結構將 annotation 下在 output data 上,有點類似 WorkflowData 為了承接 JPA annotation 的角色。

    回覆刪除
  2. Repository Pattern 講的太美好,但很少人提到 Save() 這個 Method 到底怎麼實作。

    畢竟整個 Aggregate Root 傳進去,怎麼知道這個東西到底改了什麼、刪了什麼?我自己目前的臨時方法是參考自前端 Vuex 的 State Store,當 Aggregate Root 變更東西的時候,會往自己發送 Mutation 事件。

    Save() 執行的時候,再來看這個 Aggregate Root 有哪些 Mutations,例如 UserCreated, UsernameUpdated,這樣 Save() 就可以分別呼叫 SQL 的 INSERT INTO 然後再呼叫 UPDATE。

    回覆刪除
    回覆
    1. 我目前Save的實作方式,如果用state sourcing加 ORM,就交給ORM去儲存即可,不用自己下SQL。如果是Event Sourcing,就儲存domain event即可。

      刪除