Decision Table implementation with bitwise operations in C#

When a logic becomes too complicated, but you want your code to communicate in clean and concise way rather than mess it up with countless (and sometimes nested) “if” statements or rigid “switches” - design with Decision Tables can be a good choice.

The obstacle though - there is no such a class in C# that provides you the way to hide complexities but expose clean code in kind of easy to read and understand declarative style.

This article is about developing such a class that addresses this issue.

The code example below gives an idea of what we would like to achieve.

            //Actions performing methods (represented by delegates for brevity)
            Action Driving = () => Console.WriteLine("Driving");
            Action StayingAtHome = () => Console.WriteLine("Staying at home");
            Action Dancing = () => Console.WriteLine("Dancing");
            Action RepairingTheCar = () => Console.WriteLine("Repairing the car");

            //Boolean methods to examine (represented by delegates for brevity)
            Func<bool> IsSunShining = () => true;
            Func<bool> IsCarRepaired = () => true;
            Func<bool> IsMusicPlaying = () => true;

            //---------------------------------------
            // Code that uses DecisionTable class:
            //---------------------------------------

            var DTable = new DecisionTable();

            //Conditions as abstraction of Boolean methods
            var SunIsShining = DTable.NewCondition(IsSunShining);
            var CarIsRepaired = DTable.NewCondition(IsCarRepaired);
            var MusicIsPlaying = DTable.NewCondition(IsMusicPlaying);

            //Rules (for actions to take place according to sets of conditions)
            DTable.AddRule(Driving, SunIsShining.True, CarIsRepaired.True);
            DTable.AddRule(Dancing, MusicIsPlaying.True, CarIsRepaired.True);
            DTable.AddRule(StayingAtHome, CarIsRepaired.False);
            DTable.AddRule(RepairingTheCar, CarIsRepaired.False);

            DTable.Execute();

And the outcome of above set of rules for declared conditions and actions is:

So we should have some DecisionTable object, which creates new Condition object and by now you might be wondering of what type the "True" and "False" properties of Condition object are.

Decision Tables are made of sets of Conditions, Actions and Rules. Since Actions can be well represented by System.Action delegates we definitely need some abstractions for Conditions and Rules.


Conditions

The Condition object shall serve as an abstraction for method that returns Boolean value and it shall offer convenient properties ("True" and "False") for representing an outcome of Boolean expression to be evaluated.

It shall also have an "Execute" method that will return the value of "True" or "False" according to result of that Boolean expression evaluation.

In order to achieve proper functionality different "True" and "False" values have to be easily combined.

Their values and values of their combinations have to be unique through all instances of Condition objects.

But this is exactly what values returned by exponentiation with base of 2 and power of integer (2n) can offer. We can produce those values by applying C# left-shift operator "<<" on 1 by a number of bits specified by outcomes counter.

One caveat though is that for every instance of Condition object we have to produce two values of 2n (one for each outcome), so those values become big very quickly. Therefore we will use the long type for "True" and "False" properties of Condition class and also as a return type for its Execute method. We also limit the number of Condition object instances up to 31 (that is 62 values for outcomes).

Remember, we have to keep outcome values unique for the scope of Decision Table. This limit is needed because once limit for long type range has been reached, the left-shift operator will begin producing duplicate values starting from 33-nd instance of Condition object and already in 32-nd instance the value of second outcome will exceed maximum range for "long" type.

It is hard to imagine case where we would need so many conditions anyway, but if we do - we can use another instance of DecisionTable object if we need more than 31 Conditions to declare.

So our Condition class is very simple:

        class Condition
        {
            public long True { get; private set; }
            public long False { get; private set; }

            private Func<bool> _predicate;

            public Condition(Func<bool> predicate, long t, long f)
            {
                True = t;
                False = f;
                _predicate = predicate;
            }

            public long Execute()
            {
                return _predicate() ? True : False;
            }

        }

And we can instantiate it as shown below.

var condition = new Condition(predicate, 
                              1L << _outcomesCounter++, 
                              1L << _outcomesCounter++);

The Condition abstraction is an integral part of DecisionTable object and has no usage outside of it.

The only reason for Condition object to be exposed outside is to allow for consumer of this code to conveniently declare rules using its "True" and "False" properties as shown in usage code example at the beginning.

Moreover the DecisionTable objects is the only one who knows about what values for outcomes has to be assigned to each Condition object.

For these reasons we also want to force its instantiation from inside of DecisionTable objects.

Therefore we will make it a private nested class inside DecisionTable class.

It will implements public Interface (also nested within DecisionTable) and that interface is a type we will expose to code outside with every new Condition creation.

public interface ICondition
{
    long True { get; }
    long False { get; }
}

private class Condition: ICondition
{
    . . .
}


Rules

Rules are constructs that define which actions will be invoked when desired subset of outcomes happens.

Therefore our simple Rule class will construct corresponding object with Action delegate and array of desired outcomes.

It will also have two more methods. One (Execute) to invoke an Action in case where all conditions are met, and another one (AllConditionsMet) is to test whether incoming set of all outcomes includes outcomes subset of current rule for its Action to be invoked.

        private class Rule
        {
            private readonly long[] _subSetOfOutcomes;
            private readonly Action _action;

            public Rule(Action action, long[] SubSetOfOutcomes)
            {
                _subSetOfOutcomes = SubSetOfOutcomes;
                _action = action;
            }

            public void Execute(long SetOfOutcomes)
            {
                if (AllConditionsMet(SetOfOutcomes))
                    _action.Invoke();
            }

            private bool AllConditionsMet(long SetOfOutcomes)
            {
                foreach (var outcome in _subSetOfOutcomes)
                {
                    if ((SetOfOutcomes & outcome) == 0)
                        return false;
                }
                return true;
            }
        }

