使用DDD規格Specification模式構建資料驅動規則引擎 - jonblankenship

banq發表於2020-03-25

當面臨確定物件是否滿足一組特定條件的任務時,規格/規範模式(Specification pattern)可能是開發人員工具箱中必不可少的工具。當與組合模式結合使用時,組合規範成為一種強大的工具,可以解決任何複雜的業務規則,同時確保可維護性,健壯性和可測試性。在本文中,我們將看到如何在.NET應用程式中使用組合規格/規範模式來構建資料驅動的規則引擎。

在我的庫存警報專案中,使用者為應該連續評估的給定庫存配置標準,並在滿足條件時向使用者發出通知。

我希望使用者不僅可以設定單個價格觸發條件,還可以指定多種型別的條件,並使用布林邏輯將它們組合起來以形成複雜的規則。

例如,當JNJ滿足以下條件時,可能希望收到股息增長投資者的通知:

  • 股息> 2.5%並且
  • 支付率<50%AND
  • (市盈率<20 或價格<130)

使用DDD規格Specification模式構建資料驅動規則引擎 - jonblankenship

另一位投資者可能希望使用一套完全不同的標準來提醒他們關注目標即將超出範圍。

那麼,我們如何以一種乾淨,可配置和可測試的方式完成此任務?

規範模式

過去,規範模式一直是我工具箱中的一個有價值的工具,它是當今擺在我們面前的工作的完美工具:構建資料驅動的規則引擎。

該模式由領域驅動設計領域的作者埃裡克·埃文斯(Eric Evans)提出:解決軟體核心問題的複雜性,並且是軟體開發領域驅動設計(DDD)方法之父。Evans和Martin Fowler撰寫了有關規範的白皮書,非常值得一讀,其中涉及規範的用途,規範的型別以及規範的後果。

