DDD 中的那些模式 — 使用 Specification 管理業務規則

DDD 中的那些模式 — 使用 Specification 管理業務規則

許多開發者在項目中但願可以使用 DDD 緣由在於可以管理業務的複雜度,避免在業務規則愈發複雜的狀況下代碼以及架構發生腐化,最終變的難以維護。系統複雜度體如今多個層面,例如繁瑣的流程,繁複的校驗規則,數據的多樣性等,DDD 對於不一樣層面的複雜度提供了不一樣的應對模式,今天的文章會聚焦與如何使用 Specification 模式解決「業務規則」相關的複雜性。html

業務規則

在介紹 Specification 模式以前,咱們先明確一下什麼是「業務規則」。做爲一個開發者,如下的這些必定是你平常工做中常見的工做。java

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

爲了方便後續的討論,咱們將業務規則的概念窄化爲以上三種類型。接下來的問題是這些業務規則在系統中是如何實現的?spring

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

在此基礎上的另外一個方案也是實際項目中使用比較多的,即編寫各類不一樣的 Validator 類,每一個 Validator 類中有大量對於領域對象的校驗方法。這種作法必定程度上解決了第一個問題,經過特定的類將檢驗方法集中起來,能夠方便開發人員進行維護和擴展。下面是一個典型的 Validator 的示例代碼:數據庫

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

可是這對於業務知識的傳遞沒有太大的幫助。Validator 類更像是一些工具類,和領域層並無什麼關聯。而當須要對這些校驗方式進行復用時,特別是將幾個校驗規則按照 andor 這樣的邏輯關係組合起來時,Validator 就支持不是那麼好了。微信

另外一種方法是是將這些校驗的規則在領域對象中實現,做爲該領域對象的一個方法。參考以下的代碼:架構

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

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

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

Specification 模式

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

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 最佳實踐。

歡迎關注個人微信號「且把金針度與人」,獲取更多高質量文章

QR.png

相關文章
相關標籤/搜索