Click here to Skip to main content
15,880,967 members
Articles / Programming Languages / C#

Encapsulated Decision Tables

Rate me:
Please Sign up or sign in to vote.
3.29/5 (3 votes)
13 Oct 2013CPOL6 min read 30.3K   431   13   7
A mechanism for specifying program control flow via delegated calls from a decision table

Introduction

Decision Tables are one of the oldest models for recording in writing or programming complex conditional logic. In .NET a Decision Table can be thought of as an instance of

Dictionary<TKey, Action<T1,..Tn>>

or

Dictionary<TKey, Func<T1,…Tn,TResult>>

For each KeyValuePair<TKey, TValue> in the Dictionary, the TKey completely defines the Condition under which the Action (a delegated call) specified in the TValue is executed. Decision Tables provide an elegant, easy to maintain and fast alternative to switch/case or if/else in complex logic. Use of a composite Key can actually reduce the total code size in cases where multiple state values must be evaluated to select the delegated call, a feature unavailable to switch/case and error prone and slow to implement in if/else.

Once created, the Dictionary can be initialized either with Add(TKey, TValue) or with a constructor initializer. The TValue can be initialized with a conforming method of the containing class, a conforming static member of separate class, or a lambda expression. However, having the initialized Decision Table exposed, for example, as a class member of its consuming code presents a potential for misuse since the Dictionary could be modified inadvertantly after initialization, leading to maintainability issues or unexpected results. This article shows a technique for fully encapsulating the Decision Table, closing the Dictionary from modification and enabling the Decision Table to be loaded into an IOC.

Background

The code provided operates on entities within a sample domain model, shown in static class diagram below. This model approximates that necessary for calculating taxes within a retail problem space.

Image 1

The principle concrete entities used in the code are

  • LineItem, an entity for which taxes must be calculated
  • TaxItem, an entity which retains the values of the calculated tax
  • TaxItemLink, an structural entity maintaining a relationship between a Source TaxItem and a Destination LineItem

The TaxItem contains a property of Type TaxAuthority. The instance assigned to this property might hold the parameterizations for a particular type of calculation in an actual system. Three concrete realizations of TaxAuthority are provided.

Not shown are three concrete realizations of ComputeTax each of which calculate the values for a corresponding concrete TaxAuthority instance. For simplicity, the Domain model  and computation entities are exceeding shallow, and the ComputeTax specializations simply return constant but unique values in a Tuple<decimal, decimal>. The Decision Tables constructed in the code relate a concrete TaxAuthority to an instance of its corresponding ComputeTax specialization. 

Encapsulating the Decision Table

The unit tests in the sample source in some ways show evolutionary steps in the development of this concept, illustrating a variety of ways of initializing the Dictionary and then invoking the delegation via an extension method on the Dictionary. As noted previously, in these implementations the Dictionary is not closed to modification and thus is susceptible to post-initialization traumas that could produce unexpected results at runtime. To preclude this, the Dictionary must be moved into a wrapper class as a member variable with a non-public access modifier.

The abstract generic class EncapsulatedDecisionTable is the root of the encapsulation hierarchy.

C#
    public abstract class EncapsulatedDecisionTable<TKey,TValue,TDomain>
        where TKey : class
        where TValue : class
        where TDomain : class
    {
        public readonly IKeyExtractor<TDomain, TKey> _extractor;
        protected Dictionary<TKey, TValue> _decisionTable;
 
        protected EncapsulatedDecisionTable(IKeyExtractor<TDomain,TKey> extractor)
        {
            _extractor = extractor;
        }
    }

The generic parameters are

  • TKey, the Key Type for the Dictionary
  • TValue, the Value Type for the Dictionary, either an Action<T1,...,Tn> or a Func<T1,...,TResult> as is appropriate
  • TDomain, a Domain Type from which a Key instance can be formulated

The wrapper has a protected member variable for the Dictionary and a non-default constructor whose parameter is a Type implementing IKeyExtractor

