Jonathan Boccara's blog

Declarative If Statements with a Simplified Rules Engine

Published January 18, 2019 - 0 Comments

Announcement:

My first book, The Legacy Code Programmer’s Toolbox will be released in electronic format on February 1st, that is in two weeks from now.

If you have to work with legacy code on a regular basis, this book will help you make it more expressive to your eyes by understanding it better. It will also show you how to make it actually more expressive by refactoring such anti-patterns as long functions, how to diagnose bugs quickly, how to write useful documentation, how to stay motivated, how to improve your programming skills even when you’re working with legacy code, and much more.

This is the biggest project I undertook since I started Fluent C++. Kevlin Henney made me the honour of writing the book’s foreword.

February 1st is the day it’s coming out. Make sure to visit the book’s page now so that you’re notified when it’s out!

:End of Announcement

 

If the code is like a road, its if statements are the crossings that come up every once in a while when driving. And crossings are the dangerous places in the road where you need to pay special attention if you want to reach your destination, and if you want to reach it safely.

if else

Like crossings on the road, if statements necessary points of complexity. And as the urban architect and builder of your codeline, you need to design them in such a way as to make them safe, and as easy as possible to navigate for the readers of your code.

A lot of if statements don’t require any specific design, just like two roads crossing in town will be ok with a traffic light. But the complexity of some of them require of you to design a roundabout, or even an interchange, to make sure that the code goes in the right direction and that your readers don’t get lost.

Let’s focus on those complex if statements, and express them in a declarative way in code with a simplified rules engine.

Good customers, bad customers

As a motivating example, let’s consider a piece of code that takes an action depending on whether or not a customer is classified as a good customer.

Say that the specification says that a customer is a good customer if they satisfy at least one of the following condition:

  • they purchased for more than $1,000 over the past year,
  • they never returned a purchased item,
  • they answered a customer survey at least once.

And say that we have a Customer API that will readily provide all this information for us:

const bool isAGoodCustomer = customer.purchasedGoodsValue() >= 1000 
                          || !customer.hasReturnedItems()
                          || std::find(begin(surveyResponders), end(surveyResponders), customer) != end(surveyResponders);

if (isAGoodCustomer)
{
    std::cout << "Dear esteemed customer,";
}
else
{
    std::cout << "Dear customer,";
}

To spice up this if statement a little, let’s add another clause: if a customer has defaulted (that is, they cannot pay their invoice), they’re not a good customer, regardless of all the other conditions.

How do we add this to the above code?

This is exactly what happened to me with a feature to add in our application. Even if it wasn’t about customers and invoices, the structure of the problem was the same.

One possibility would be to tack on a new boolean over the logic expression:

const bool isAGoodCustomer = (customer.purchasedGoodsValue() >= 1000 
                          || !customer.hasReturnedItems()
                          || std::find(begin(surveyResponders), end(surveyResponders), customer) != end(surveyResponders))
                      && !customer.hasDefaulted();

But the if statement becomes dangerously hard to read.

To make if statements more understandable, we’ve seen that they should look read as much as possible like their specification. So et’s use a simplified rules engine to make our if statement declarative.

A rules engine

What’s a rules engine? A rules engine is a piece of software designed to swallow up some rules and apply them to a given situation. For example, we could tell a rules engine all the clauses that determine whether a customer is a good customer, and then present it a given customer. The engine would match that customer against the rules and output the result of applying those rules.

Rules engine are complex pieces of software that run outside of the main application, to alleviate the code of some business logic and to treat the rules in a very optimized way.

Putting a rules engine into place for our little if statement seems like over-engineering. However, we can use the idea of a rules engine and implement a simplified version in the code.

A target interface

Let’s start by deciding how we would like the code to look like, then create a rules engine to implement that interface.

Looking back at our specification:

A customer is a good customer if they satisfy at least one of the following conditions:

  • they purchased for more than $1,000 over the past year,
  • they never returned a purchased item,
  • they answered a customer survey at least once.

However a customer is Not a good customer as soon as they satisfy at least one of the following conditions:

  • they have defaulted.

A declarative code that looks like this specification would look like:

isAGoodCustomer if (customer.purchasedGoodsValue() >= 1000)
isAGoodCustomer if (!customer.hasReturnedItems())
isAGoodCustomer if (std::find(begin(surveyResponders), end(surveyResponders), customer) != end(surveyResponders))

isNotAGoodCustomer if (customer.hasDefaulted())