Inside AllConditionsMet method we loop through all desired outcomes in private subset and test each one on presence in complete set of outcomes (SetOfOutcomes) using logical bitwise AND operator (&).

The Rule class does not even have to be exposed outside of DecisionTable class, therefore it will also be implemented as private nested class and DecisionTable class will use its public AddRule method to take parameters and instantiate Rule objects.


Decision Table

Besides above mentioned two nested classes and one interface the DecisionTable class will have

  • Two counters (for Condition instances and outcomes).
  • Two lists (for Conditions and Rules).
  • Constant field to refer to (for enforcing limit on Condition instances).
  • Two methods to Instantiate Conditions and Rules respectively.
  • One method to check limit for Condition instances.
  • An Execute method to evaluate conditions outcomes and invoke actions of appropriate Rules.

Method to instantiate new Condition object

This simple method will

  • Get Func<bool> "predicate" parameter for new Condition and test it for not being null.
  • Test whether new instance would exceed limit and throw ArgumentOutOfRangeException if required.
  • Create instance of Condition object providing its constructor with correct and unique values for outcomes.
  • Add new instance to the list of Conditions.
  • Return new instance as ICondition interface

        public ICondition NewCondition(Func<bool> predicate)
        {
            if (null == predicate)
                throw new ArgumentNullException("predicate");

            CheckLimit();
            var condition = new Condition(predicate, 
                                          1L << _outcomesCounter++, 
                                          1L << _outcomesCounter++);
            _conditions.Add(condition);
            return condition;

        }

Method to add a new Rule

This method is quite straightforward, but it has one cool thing that deserves to be mentioned here.
It is a usage of "params" C# keyword which specifies a method parameter that takes a variable number of arguments. We use it to supply an array of desired outcomes for action to be invoked.

public void AddRule(Action action, params long[] outcomes)
{
    _rules.Add(new Rule(action, outcomes));
}

This allows us to add as much desired outcomes as we wish for the Rule in clean and a nice way as shown below.

DTable.AddRule(Dancing, MusicIsPlaying.True, 
                        CarIsRepaired.True,
                        SunIsShining.True //,
						// . . .,
						// . . .
			);

"Execute" method of Decision Table

This is a key method of DecisionTable class where we loop through all created instances of Condition objects to evaluate their inner predicates and combine all resulting outcomes of those evaluations using bitwise logical OR assignment operator into complete set of outcomes.

We then loop again through the list of Rules, calling their Execute methods and providing each one with a complete set of outcomes produced above.

Each Rule will then decide whether to invoke its own action depending on existence of its desired outcomes subset in provided complete set of outcomes.

public void Execute()
{
    long SetOfAutcomes = 0;
    _conditions.ForEach(c => 
		SetOfAutcomes |= c.Execute());
    _rules.ForEach(r => r.Execute(SetOfAutcomes));
}

Happy programming.

Alex Movshovich,
Software Developer.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Complete Code for Decision Table class

    public class DecisionTable
    {
        private int _coditionInstancesCounter = 0;
        private int _outcomesCounter = 0;

        private const int SCOPE_LIMIT = 31;

        private List<Condition> _conditions = 
			new List<Condition>();
        private List<Rule> _rules = new List<Rule>();

        public ICondition NewCondition(Func<bool> predicate)
        {
            if (null == predicate)
                throw new ArgumentNullException("predicate");

            CheckLimit();
            var condition = new Condition(predicate, 
                                          1L << _outcomesCounter++, 
                                          1L << _outcomesCounter++);
            _conditions.Add(condition);
            return condition;

        }

        private void CheckLimit()
        {
            _coditionInstancesCounter++;
            if (_coditionInstancesCounter > SCOPE_LIMIT)
                throw new ArgumentOutOfRangeException("Call to create new Condition instance exceeded scope limit");
        }

        public void AddRule(Action action, params long[] outcomes)
        {
            _rules.Add(new Rule(action, outcomes));
        }

        public void Execute()
        {
            long SetOfAutcomes = 0;
            _conditions.ForEach(c => SetOfAutcomes |= c.Execute());
            _rules.ForEach(r => r.Execute(SetOfAutcomes));
        }

        private class Rule
        {
            private readonly long[] _subSetOfOutcomes;
            private readonly Action _action;

            public Rule(Action action, long[] SubSetOfOutcomes)
            {
                _subSetOfOutcomes = SubSetOfOutcomes;
                _action = action;
            }

            public void Execute(long SetOfOutcomes)
            {
                if (AllConditionsMet(SetOfOutcomes))
                    _action.Invoke();
            }

            private bool AllConditionsMet(long SetOfOutcomes)
            {
                foreach (var outcome in _subSetOfOutcomes)
                {
                    if ((SetOfOutcomes & outcome) == 0)
                        return false;
                }
                return true;
            }
        }

        public interface ICondition
        {
            long True { get; }
            long False { get; }
        }

        private class Condition: ICondition
        {
            public long True { get; private set; }
            public long False { get; private set; }

            private Func<bool> _predicate;

            public Condition(Func<bool> predicate, long t, long f)
            {
                True = t;
                False = f;
                _predicate = predicate;
            }

            public long Execute()
            {
                return _predicate() ? True : False;
            }
        }
    }

© 2014 softomiser

More by this Author


Comments

No comments yet.

    Sign in or sign up and post using a HubPages Network account.

    0 of 8192 characters used
    Post Comment

    No HTML is allowed in comments, but URLs will be hyperlinked. Comments are not for promoting your articles or other sites.


    Click to Rate This Article
    working