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.
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.
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:
And we can instantiate it as shown below.
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.
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.
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.
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
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.
This allows us to add as much desired outcomes as we wish for the Rule in clean and a nice way as shown below.
"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.
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
© 2014 softomiser