if (isAGoodCustomer)
{
    std::cout << "Dear esteemed customer,";
}
else
{
    std::cout << "Dear customer,";
}

This code would not compile as is. But we can make something close enough that compiles and has the expected behaviour.

Implementation of the rules engine

Our rules engine can receive some booleans value that can have two meanings:

  • a sufficient condition, like having purchased for more than $1,000. A sufficient condition is enough to output true as the final result
  • a preventing condition, like having defaulted. If a preventing condition is met, then the output is false whatever the other conditions.

Let’s start by inputting sufficient conditions with an If method, and preventing conditions with a NotIf method:

class RulesEngine
{
public:
   void If(bool sufficientCondition) { sufficientConditions.push_back(sufficientCondition); }
   void NotIf(bool preventingCondition) { preventingConditions.push_back(preventingCondition); }

private:
   std::deque<bool> sufficientConditions;
   std::deque<bool> preventingConditions;
};

Note that I use std::deque<bool> instead of std::vector<bool> here, because this particular instantiation of std::vector is flawed. The reason why it is flawed is off-topic here but if you want to hear more about it you will know everything by reading Item 18 of Effective STL.

Now that the rules engine stores all the data, we need to make it evaluate it. A nice syntax in C++ is to use operator() to invoke the engine. But in another language the evaluation could also be a regular method like .get() or .evaluate() for example.

   bool operator()() const
   {
      auto isTrue = [](bool b){ return b; };
      return std::any_of(sufficientConditions, isTrue) && std::none_of(preventingConditions, isTrue);
   }

How lovely and expressive is the line of code of the return statement? An expressive interface and an expressive implementation is a good sign that it’s a worthy abstraction.

Unfortunately this range based syntax is too good to be true in std, although it is likely to be included in C++20. Anyway we can either write wrapper functions that take a container and calls the STL algorithms with its iterators, or directly use the STL algorithms as they are today:

   bool operator()() const
   {
      auto isTrue = [](bool b){ return b; };
      return std::any_of(begin(sufficientConditions), end(sufficientConditions), isTrue) 
          && std::none_of(begin(preventingConditions), end(preventingConditions), isTrue);
   }

Let’s now rewrite our original code by using the rules engine:

auto isAGoodCustomer = RulesEngine{};

isAGoodCustomer.If(customer.purchasedGoodsValue()) >= 1000);
isAGoodCustomer.If(!customer.hasReturnedItems()));
isAGoodCustomer.If(std::find(begin(surveyResponders), end(surveyResponders), customer) != end(surveyResponders));

isAGoodCustomer.NotIf(customer.hasDefaulted());

if (isAGoodCustomer())
{
    std::cout << "Dear esteemed customer,";
}
else
{
    std::cout << "Dear customer,";
}

Refining the interface

The above code is not far from our target, except the line describing the preventing conditions:

isAGoodCustomer.NotIf(customer.hasDefaulted());

Whereas our target was:

isNotAGoodCustomer if (customer.hasDefaulted())

To achieve this, we can create a subordinate rules engine called isNotAGoodCustomer, that would receive preventing conditions with an If method and forward them to the main rules engine isAGoodCustomer.

class PreventingRulesEngine
{
  public:
     explicit PreventingRulesEngine(RulesEngine& rulesEngine) : rulesEngine_(rulesEngine) {}
     void If(bool preventingCondition){ rulesEngine_.NotIf(preventingCondition); }
  private:
     RulesEngine& rulesEngine_;
};

The main rules engine can then provide a subordinate PreventingRulesEngine under the term Not:

class RulesEngine
{

public:
   RulesEngine() : Not(*this){}

   PreventingRulesEngine Not;

   // ...

There is a technical subtlety to implement this because both classes depend on each other, and we’ll see that in a moment. But let’s first have a look at the result in business code:

auto isAGoodCustomer = RulesEngine{};

isGoodCustomer.If(customer.purchasedGoodsValue()) >= 1000);
isGoodCustomer.If(!customer.hasReturnedItems()));
isGoodCustomer.If(std::find(begin(surveyResponders), end(surveyResponders), customer) != end(surveyResponders));

auto isNotAGoodCustomer = isAGoodCustomer.Not;
isNotAGoodCustomer.If(customer.hasDefaulted());

if (isAGoodCustomer())
{
    std::cout << "Dear esteemed customer,";
}
else
{
    std::cout << "Dear customer,";
}