維基百科文章的規範模式為我們提供了一個很好的定義(以及一些C#示例程式碼)入手:

在計算機程式設計,所述規範圖案是特定的設計模式,由此業務規則可以通過連結業務規則一起使用布林邏輯來重新組合。

Evans和Fowler確定了規範模式適用的核心問題:

  • 選擇:您需要根據一些條件選擇物件的子集,並在不同時間重新整理選擇
  • 驗證:您需要檢查僅將合適的物件用於特定目的
  • 按訂單構建:您需要描述一個物件可以做什麼,而無需解釋該物件如何執行操作的詳細資訊,而是以一種可以構建候選人的方式來滿足要求。

這些問題中的每一個的解決方案是建立一個規範,該規範定義物件滿足該規範必須滿足的條件。通常,規範具有IsSatisfied(candidate)將候選物件作為引數的方法。該方法返回true或false,取決於候選物件是否滿足規範的標準。

在本文中,我們將重點介紹一種特殊的規範型別(組合規範),以構建我們的規則引擎。這種規範的名稱源於以下事實:它是另一種常用設計模式的實現:複合模式。

組合Compose模式

複合/組合模式是在“四人幫”(GoF)的開創性工作“ 設計模式:可重用的物件導向軟體的元素”中引入的二十三種軟體設計模式之一。作者將模式分為三類之一:創造型,結構型和行為型。複合模式是一種結構模式,這意味著它是一種模式,它描述實體之間的相互關係,目的是簡化系統的結構。

組合模式描述了一組實體,這些實體可以組合成樹狀結構,其中各個部分與整個結構具有相同的介面,從而允許客戶與各個部分和整體進行互動。這就是複合模式的優勢所在。通過統一處理複合材料及其元件,客戶可以避免在葉節點和分支之間進行區分,從而降低了複雜性並降低了出錯的可能性。

此外,通過將元件結構化為可重組的原始物件的組合,我們可以獲得程式碼重用的好處,因為我們能夠利用現有元件來構建其他組合。在實踐規則引擎的程式碼時,我們將在實踐中看到這一點。

規範構建塊

因此,讓我們從抽象轉向具體,然後看一些程式碼。在開始考慮需要實施的特定於域的規則之前,我們首先需要一些構建塊。

首先,我們需要一個介面來與整個複合規範及其單個元件規範進行互動。所以這是我們的ISpecification介面:

public interface ISpecification<in TCandidate>
{
    bool IsSatisfiedBy(TCandidate candidate);
}

這是一個非常簡單的介面,由一個方法組成,該方法IsSatisfiedBy(TCandidate candidate)根據傳遞給它的候選物件是否滿足給定的規範返回true或false。

type引數TCandidate指定規範將要評估的物件的型別。對於複合規範,傳遞給根節點的候選物件的型別將傳遞給子節點,因此對於構成複合規範的所有單個規範,期望的型別都是相同的。

接下來,我們有一個抽象類CompositeSpecification,它將作為複合規範中任何分支(非葉)節點的基類:

public abstract class CompositeSpecification<TCandidate> : ISpecification<TCandidate>
{
    protected readonly List<ISpecification<TCandidate>> _childSpecifications = new List<ISpecification<TCandidate>>();

    public void AddChildSpecification(ISpecification<TCandidate> childSpecification)
    {
        _childSpecifications.Add(childSpecification);
    }

    public abstract bool IsSatisfiedBy(TCandidate candidate);

    public IReadOnlyCollection<ISpecification<TCandidate>> Children => _childSpecifications.AsReadOnly();
}

CompositeSpecification此處實現的主要行為是節點子規範的管理。它處理子規範向複合規範的新增,並將子規範公開為可以遍歷的只讀集合。

現在介紹布林規範模式,分支(非葉子節點)是表示連線1..n其他規範的布林運算的規範,它們從派生CompositeSpecification。對於我們的初始實現,我們有AND和OR規範(短路)。

AndSpecification:

public class AndSpecification<TCandidate> : CompositeSpecification<TCandidate>
{
    public override bool IsSatisfiedBy(TCandidate candidate)
    {
        if (!_childSpecifications.Any()) return false;

        foreach (var s in _childSpecifications)
        {
            if (!s.IsSatisfiedBy(candidate)) return false;
        }

        return true;
    }
}

OrSpecification:

public class OrSpecification<TCandidate> : CompositeSpecification<TCandidate>
{
    public override bool IsSatisfiedBy(TCandidate candidate)
    {
        if (!_childSpecifications.Any()) return false;

        foreach (var s in _childSpecifications)
        {
            if (s.IsSatisfiedBy(candidate)) return true;
        }

        return false;
    }
}

當然,可以很容易地實現其他布林運算子,例如NOT和XOR,但是到目前為止,這是我到目前為止對我的應用程式唯一需要的兩個,它們足以演示模式。

在繼續介紹域規範之前,讓我們簡要討論一下單元測試。規範模式的吸引人的特徵之一是,由於圍繞單個邏輯小塊的清晰邊界,可以輕鬆地對規範進行單元測試。(點選標題見原文)

特定領域規範(Domain-Specific Specifications)

既然我們已經具備了構建任何規範所需的構建塊,那麼我們就可以檢視構建特定於我們領域的規範所需的內容:庫存警報。當我們從討論布林規範過渡到討論領域特定規範時,我們的重點是從複合規範的分支(非葉子)節點轉移到葉子節點。

1.價格規格

我們的規範必須測試的主要標準之一是,當給出新的報價時,新價格是否超過一定水平。為此,我們將建立一個PriceSpecification知道警報條件的警報標準,該警報標準指定了重要的價格水平,並將根據新股票報價是否違反該水平返回true或false:

public class PriceSpecification : ISpecification<AlertEvaluationMessage>
{
    private readonly AlertCriteria _alertCriteria;

    public PriceSpecification(AlertCriteria alertCriteria)
    {
        _alertCriteria = alertCriteria ?? throw new ArgumentNullException(nameof(alertCriteria));
    }

    public bool IsSatisfiedBy(AlertEvaluationMessage candidate)
    {
        if (_alertCriteria.Operator == CriteriaOperator.GreaterThan)
        {
            return candidate.LastPrice > _alertCriteria.Level &&
                candidate.PreviousLastPrice <= _alertCriteria.Level;
        }

        if (_alertCriteria.Operator == CriteriaOperator.GreaterThanOrEqualTo)
        {
            return candidate.LastPrice >= _alertCriteria.Level &&
                   candidate.PreviousLastPrice < _alertCriteria.Level;
        }

        if (_alertCriteria.Operator == CriteriaOperator.Equals)
        {
            return candidate.LastPrice == _alertCriteria.Level &&
                   candidate.PreviousLastPrice != _alertCriteria.Level;
        }

        if (_alertCriteria.Operator == CriteriaOperator.LessThanOrEqualTo)
        {
            return candidate.LastPrice <= _alertCriteria.Level &&
                   candidate.PreviousLastPrice > _alertCriteria.Level;
        }

        if (_alertCriteria.Operator == CriteriaOperator.LessThan)
        {
            return candidate.LastPrice < _alertCriteria.Level &&
                   candidate.PreviousLastPrice >= _alertCriteria.Level;
        }

        return false;
    }
}

2.價格規範

我們的規範必須測試的主要標準之一是,當給出新的報價時,新價格是否超過一定水平。為此,我們將建立一個PriceSpecification知道警報條件的警報標準,該警報標準指定了重要的價格水平,並將根據新股票報價是否違反該水平返回true或false:

public class PriceSpecification : ISpecification<AlertEvaluationMessage>
{
    private readonly AlertCriteria _alertCriteria;

    public PriceSpecification(AlertCriteria alertCriteria)
    {
        _alertCriteria = alertCriteria ?? throw new ArgumentNullException(nameof(alertCriteria));
    }

    public bool IsSatisfiedBy(AlertEvaluationMessage candidate)
    {
        if (_alertCriteria.Operator == CriteriaOperator.GreaterThan)
        {
            return candidate.LastPrice > _alertCriteria.Level &&
                candidate.PreviousLastPrice <= _alertCriteria.Level;
        }

        if (_alertCriteria.Operator == CriteriaOperator.GreaterThanOrEqualTo)
        {
            return candidate.LastPrice >= _alertCriteria.Level &&
                   candidate.PreviousLastPrice < _alertCriteria.Level;
        }

        if (_alertCriteria.Operator == CriteriaOperator.Equals)
        {
            return candidate.LastPrice == _alertCriteria.Level &&
                   candidate.PreviousLastPrice != _alertCriteria.Level;
        }

        if (_alertCriteria.Operator == CriteriaOperator.LessThanOrEqualTo)
        {
            return candidate.LastPrice <= _alertCriteria.Level &&
                   candidate.PreviousLastPrice > _alertCriteria.Level;
        }

        if (_alertCriteria.Operator == CriteriaOperator.LessThan)
        {
            return candidate.LastPrice < _alertCriteria.Level &&
                   candidate.PreviousLastPrice >= _alertCriteria.Level;
        }

        return false;
    }
}

3.AlertCriteria

該規範在構建時已傳遞給它一個域模型:AlertCriteria。現在,我們只看一些AlertCriteria與該特定規範相關的屬性:

public class AlertCriteria
{
    // snip

    public CriteriaType Type { get; set; }

    public CriteriaOperator Operator { get; set; }

    public decimal? Level { get; set; }

    // snip
}

Type指定CriteriaType我們正在評估的警報。可能的值Composite,Price,DailyPercentageGainLoss,並有可能會更多。 Operator指定CriteriaOperator適用於我們正在評估的特定方案的。最後,如果需要觸發價格警報,則需要知道應在哪個級別上觸發該警報,該警報由Level屬性指定。有了這三個資料,我們就需要了解建立一個規範,該規範表示給定股票價格大於或等於150美元時的價格警報。

該AlertCriteria域物件將最終來自於資料庫,是資料模型,這將使這是一個資料驅動的規則引擎的重要組成部分。我們將對其進行更詳細的研究。

3.AlertEvaluationMessage

我們所需的下一個物件PriceSpecification是AlertEvaluationMessage,這是我們的規範旨在評估的候選物件的型別。在我們的示例中,AlertEvaluationMessage表示新的報價(Message因在這種特殊情況下被從訊息佇列中拉出而命名)。

public class AlertEvaluationMessage
{
    public Guid AlertDefinitionId { get; set; }

    public decimal LastPrice { get; set; }

    public decimal PreviousLastPrice { get; set; }

    public decimal OpenPrice { get; set; }
}

與PriceSpecification相關的是LastPrice和PreviousLastPrice,以此我們可以確定價格是否已超過某個價格水平。

現在我們有了所需的資訊,我們可以評估PriceSpecification中AlertCriteria 是否滿足AlertEvaluationMessage ,這是通過PriceSpecification中的 IsSatisfiedBy(AlertEvaluationMessage candidate)方法實現。

更多點選標題見原文

 

相關文章