C#
public interface IKeyExtractor<TDomain,TKey>
    where TDomain : class
    where TKey : class
{
    TKey ExtractKey(TDomain arg);
}

The concrete implementation of IKeyExtractor must be able to return an instance of the TKey from analysis of an instance of the TDomain. Failure to do should should throw an appropriate Exception from the implementation. The extractor is cached in a public member variable on construction.

Two abstract subclasses of EncapsulatedDecisionTable are provided, one for use with TValues based on Action<T1,...,Tn> and the other for use with TValues based on Func<T1,...,TResult>. The latter has an additional generic parameter also of Type TResult.

C#
public abstract class ActionBasedEncapsulatedDecisionTable<TKey,TValue,TDomain>
    : EncapsulateDecisionTable<TKey,TValue,TDomain>
    where TKey : class
    where TValue : class
    where TDomain : class
{
    protected ActionBasedEncapsulatedDecisionTable(IKeyExtractor<TDomain, TKey> extractor)
        :base(extractor)
    {
    }

    protected virtual void Execute(TDomain arg)
    {
        throw new DecisionTableDispatchException
            (DecisionTableDispatchException.FormatMessage(arg));
    }
}
C#
public abstract class FuncBasedEncapsulatedDecisionTable<TKey,TValue,TDomain,TResult>
        : EncapsulateDecisionTable<TKey,TValue,TDomain>
        where TKey : class
        where TValue : class
        where TDomain : class
        where TResult : class
    {
        protected FuncBasedEncapsulatedDecisionTable(IKeyExtractor<TDomain, TKey> extractor)
            base(extractor)
        {
        }
 
        protected virtual TResult Execute(TDomain arg)
        {
            throw new DecisionTableDispatchException
                (DecisionTableDispatchException.FormatMessage(arg));
        }
    }

The virtual method Execute methods in in these classes dictate the signature of the call delegation functionality. The base implementation simply throws a DecisionTableDispatchException if called. The actual delegation must occur in a concrete subclass of the wrapper. The base Execute is provided to simplify coding in the case where the TKey instance does not exist in the Dictionary.

Each concrete subclass is responsible for creating and initialiizing its Dictionary and setting the _decisionTable member variable of the base class. This activity should occur during construction. In order to externally expose the call delegation functionality, each a concrete subclass must als implement either IEncapsulatedDecisionTable<TDomain>

C#
public interface IEncapsulatedDecisionTable<TDomain>
        where TDomain : class
    {
        void Execute(TDomain arg);
    }
 or IEncapsulatedDecisionTable<TDomain,TResult>
C#
public interface IEncapsulatedDecisionTable<TDomain,TResult>
    where TDomain : class
    where TResult : class
{
    TResult Execute(TDomain arg);
}

as is appropriate for the TValue.

If the goal is to have the Decision Table dynamically loaded from an IOC at runtime, the concrete subclass class must also implement IEncapsulatedDecisionTableIoc<TKey,TValue>

C#
public interface IEncapsulatedDecisionTableIoc<TKey,TValue>
                         where TKey: class
                         where TValue : class
{
    void Clear();
    void Add(TKey condition, TValue action);
}

While necessary to allow the Dictionary to be populated during IOC initialization, this interface breaks the encapsulation goal and therefore should never appear on a wrapper subclass intended to be directly created via the new operator.

Invocation of the delegated call in the wrapper subclass should always be identical, differing only in the generics associated with TValue and the TDomain assigned to the wrapper. Each subclass should override the Execute method similar to the following sample.

C#
public class UnfilledTaxDecisionTable :
        FuncBasedEncapsulatedDecisionTable<Type,
            Func<TaxItemLink, Tuple<decimal, decimal>>,
            TaxItemLink,
            Tuple<decimal, decimal>>,
        IEncapsulatedDecisionTable<TaxItemLink, Tuple<decimal, decimal>>
    {
        :
       public Tuple<decimal,decimal> Execute(TaxItemLink link)
        {
            Func<TaxItemLink, Tuple<decimal, decimal>>  f;
            if (_decisionTable.TryGetValue(_extractor.ExtractKey(link), out f))
            {
                return f(link);
            }
            return base.Execute(link); //always throws
        }
        :
    }

