OdeToCode IC Logo

Modeling Flowcharts

Monday, November 10, 2008

A few months ago I worked on a system that was based on a set specifications that included some gnarly flowcharts (see pages 7 – 17 for an example). The good news was that the specs were concrete and readily available. The bad news was that the specs change every 6 months.

I explored a number of options for modeling the logic in the flowcharts, including WF, WF rules, and code generation from XML, but ultimately decided on staying as close as possible to the flowcharts with C# code. Maintainability and testability were the keys to survival. The end result of building a flowchart in code looks like the following:

var flowChart = new Flowchart<PnemoniaCaseData, MeasureResult>()
   // ... 
   .WithShape("TransferFromAnotherED")
     .RequiresField(pd => pd.TransferFromAnotherED)
       .WithArrowPointingTo("Rejected")
         .AndTheRule(pd => pd.TransferFromAnotherED.IsMissing)
       .WithArrowPointingTo("Excluded")
         .AndTheRule(pd => pd.TransferFromAnotherED == YesNoAnswer.Yes)
       .WithArrowPointingTo("PointOfOriginForAdmissionOrVisit")
         .AndTheRule(pd => pd.TransferFromAnotherED == YesNoAnswer.No)
    .WithShape("PointOfOriginForAdmissionOrVisit")
      .RequiresField(pd => pd.PointOfOriginForAdmissionOrVisit)
      // ... lots more of the same
    .WithShape("Rejected")
        .YieldingResult(MeasureResult.Rejected)
    .WithShape("Excluded")
      .YieldingResult(MeasureResult.Excluded)
    .WithShape("InDenominator")
      .YieldingResult(MeasureResult.InMeasurePopulation)
    .WithShape("InNumerator")
      .YieldingResult(MeasureResult.InNumeratorPopulation);

The flowcharts, particularly the lengthier ones, were still tedious to build, but overall I think this approach gave us something that we could use to crank out the 30 odd flowcharts in a maintainable, testable, and arguably more readable fashion than some of the other methods I considered. All the string literals tend to make me nervous, but they mostly match a corresponding shape in the specification, making an eyeball comparison easier. Typographical errors in the strings are easily negated with tests that use LINQ queries. For example, there should never be two shapes in the same flowchart with the same name:

var duplicateShapes = Shapes.GroupBy(s => s.Name).Where(g => g.Count() > 1);

And there should never be an arrow pointing to a shape name that doesn’t exist:

var names = Shapes.Select(s => s.Name);
var problemTransitions = Shapes.SelectMany(s => s.Arrows)
                               .Where(a => !names.Contains(a.PointsTo));

The classes used to model the flowchart were relatively simple. For example, here is the Shape class that takes generic parameters to define the type of data it operates on, and the type of the result it can yield.

public class Shape<T,R>
{
    public Shape()
    {
        Arrows = new List<Arrow<T>>();
        Result = default(R);
    }
    public R Result { get; set; }        
    public string Name { get; set; }
    public PropertySpecifier<T> RequiredField { get; set; }
    public List<Arrow<T>> Arrows { get; set; }
}

Every shape contains zero or more arrows:

public class Arrow<T>
{
    public string PointsTo { get; set; }
    public Func<T, bool> Rule { get; set; }
    public Action<T> Action { get; set; }
}

I learned a lot about the capabilities of LINQ, internal DSLs, and functional programming to some degree. If there is some interest, I thought it might be interesting to dig into some of these details in future posts…