天天看點

jpa specification groupby查詢_DDD 中的那些模式 — 使用 Specification 管理業務規則

許多開發者在項目中希望能夠使用 DDD 原因在于能夠管理業務的複雜度,避免在業務規則愈發複雜的情況下代碼以及架構發生腐化,最終變的難以維護。系統複雜度展現在多個層面,例如繁瑣的流程,繁複的校驗規則,資料的多樣性等,DDD 對于不同層面的複雜度提供了不同的應對模式,今天的文章會聚焦與如何使用 Specification 模式解決「業務規則」相關的複雜性。

業務規則

在介紹 Specification 模式之前,我們先明确一下什麼是「業務規則」。作為一個開發者,以下的這些一定是你日常工作中常見的工作。

  • 校驗業務對象的某些狀态是否合法,例如目前賬戶是否啟用,賬戶餘額是否充足,事故日期是否在保險單的有效時間内。
  • 從業務對象的集合中篩選出符合條件的結果集,例如從使用者的交易記錄中找出購買打折産品的記錄。
  • 檢查一個新建立的業務對象是否符合某些業務條件,例如一張新建立的訂單,它對應的客戶與商戶都應該是合法系統使用者。

為了友善後續的讨論,我們将業務規則的概念窄化為以上三種類型。接下來的問題是這些業務規則在系統中是如何實作的?

最原始的方法是編寫了許多小方法實作這些校驗或是篩選邏輯,分散在各個 Domain Service 類中。這樣做的缺點很明顯,其一是難以管理,當業務規則越來越多時,這些散落在各處的方法就無法複用,而開發人員也沒有辦法集中的管理這些方法。其二是丢失了業務知識,這些方法大部分都很短小,簡單,但是這些規則其實包含了大量的業務知識,如果任其分散在不同的 Domain Service 中,後續的開發過程中就很容易丢失這些業務知識。

在此基礎上的另一個方案也是實際項目中使用比較多的,即編寫各種不同的 Validator 類,每個 Validator 類中有大量對于領域對象的校驗方法。這種做法一定程度上解決了第一個問題,通過特定的類将檢驗方法集中起來,可以友善開發人員進行維護和擴充。下面是一個典型的 Validator 的示例代碼:

public class CustomerValidator {
    public static boolean isVIP(Customer customer) {
        ……
    }
}
           

但是這對于業務知識的傳遞揖讓沒有太大的幫助。Validator 類更像是一些工具類,和領域層并沒有什麼關聯。而當需要對這些校驗方式進行複用時,特别是将幾個校驗規則按照

and

or

這樣的邏輯關系組合起來時,Validator 就支援不是那麼好了。

另一種方法是是将這些校驗的規則在領域對象中實作,作為該領域對象的一個方法。參考如下的代碼:

public class Customer {
    public boolean isVIP() {
        ……
    }
}
           

這總做法優點是校驗規則與領域對象結合的非常緊密,開發人員一看就明白。但是缺點同樣明顯,就是你的領域對象會變得日益臃腫,充斥着大量類似這種的校驗方法掩蓋了核心的業務規則。

那麼有沒有更好些的做法呢?Specification 模式提供了一種不錯的選擇。

Specification 模式

DDD 中認為這些規則都是純粹的「動詞」,是以需要單獨的建立模型,而這些模型都應該是簡單的「值對象」。一個 Specification 接口示例如下:

public interface Specification<T> {
    boolean isMatch(T domainObject);
}
           

然後我們可以實作是否 VIP 客戶的校驗:

public class VIPCustomerSpecification<Customer> {
    @Override
    public boolean isMatch(Customer customer) {
        ……
    }
}
           

通過實作

Specification

接口,我們可以對不同的領域對象擴充不同的校驗邏輯,而這些類都是可以複用的。同時這些

Specification

可以作為基礎元素進行任意的組合,組合更為複雜的校驗規則與篩選邏輯。例如下面的例子中,我們将所有更上層的領域邏輯封裝在

CustomerSpecifications

中,而它可以通過組合各個單獨的

Specification

提供具體的功能。

public class CustomerSpecification {
    public boolean isSpecialCustomer(List<Specification<Customer>> specifications, Customer customer) {
        ……
    }
 }
           

在上面的

isSpecialCustomer

的方法中可以傳入校驗所需的一系列

Specification

,并依次校驗,這種做法也便于擴充與複用,與領域模型結合的更為緊密。

使用 Specification 模式過濾資料

從一個資料集中篩選出符合條件的結果也是日常開發中常常需要實作的業務規則。那麼我們一般的做法是如何的呢?

假設我們需要篩選出某個客戶名下在一月到二月的訂單記錄,一種最簡單也是最常見的做法就是通過 SQL(假設這些資料都是存放在關系型資料庫中)。例如通過如下的 SQL 查詢:

select * from t_order where t_order.customer_id = ? and t_order.created_at >= ? and t_order.created_at <= ?
           

使用 SQL 在查詢中直接實作篩選邏輯看起來很自然,但問題是這部分的邏輯本來應該是屬于領域層的,現在卻洩漏到了資料層,造成的後果就是維護的難度大大提升,很多業務系統到後期都是在和大段大段的 SQL 做鬥争,而應該編寫邏輯的 Service 層,Domain 層都成了擺設,退化成了純粹的資料對象,傳來傳去,與 DTO 沒什麼差别。而 Specification 模式可以提供一種不錯的解決思路。

我們可以在有如下的代碼:

public class OrderSpecifications {
    public Specification<Order> inPeriod(LocalDateTime beginTime, LocalDateTime endTime) {
        ……
    }
}
           

在這裡

OrderSpecifications

并沒有直接進行資料篩選,而是通過輸入參數建立了一個特有的

Specification

對象,然後由

OrderRepository

對象接受

Specification

為參數進行真正的資料篩選操作。這樣就将查詢與過濾的邏輯分開了。

此時需要考慮的問題是性能,如果按照 SQL 的做法,那麼在資料庫端就會完成資料的查詢與過濾,傳回給應用端的資料量不會很大,但是如果使用 Specification 模式那麼,就是在記憶體中進行過濾了,在資料量大的情況下必然會遭遇性能的問題。之前在項目中的确也遇到過類似的問題,由于查詢結果資料量過大,而且需要分頁展示,這些都在記憶體中完成的化,并發量一大記憶體的占用量以及響應速度都變得非常差。

解決的辦法有兩種,一種如 DDD 書上所介紹,在

Specification

接口上提供一個類似

asSQL()

的方法,将目前的 Specification 對象轉化為 SQL 語句。在我一些項目的實踐中這種做法比較麻煩,可以認為是換了一種方式拼接 SQL,效果并不好,且難以處理。而另一種則是使用 ORM 架構或是其他進階架構的能力。例如 Spring Data JPA 就提供了基于 JPA 的 Specification 模式的查詢功能,使用起來非常友善,也是我建議大家在項目中可以嘗試的方式。

小結

Specification 模式是一種非常實用的模式,能夠很友善的幫助開發人員對狀态校驗,資料篩選這樣的業務規則進行管理與抽象,而且實際操作的難度較低,對外部的依賴也少,是個十分值得推薦的 DDD 最佳實踐。

**

歡迎關注我的微信号「且把金針度與人」,擷取更多高品質文章

***

jpa specification groupby查詢_DDD 中的那些模式 — 使用 Specification 管理業務規則