More Than Just a Wrapper

Wrappering for encapsulation is a well-known technique. However, moving programatic flow control into an IOC creates a Customization Seam when developing an SDK for a product. A Customization Seam describes a deliberate point in the software design where customization is meant to be absorbed, leveraging the most from the base product's normal behavior while limiting the scope of the required customization effort. In an SDK customization seams help define the assembly structure, source code delivery requirements, and required guidance documentation for doing the likely customizations.

The IOC unit test in the sample suggests the power of the concept

C#
public void TestIOCDecisionTable()
{
    var container = new UnityContainer();
    // Adding the extension initializes the DecisionTable
    container.AddNewExtension<ContainerExtension>();

    var link = new TaxItemLink
        {
            Destination = new LineItem(),
            Source = new TaxItem {Authority = new VATTax()}
        };

    Tuple<decimal, decimal> result =
        container.Resolve<IEncapsulatedDecisionTable<TaxItemLink,
            Tuple<Decimal, decimal>>>().Execute(link);
    Assert.True(result.Item1 == 3.0M);
}

In a software product, the base could be implemented very similarly to the above. The Customization Seam exists in how the ContainerExtension populates the underlying Decision Table.

c#"
internal class ContainerExtension : UnityContainerExtension
{
    :
    protected override void Initialize()
    {
        var scanner = new SimpleScanner();

        //find DecisionTable related entities
        Type decisionTableType = scanner.FindTypes(decisionTablePredicate).First();
        Type[] computerTypes = scanner.FindTypes(decisionEntryPredicate).ToArray();
        IEnumerable<Type> authorityTypes = scanner.FindTypes(taxAuthorityPredicate);

        //Create the decision table, injecting a key extractor
        object decisionTable = Activator.CreateInstance(decisionTableType,
            new object[] {new TaxKeyExtractor()});

        // Clear the internal table
        (decisionTable as IEncapsulatedDecisionTableIoc<Type, ComputeTax>).Clear();

        // load the table by (in this model) matching prefixes of Types
        Array.ForEach(computerTypes, c =>
            {
                string prefix = c.Name.Substring(0, c.Name.IndexOf("Compute"));
                Type authority = authorityTypes.FirstOrDefault(s => s.Name.StartsWith
                    (prefix));
                // Create the DecisionEntry
                object computeInstance = Activator.CreateInstance(c, false);
                // Load it to the DecisionTable
                (decisionTable as IEncapsulatedDecisionTableIoc<Type,
                    ComputeTax>).Add(authority,
                    computeInstance as ComputeTax);});

        // Register the DecisionTable in the IOC
        Container.RegisterInstance(typeof (IEncapsulatedDecisionTable<TaxItemLink,
            Tuple<decimal, decimal>>), decisionTable,
            new ContainerControlledLifetimeManager());
    }
    :
}

While this code simply reproduces the behavior of the other samples, the potential for different strategies of populating the Decision Table is vast and could include conventions for selecting a derived Type in preference to one of the base product Types when poplulating the Decision Table. Thus the customization guidance scope is limited topically, in this case, to the discussion of subclassing ComputeTax concrete types and deploying the resulting assemblies for runtime insertion by the IOC.

By no means is this recommended as a generic technique in every situation. However, runtime replacable logic is a powerful customization strategy.

Using the code

The code is delivered a single solution (VS2012) containing a single unit text project (xUnit.net). The various unit tests exercise all of the points discussed above in a variety of ways, including hoisting an encapsulated Decision Table into the Unity IOC container

History

     13 Oct 2013 Added "More than a Wrapper"; removed Point of Interest.

 

