Using the Specification Pattern to Build a Data-Driven Rules Engine

栏目: IT技术 · 发布时间: 4年前

Oct 4, 2019

Using the Specification Pattern to Build a Data-Driven Rules Engine

The specification pattern can be an indispensable tool in the developer's toolbox when faced with the task of determining whether an object meets a certain set of criteria. When coupled with the composite pattern, the composite specification becomes a power tool that can tackle any combination of business rules no matter how complex, all while ensuring maintainability, robustness, and testability. In this post we'll see how the composite specification pattern can be used in a .NET application to build a data-driven rules engine.

In myStock Alerts project, the user configures criteria for a given stock that should be evaluated continuously and trigger a notification to the user when the criteria are satisfied.

Rather than simply setting a single price trigger, I want the user to be able to specify multiple types of criteria and combine them using Boolean logic to form a complex rule.

For example, a dividend growth investor might want to be notified when JNJ meets the following criteria:

  • Dividend > 2.5% AND
  • Payout Ratio < 50% AND
  • (PE Ratio < 20 OR Price < 130)

Using the Specification Pattern to Build a Data-Driven Rules Engine

Another investor might want a completely different set of criteria to alert them when their momentum stock darling is about to take off out of a range.

So how do we accomplish this in a clean, configurable, and testable manner?

Enter the Specification Pattern

The specification pattern has been a valuable tool in my toolbox in the past, and it is the perfect tool for the job ahead of us today: building a data-driven rules engine.

Using the Specification Pattern to Build a Data-Driven Rules Engine

The pattern was introduced by Eric Evans, author of Domain-Driven Design: Tackling Complexity in the Heart of Software and father of the domain-driven design (DDD) approach to software development. Evans and Martin Fowler co-wrote a white paper on specifications that is well worth the read addressing the uses of specification, types of specifications, and consequences of specification.

The Wikipedia article on the specification pattern gives us a pretty good definition (as well as some sample C# code) to start with:

In computer programming, the specification pattern is a particular software design pattern , whereby business rules can be recombined by chaining the business rules together using boolean logic.

Evans and Fowler identify the core problems to which the specification pattern is applicable:

  • Selection: You need to select a subset of objects based on some criteria, and to refresh the selection at various times
  • Validation: You need to check that only suitable objects are used for a certain purpose
  • Construction-to-order: You need to describe what an object might do, without explaining the details of how the object does it, but in such a way that a candidate might be built to fulfill the requirement.

The solution to each of these problems is to build a specification that defines the criteria that an object must meet to satisfy the specification. Typically, a specification has a method IsSatisfied(candidate) that takes the candidate object as a parameter. The method returns true or false , depending on whether the candidate satisfies the criteria of the specification.

In this post, we’ll focus on a particular type of specification, a composite specification , to build our rules engine. This type of specification gets its name from the fact that it is an implementation of another commonly used design pattern: the composite pattern.

The Composite Pattern

Using the Specification Pattern to Build a Data-Driven Rules Engine

The composite pattern is one of the twenty-three software design patterns introduced in the Gang of Four’s (GoF) seminal work, Design Patterns: Elements of Reusable Object-Oriented Software . The authors classify the patterns into one of three categories: creational, structural and behavioral. The composite pattern is a structural pattern, meaning it is a pattern that describes how entities relate to each other with the goal of simplifying the structure of a system.

The composite pattern describes a group of entities that can be combined into a tree-like structure, where the individual parts have the same interface as the structure as a whole, allowing clients to interact with the parts and the whole uniformly. This is where the advantage of the composite pattern lies. By treating the composite and its components uniformly, clients avoid having to discriminate between a leaf node and a branch, reducing complexity and the potential for error.

Further, by structuring a component as a composite of primitive objects that are recombinable, we get the benefits of code reuse as we are able to leverage existing components to build other composites. We’ll see this in practice as we get into the code for our rules engine.

Specification Building Blocks

So let’s move from the abstract to the concrete and see some code. Before we start to think about domain-specific rules that we need to implement, we’ll need a few building blocks first.

ISpecification

First, we need an interface for interacting with the composite specification as a whole, as well as its individual component specifications. So here’s our ISpecification interface:

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

It’s a very simple interface consisting of one method, IsSatisfiedBy(TCandidate candidate) , which returns true or false depending on whether the given specification is satisfied by the candidate passed to it.

The type parameter TCandidate specifies the type of object that the specification will be used to evaluate. In the case of a composite specification, the type of candidate object passed to the root node will be passed to the child nodes, so the type is expected to be the same for all individual specifications that make up a composite specification.

CompositeSpecification

Next, we have an abstract class CompositeSpecification , which will be the base class for any branch (non-leaf) nodes in our composite specification:

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();
}

