【DDD】領域驅動設計之規約模式

工具人發現一個很有意思的現象,在傳統企業中,由於成果導向不同,所以採購師和工具人對產品功能的審美是截然相反的。

採購師以高顏值爲價值取向,內在腐朽不重要,無論是加拿大火腿腸還是墨西哥雞肉卷,只要能評獎,就是好產品。

而工具人則以內在美爲審美標準,沒有優秀的內部設計,再光鮮的麪皮,也難逃照妖鏡的聚光。

所以,工具人推薦使用 DDD 的緣由在於可以管理業務的複雜度,避免在業務規則愈發複雜的狀況下代碼以及架構發生腐化,變的難以維護,最終退化成下一個 WuYF。

大多數業務系統的複雜度體現在多個層面,比如:繁瑣的流程,繁複的校驗規則,數據的多樣性等。

DDD 對於不一樣層面的複雜度提供了不一樣的應對模式,今天我們聊瞭解下規約模式( Specification 模式)是如何解決業務規則的複雜性的。

常見 html

**常見的業務規則 **

作爲一個 CRUD 工具人,日常工作經常會遇到如下場景:

以往我們常常會在 Domain service 中編寫一些簡單方法和校驗邏輯,但是由於它們被分散在各處,當業務越來越複雜、不同的場景這些規則需要多種不同的排列組合關係時,傳統的模式就會變得難以管理。

就像剛纔我們提到的理財業務,其校驗規則會比較複雜,例如理財購買,你可能需要驗證用戶賬號是否可用、理財產品的剩餘額度是否充足、產品是否處於認購期,與用戶的風險等級是否匹配等等…… 僅僅是通過一連串的 if 判斷,那就真的太不利於維護了,並且 if 嵌套的多了代碼難於理解,不好說明白具體意圖,而簡單短小的方法,若是任其分散在不同的 Domain Service 中,後續的開發過程當中就這些業務知識就很容易被丟失。因此需要將隱式業務規則轉換成顯示概念,這也是 DDD 的要求。

規約模式提煉隱式規則

而規約模式經常在 DDD 中使用,用來將業務規則(通常是隱式業務規則)封裝成獨立的邏輯單元,從而將隱式業務規則提煉爲顯示概念,並達到代碼複用的目的。

DDD 中認爲這些規則都是純粹的 “動詞”,所以需要單獨的創建模型,而這些模型都應該是簡單的值對象。

首先,定義規約接口:

public interface ISpecification<T> {
    boolean isSatisfied(T entity);
}

以風險等級規約爲例:

public class FinanceRiskSpecification implements ISpecification<FinanceProduct> {
    Customer customer;
    public FinanceRiskSpecification(Customer customer) {
        this.customer = customer;
    }
    @Override
    public boolean isSatisfied(FinanceProduct financeProduct) {
        return financeProduct.getRiskLevel().compareTo(this.customer.getRiskLevel()) > 0;
    }
}

同樣,我們可以實現認購額度規約等:

public class FinanceQuotaSpecification implements ISpecification<FinanceProduct> {
    double purchaseQuota;
    public FinanceRiskSpecification(double purchaseQuota) {
        this.purchaseQuota = purchaseQuota;
    }
    @Override
    public boolean isSatisfied(FinanceProduct financeProduct) {
        return financeProduct.getRestQuota() > purchaseQuota;
    }
}

理財購買是一種組合規約的場景,多個規約需要同時滿足,所以我們可以對規約模式進行擴展:

public class AndSpecification<T> implements ISpecification<T> {
    private ISpecification<T> left;
    private ISpecification<T> right;
    public AndSpecification(ISpecification<T> left, ISpecification<T> right) {
        this.left = left;
        this.right = right;
    }
    @Override
    public boolean isSatisfied(T entity) {
        return this.left.isSatisfied(entity)
                && this.right.isSatisfied(entity);
    }
}

組合使用多個規約:

ISpecification<FinanceProduct> compositSpecification = 
  new AndSpecification<FinanceProduct>(new FinanceRiskSpecification<FinanceProduct>(XXX),
                                       new FinanceQuotaSpecification<FinanceProduct>(1000.00));

規約模式過濾查詢

從業務對象的集合中篩選出符合條件的結果集,也是工具人日常工作中的常見場景。傳統模式中,當數據量很大的情況下,我們常常將邏輯下沉至 DAO,直接在 SQL 中完成。如查詢所有的融資融券股票:

// 以下代碼爲工具人 YY
select * from t_security 
    where t.margin_trade = '1'   //融資融券標識
      and security_type = 'EQUIT' //證券類型爲股票
      and list_status = '1';      // 標的狀態爲上市

其實,這部分的邏輯原本應該是屬於領域層的,如今卻泄漏到了數據層,形成的後果就是維護的難度大大提高,不少業務系統到後期都是在和大段大段的 SQL 作鬥爭,而應該編寫邏輯的 Service 層,Domain 層都成了擺設,退化成了純粹的數據對象,又回到了傳統模式的老路。而 規約 模式能夠提供一種不錯的解決思路。我們可以爲每個規約增加一個 queryWrapper 的方法,返回一個類似查詢包裝對象(這裏和使用的 ORM 框架有點耦合, 工具人這裏舉例的是 baomidou)。

public interface ISpecification<T> {
    boolean isSatisfied(T entity);
    QueryWrapper<T> queryWrapper(QueryWrapper<T> tQueryWrapper);
}

構建查詢股票型基金產品的規約:

public class FinanceInvestStyleSpecification implements ISpecification<FinanceProduct> {
    String investStyle;
    QueryWrapper<FinanceProduct> tQueryWrapper;
    public FinanceRiskSpecification(String investStyle,QueryWrapper<FinanceProduct> tQueryWrapper) {
        this.investStyle = investStyle;
        this.tQueryWrapper = tQueryWrapper;
    }
    ....
    @Override
    public QueryWrapper<FinanceProduct> queryWrapper() {
        return tQueryWrapper.eq("invest_style", investStyle);
    }
}

在理財產品倉庫,我們則可以用 findBySpecifications,統一抽象該類查詢

public List<FinanceProduct> queryProductBySpec(ISpecification<FinanceProduct> spec)

最後,如果颱風天氣下,上班的工具人,請一定注意安全。

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