License

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


Written By
Architect SoftwareDo, Inc.
United States United States
I'm the President and Chief Architect of SoftwareDo, Inc. I've spent most of my career architecting, designing, and implementing large distributed compute systems for the retail industry, mostly focused on the Point-Of-Service problems. Most of this has work has been done on the Microsoft platforms.

I have extensive experience in COM, C++, Sql Server, and C#. I have a longterm interest in domain specific modeling leading to metadata driven code generation. I've been involved with WCF from the earliest TAP programs at Microsoft onward. My current focus is on distributed, enterprise level Service Oriented Applications using WCF and the Azure service bus.

I hold a BS in Physics from Northwest Missouri State University and a PhD in Astronomy from the University of Florida.

My major avocation for the last 40 years has been as a martial arts instructor. I hold a 4th degree blackbelt in karate and a 5th degree blackbelt in aikido.

Comments and Discussions

 
Question+ 5 Pin
RAND 45586613-Oct-13 10:29
RAND 45586613-Oct-13 10:29 
GeneralMy vote of 2 Pin
Paulo Zemek12-Oct-13 18:02
mvaPaulo Zemek12-Oct-13 18:02 
GeneralRe: My vote of 2 Pin
David Killian12-Oct-13 18:08
professionalDavid Killian12-Oct-13 18:08 
QuestionI am unsure about the entire idea. Pin
Paulo Zemek12-Oct-13 17:50
mvaPaulo Zemek12-Oct-13 17:50 
The introduction seems OK to me. In fact, replacing a switch by an array of delegates (if the key is numeric and without empty entries... like from 0 to 10) or using a dictionary (if the keys are of other types, if they are numeric but with lots of empty spaces or anything similar) is great.

Having the delegate strongly typed with Action<X, Y, Z> has a good performance (as it is already typed), but I will usually prefer dedicated delegate types if parameters are expected by the action as this usually makes things easier to understand and so is more maintainable.

But after the introduction, I am not really sure what you are trying to achieve. OK, I got the idea that the dictionary should not be public if you expect the decision table to remain intact after created, but the presented EncapsulatedDecisionTable seems an overcomplication to me. If you can simply create a DecisionTableForSomethingNeeded class, you can put the dictionary as a private field of such class, you can initialize the dictionary accordingly (and even use Tuples as keys if you want to avoid creating types for the key) yet the presented methods (like a Calculate) can receive all the parameters rightly typed and with appropriate names, create the tuple key, look the dictionary for the appropriate delegate and invoke it.

This way you will be avoiding classes with 3 or 4 generic parameters. You will also avoid the need of a IKeyExtractor interface and I can't say that you will be writing more code each time you need a decision table, as you will only need to add the methods that "use" the decision table.

Finally, I can say that I see real problems with 2 things:
* The names of your parameters. Usually classes with a single generic parameter use the name T. If there are more generic parameters with the same purpose, using T1, T2, T3 is acceptable. But look at the Dictionary class itself, it uses TKey, TValue. Even if I can be unsure what a Key means, it is more clear than K. Seeing the parameters K, V, T and R is already a big concern to me. You shouuld try to use TKey, TValue, TDomain and TResult.
* Having an abstract method that always has a similar implementation should be avoided. You could very easily have methods like ValidateSource and ValidateDestination as abstract to do the CheckExpectedType, so the properties themselves don't have to be overriden and it will not be possible to forget to override the properties, as those already have an implementation.
AnswerRe: I am unsure about the entire idea. Pin
David Killian12-Oct-13 19:43
professionalDavid Killian12-Oct-13 19:43 
GeneralRe: I am unsure about the entire idea. Pin
Paulo Zemek13-Oct-13 5:47
mvaPaulo Zemek13-Oct-13 5:47 
AnswerRe: I am unsure about the entire idea. Pin
Artem Elkin23-Oct-13 0:22
professionalArtem Elkin23-Oct-13 0:22 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.