Tuesday, April 19, 2016

A simple rules engine

I'm extracting data from some OCR'd letters and, in order to determine which type of letter I'm parsing, I'm using a method similar to this:

        public Letter Parse(string text)
        {
            Letter result;
            if (text.IndexOf("...", StringComparison.OrdinalIgnoreCase) >= 0)
                letter = new LetterA();
            else
                letter = new LetterB();

            //... additional processing
            return letter;
        }

If the letter contains a specific text, I know it's of one type; otherwise I'll default to the other type. Unfortunately that's going to get really complicated, really fast once I start adding new letter types. I read somewhere that "you should move logic out of the code and into the data when possible"; it made sense and I never had a reason to regret it. So, let me try to do that here.

First I'll add a "rules list" class that will allow me to store the various criteria:

    public class RulesList<T, TResult>
    {
        public RulesList()
        {
            rules = new List<Tuple<Predicate<T>, Func<TResult>>>();
        }

        public void Add(Predicate<T> condition, Func<TResult> constructor)
        {
            rules.Add(Tuple.Create(condition, constructor));
        }

        public TResult Get(T criteria)
        {
            return rules
                .Where(rule => rule.Item1(criteria))
                .Select(rule => rule.Item2())
                .FirstOrDefault();
        }

        //

        private readonly List<Tuple<Predicate<T>, Func<TResult>>> rules;
    }

I've made this class more generic by replacing the string type with T; honestly, I don't think there will ever be a need for anything else in this project but… it wasn't a big "expense".

Using this is quite simple at the moment:

    public class LetterSelector
    {
        public LetterSelector()
        {
            rules = new RulesList<string, Letter>();

            rules.Add(s => s.IndexOf("...", StringComparison.OrdinalIgnoreCase) >= 0, () => new LetterA());
            rules.Add(_ => true, () => new LetterB());
        }

        public Letter Parse(string text)
        {
            var letter = rules.Get(text);
            //... additional processing

            return letter;
        }

        //

        private readonly RulesList<string, Letter> rules;
    }

Is this a big gain? Right now it doesn't look like I gained anything; however, bitter experience taught me that methods with many conditionals quickly become an unmaintainable mess (you haven't lived until you've had to fix a method with 700+ lines and a cyclomatic complexity over 200). This will allow me to separate those conditionals into their own lambdas or small private methods in the LetterSelector class.

No comments: