DDD 之 Repository 對象生命週期管理

在 DDD 中 Repository 是一個相當重要的概念。聚合是戰略與戰術之間的交匯點。而管理聚合的正是 Repository。

因爲從戰略層次,分而治之,我們會按領域、子域、界限上下文、聚合逐步劃分降低系統複雜度;從戰術層次,我們會從實體、值對象、聚合逐步歸併,匯合。

也因此有人解析 DDD 關鍵就是兩個字:分與合,分是手段,合是目的

之前寫的《DDD 之 Repository》[1],大致介紹了 Repository 作用。

一是從 “硬件”、“軟件”、“固件” 闡述了 Repository 的必要性,相對 DAO,具有更高抽象,不再關心數據是從 db,cache 甚至第三方系統,Repository 管理着數據在存檔態與活躍態之間的轉換

二是 Respository 與 Domain Service 之間的調用關係

雖然解釋了不少,但也有些問題沒有闡述清楚,借這篇再進一步詳情補充說明下

常見的兩個錯誤:

1、領域模型中不能出現技術詞,所以在設計模型時,不要出現 DAO 之類的技術詞。而在 DDD 中提出了 Repository,一是從 DDD 統一語言角度,數據具體技術存儲抽象爲 Repository;二是 Repsotiory 也表達模型概念。

2、Repository 是 DDD 中作爲 DAO 的替身,換湯不換藥,所以從以前的 XXXDao, 變成了 XXXRepository,然而 Repository 在 DDD 中並不是這麼簡單,它管理着聚合的生命週期,而其他實體對象由對應的聚合對象管理。

對於第一點,再詳述下。在面向對象中有兩種對象邏輯:單對象邏輯和集合對象邏輯。

如單純的 User 對象,還有表示 Collection users 邏輯,如年齡最大最小的用戶,是集合邏輯

如果把 Collection 變成 PersistentCollection,就是 DB。

再進一步,看個小示例, 一個辦事處有很多的員工,以往模型表達爲:

class Office {
    List<User> users;
}

換一種方式:

class Office {
    Users users;
}

使用 Users 來表達集合對象,這樣原先使用 List 不能表達的模型,在抽象集合對象 Users 上可以很好的表達出來。

而 Repository 就是代表了一種集合領域邏輯,如我們直接把 UserRespository 想像爲 Users 處理。

使用這種方式一是能更好地表達模型,二也能解決在《處理業務邏輯的三種方式》[2] 中提到的性能問題。

性能與模型的選擇其實是在實踐 DDD 過程中很多人的攔路虎。

如上面的 Office 對象,如果使用 List 來表達 users 集合數據,那當加載 Office 對象,users 是不是必須加載出來,從模型完整性角度必須得加載出來,但加載出來必須帶來性能損耗,如果 users 數量很大,不借助類似 hibernate 提供的懶加載機制來規避 N+1 帶來的性能損耗,這個模型根本不可行。

這也是 Repository 不能按 DDD 原意來落地的原因。

進一步思考,其實上面的原因只是表象,背後是生命週期的管理。

生命週期管理

不論是設計,還是性能,對於聚合,除了顯現的要求是聚合內的數據一致性。在數據庫體系中,我們都是使用事務一致性來管理一致性和完整性。也是變相得把實體一致性與事務一致性兩者的邊界在同一邊界上。

還有隱含的構建關係和級聯生命週期

比如:Order 與 OrderItem

創建:

Order {
    List<OrderItem> items;
    public OrderItem newItem(String itemName,String price){
        OrderItem item = new OrderItem(this);
        items.add(item);
        return item;
    }
}

那麼當 domain service 去處理 Order 業務時

OrderService {
    void doOrder() {
        Order order = new Order(orderId);
        // order do something
        order.newItem(name,price);
        orderRepository.save(order);
    }
}

當 orderRepository.save() 時,不僅讓 order 從活躍態變成持久態,還會把 orderItem 也由活躍態變成持久態

當 orderRepository.delete() 時,也不僅刪除了 order,也得刪除 orderItem。才能保持一致性。

自然讀取 Order 時,orderItems 也得加載完整,保持模型的完整性。

這就是構建關係與級聯生命週期。

怎麼處理呢?

大致有三種方法

技術手段

在《DDD 之 Repository》[3] 提到的對象追蹤,其實有很多的名字,也有叫 Dirty Tracking

再配合延遲加載技術,達到了我們的目標:模型完整,落地可控。

失聯領域 disconnected aggregate

Order {
    List<Item> items;
    addItem(amount) {
      Item item = new Item(orderId,amount)
      items.add(item)
      this.jpa.insert(item);
    }
}

在《IDDD》也提到,在聚合中使用 repository 來操作聚合。但不推薦,這只是延遲加載的一種形式

把聚合看作一個整體,不用關心聚合內實體的改變,將所有改變,看作是聚合本身的改變。

在《IDDD》中也不推薦這樣,給出的做法是在調用聚合方法前,先取出所需要的實體,也就是像在上述文章中所講:Domain service 不要依賴 Repository。可以在 application service 裏通過 repository 查出需要數據,再傳給 domain service,讓 domain service 變得無狀態。

但這種方式,看着是個方法,但實踐時,有違直覺。什麼意思呢?就是 Aggregate 依賴了 Repository。相當於實體依賴了 DAO,是不是很不應該?

其實 domain service, entity, repository 都屬於 domain 層,那爲什麼同一層的類不能相互調用呢?

制定規則是來協調處理複雜性,都是基於認知或經驗制定的,而不是爲了規則而規則

既然我們的認知是他們都在一層,應該可以調用,憑什麼不能調用,不違背降低複雜性的前提下不要特意限制。

上文講過 Repository 其實包含了一種集合邏輯,那我們把 OrderRepository 變名爲 Orders,也是一樣的。

那麼下面的代碼是沒有毛病的

User {
   List<User> users; 
}

把 List 抽象成 Users 集合對象

Users implements List<User> {
}

到這兒,自然第一段代碼,就變成了

User {
    Users users;
}

這樣寫,是不是也沒毛病?把 Users 再替換成 Repository

User {
    UserRepository repository;
}

是不是也沒問題了,也就是 User 依賴了 UserRepository。

由上面四段小代碼,推導出了 User 依賴 UserRepository 的合理性與可行性,只是平常被 DAO 方式習慣了,以致於心理上有點彆扭而已。這也變相說明了 Repository 不是 DAO。

再進一步:

Orders {
    void addOrder(order) {
      this.dao.insert(order);
    }
}

這段代碼,如果沒有使用 jpa,orm 框架,也是有問題的。

爲何?破壞了封裝性

因爲在 dao.insert 裏面必然會暴露 order 的內部數據

OrderDao {
    void insert(order) {
        db.execute(order.getId(),order.getTime(),...);
    }
}

我們使用對象建模,就是把業務邏輯 建模爲數據變化,然後把數據的改變和改變數據的行爲放一起

不同於面向過程是建模業務流程。

數據變化,以及生命週期變化是業務的核心邏輯。

對象狀態變化來自隊列和緩存,那麼也要被 domain 封裝對象生命週期。

因此代碼得這樣寫,纔不被破壞封裝性:

Order {
   void save(OrderRepo) {
        orderRepo.save(thid.id,thid.time,...);
   }
}

repo.save(order) 與 order.save(repo) 兩種寫法看似簡單,背後的思想卻讓人的思考變得如此膚淺。

前一種寫法,如果不與 orm 綁定,會造成封裝性破壞,而且會從充血模型變成了貧血模型,table module[4]

後一種寫法,在不與 orm 綁定前提下保護了封裝性,但 save 行爲賦給了當前對象,這是在面向對象早期流行的真實世界建模。

不管怎麼寫,從活躍態到歸檔態是很重要的行爲,因爲數據一致性是業務邏輯的核心。也說明了不管如何建模,都要考慮到技術實現,domain 不是一片靜土,沒有約束的理想化實現,而是受特定技術棧與技術生態環境約束的實現。所以在分層時,有人認爲基礎設施層不是層的原因。

關聯對象 association object

除了上面兩種方式,還有在《分析模式》中提到的關聯對象模式。

關聯對象,顧名思義,就是將對象間的關聯關係直接建模出來,然後再通過接口與抽象的隔離,把具體技術實現細節封裝到接口的實現中。這樣既可以保證概念上的統一,又能夠避免技術實現上的限制。

總結

DDD 中實體大致分成了兩種:一是聚合根,二是聚合內實體。兩者的生命週期管理也不一樣,聚合根由 repository 管理,而其他實體由聚合根管理。

因此當在創建聚合根的時候,聚合根與其內部實體的生命週期有級聯關係。通過三種方式可以實現這種級聯關係。不管是何方式,要達到的目標:一是數據一致性,二是模型顯現表達出來。

References

[1] 《DDD 之 Repository》: https://www.zhuxingsheng.com/blog/ddd-repository.html
[2] 《處理業務邏輯的三種方式》: https://www.zhuxingsheng.com/blog/three-ways-to-implement-business-logic-transaction-script-anemia-model-and-ddd.html
[3] 《DDD 之 Repository》: https://www.zhuxingsheng.com/blog/ddd-repository.html
[4] table module: https://www.zhuxingsheng.com/blog/three-ways-to-implement-business-logic-transaction-script-anemia-model-and-ddd.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/J5nqE-mQqTGXAeiTCLQiwg