Click here to Skip to main content
15,887,374 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 
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 
Decision table from the article is an entity that implements 2 interfaces: mutable interface (with the postfix "Ioc") and immutable one. The immutable interface is supposed to be injected to the consumer code. The mutable one is needed to register the decision entries. It is used by the created unity container extension that scans the assembly(ies), gathers classes implementing the interface IComputeTax, determines the Tax types (convention over configuration) and then populates the decision table. So you can add a new decision table entry just by creating a new class implementing IComputeTax.

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.