The main behavior that CompositeSpecification implements here is the management of a node’s child specifications. It handles the addition of a child specification to the composite specification, and it exposes the child specifications as a read-only collection that can be traversed.

Boolean Specifications

The branches (non-leaf nodes) are specifications that represent Boolean operations that connect 1..n other specifications, and they derive from CompositeSpecification . For our initial implementation, we have AND and OR specifications (short-circuited).

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;
    }
}

Of course, other Boolean operators, such as NOT and XOR , could be implemented without too much difficulty, but these are the only two that I’ve needed so far for my application, and they are sufficient for demonstrating the pattern.

Unit Testing the Boolean Specifications

Before we move on to the domain-specification specifications, let’s talk briefly about unit testing. One of the attractive characteristics of the specification pattern is the ease with which specifications can be unit-tested due to the clear boundaries around small, individual chunks of logic.

To assist with unit-testing, I’ve implemented very simple True and False specifications:

public class TrueSpecification<TCandidate> : ISpecification<TCandidate>
{
    public bool IsSatisfiedBy(TCandidate candidate) => true;
}

public class FalseSpecification<TCandidate> : ISpecification<TCandidate>
{
    public bool IsSatisfiedBy(TCandidate candidate) => false;
}

These one-line specifications aren’t useful in application code, but they come in handy when unit-testing the branch (non-leaf) node specifications. Here’s one of the unit tests for the AndSpecification , utilizing the TrueSpecification :

[Fact]
public void IsSatisfiedBy_TwoTrueChildren_True()
{
    // Arrange
    var spec = new AndSpecification<AlertEvaluationMessage>();
    spec.AddChildSpecification(new TrueSpecification<AlertEvaluationMessage>());
    spec.AddChildSpecification(new TrueSpecification<AlertEvaluationMessage>());
    var message = new AlertEvaluationMessage();

    // Act
    var result = spec.IsSatisfiedBy(message);

    // Assert
    result.Should().BeTrue();
}

TrueSpecification and FalseSpecification facilitate the writing of straight-forward unit tests like the one above that zero in on the unit under test ( AndSpecification.IsSatisfiedBy(..) in this case). (You may be wondering about AlertEvaluationMessage - it is the type of the candidate object that we are evaluating, which we’ll examine in a bit.)

Domain-Specific Specifications

Now that we have the building blocks needed to build any specification, we’re ready to look at the things we’ll need to build specifications specific to our domain: stock alerts. As we transition from discussing the Boolean specifications to talking about domain-specific specifications, our focus is moving from the branch (non-leaf) nodes of the composite specification to the leaf nodes.

PriceSpecification

One of the main criteria that our specification will have to test for is, when given a new price quote, whether the new price exceeds a certain level. For this, we’ll create a PriceSpecification that is aware of an alert criteria specifying an important price level, and it will return true or false depending on whether a new stock quote breaches that level:

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

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Programming Computer Vision with Python

Programming Computer Vision with Python

Jan Erik Solem / O'Reilly Media / 2012-6-22 / USD 39.99

If you want a basic understanding of computer vision's underlying theory and algorithms, this hands-on introduction is the ideal place to start. As a student, researcher, hacker, or enthusiast, you'll......一起来看看 《Programming Computer Vision with Python》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

在线进制转换器
在线进制转换器

各进制数互转换器