Which gets close enough to the target code.

Putting all the code together

As promised, let’s look at how to implement the two classes RulesEngine and PreventingRulesEngine that depend on each other.

If you want a header-only implementation, you can define PreventingRulesEngine as a nested class of RulesEngine:

class RulesEngine
{
public:
    RulesEngine() : Not(*this){}

    void If(bool sufficientCondition) { sufficientConditions.push_back(sufficientCondition); }
    void NotIf(bool preventingCondition) { preventingConditions.push_back(preventingCondition); }

    class PreventingRulesEngine
    {
      public:
         explicit PreventingRulesEngine(RulesEngine& rulesEngine) : rulesEngine_(rulesEngine) {}
         void If(bool preventingCondition){ rulesEngine_.NotIf(preventingCondition); }
      private:
         RulesEngine& rulesEngine_;
    };
    PreventingRulesEngine Not;

    bool operator()() const
    {
       auto isTrue = [](bool b){ return b; };
       return std::any_of(begin(sufficientConditions), end(sufficientConditions), isTrue) 
           && std::none_of(begin(preventingConditions), end(preventingConditions), isTrue);
    }
    
private:
    std::deque<bool> sufficientConditions;
    std::deque<bool> preventingConditions;
};

If you don’t like nested classes but still want a header-only solution, you can still forward declare RulesEngine and then implement inline the methods of PreventingRulesEngine:

class RulesEngine;

class PreventingRulesEngine
{
  public:
     explicit PreventingRulesEngine(RulesEngine& rulesEngine) : rulesEngine_(rulesEngine) {}
     void If(bool preventingCondition);
  private:
     RulesEngine& rulesEngine_;
};

class RulesEngine
{

public:
   RulesEngine() : Not(*this){}

   void If(bool sufficientCondition) { sufficientConditions.push_back(sufficientCondition); }
   void NotIf(bool preventingCondition) { preventingConditions.push_back(preventingCondition); }
   PreventingRulesEngine Not;

   bool operator()() const
   {
      auto isTrue = [](bool b){ return b; };
      return std::any_of(begin(sufficientConditions), end(sufficientConditions), isTrue) 
          && std::none_of(begin(preventingConditions), end(preventingConditions), isTrue);
   }
private:
   std::deque<bool> sufficientConditions;
   std::deque<bool> preventingConditions;
};

inline void PreventingRulesEngine::If(bool preventingCondition){ rulesEngine_.NotIf(preventingCondition); }

But that’s maybe not the prettiest code ever. In this case, it’s probably clearer to split the code between a header file and a .cpp file:

// RulesEngine.hpp

class RulesEngine;

class PreventingRulesEngine
{
  public:
     explicit PreventingRulesEngine(RulesEngine& rulesEngine);
     void If(bool preventingCondition);
  private:
     RulesEngine& rulesEngine_;
};

class RulesEngine
{
public:
   RulesEngine();

   void If(bool sufficientCondition);
   void NotIf(bool preventingCondition);
   PreventingRulesEngine Not;

   bool operator()() const;
   
private:
   std::deque<bool> sufficientConditions;
   std::deque<bool> preventingConditions;
};


// RulesEngine.cpp

RulesEngine::RulesEngine() : Not(*this){}

void RulesEngine::If(bool sufficientCondition)
{
   sufficientConditions.push_back(sufficientCondition);
}

void RulesEngine::NotIf(bool preventingCondition)
{
    preventingConditions.push_back(preventingCondition);
}

bool RulesEngine::operator()() const
{
   auto isTrue = [](bool b){ return b; };
   return std::any_of(begin(sufficientConditions), end(sufficientConditions), isTrue) 
       && std::none_of(begin(preventingConditions), end(preventingConditions), isTrue);
}

PreventingRulesEngine::PreventingRulesEngine(RulesEngine& rulesEngine) : rulesEngine_(rulesEngine) {}
   
void PreventingRulesEngine::If(bool preventingCondition)
{
    rulesEngine_.NotIf(preventingCondition);
}

Have an expressive trip

Should we use a rules engine for every if statement? No, the same way that there is no need for a roundabout at every crossroads. But our simplified rules engine can mitigate the complexity of some if statements and make the code more expressive by adopting a declarative style.

Should we enrich the rules engine? Do you see other methods to add, that could express complex if statements in a declarative way?

Until then, I wish you a safe and expressive trip writing your code.

declarative if C++ rules engine

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin