Monday, September 27, 2010

Test Driven Approach - Positive Impact for Developers - Workshop IV

Now we need to implement the Calculate method within Core.Calculator.

So, there are many ways this can be done and whether this method is developed to its best code is not the interest of the workshop. Here is my sample: -
using System;
using System.Collections.Generic;
using System.Linq;

namespace Calculator.Core
{
    public class Calculator
    {
        private const string Add = "+";
        private const string Sub = "-";
        private const string Div = "/";
        private const string Mul = "*";
        private const string Sqrt = "sqrt";
        private const string Percent = "%";
        private const string Reciproc = "reciproc";
        private const string Pow = "pow";

        private readonly string[] _keywords = new []{ Add, Sub, Div, Mul, Sqrt, Percent, Reciproc, Pow, "(", ")", ","};

        private readonly string[] _operatorKeywords = new[] { Add, Sub, Div, Mul, Sqrt, Percent, Reciproc, Pow };

        private static readonly Dictionary<string, Math.MathOperatorHandler> Operators;

        static Calculator()
        {
            Operators = new Dictionary<string, Math.MathOperatorHandler>
                             {
                                 {Add, Math.Add},
                                 {Reciproc, Math.Reciproc},
                                 {Pow, Math.Pow},
                                 {Sub, Math.Subtract},
                                 {Div, Math.Divide},
                                 {Mul, Math.Multiply},
                                 {Sqrt, Math.SquareRoot},
                                 {Percent, Math.Percent}
                             };
        }

        public string Calculate(string formula)
        {
            double temp;
            if (double.TryParse(formula, out temp))
            {
                return formula;
            }
            var values = formula.Split(_keywords, StringSplitOptions.RemoveEmptyEntries);
            if (values.Length == 0)
            {
                var msg = string.Format("No values to be calculated. Values length is zero for formula {0}.", formula);
                throw new UnexpectedCalculationException(msg);
            }

            var subPart = formula.TrimStart(values[0].ToCharArray());
            var opCode = _operatorKeywords.FirstOrDefault(subPart.StartsWith);

            if (string.IsNullOrEmpty(opCode))
            {
                var msg = string.Format("No calculation operator found in formula {0}.", formula);
                throw new UnexpectedCalculationException(msg);
            }

            Math.MathOperatorHandler mo;

            // this is only possible when there is a %.
            if (subPart.EndsWith(Percent))
            {
                if (!Operators.TryGetValue(Percent, out mo))
                {
                    var msg = string.Format("Cannot resolve calculation operator of {0}.", Percent);
                    throw new UnexpectedCalculationException(msg);
                }
                values[values.Length - 1] = mo(values.Select(item => double.Parse(item))).ToString();
            }

            if (!Operators.TryGetValue(opCode, out mo))
            {
                var msg = string.Format("Cannot resolve calculation operator of {0}.", opCode);
                throw new UnexpectedCalculationException(msg);
            }
            var valueArray = values.Select(item => double.Parse(item));
            var result = mo(valueArray);
            return result.ToString();
        }
    }
}

But what is more interesting is at the supporting class below: -
    internal static class Math
    {
        private delegate T TryExecuteHandler<out T>();

        internal delegate double MathOperatorHandler(IEnumerable<double> values);

        private static double Handle(TryExecuteHandler<double> handler)
        {
            try
            {
                return handler();
            }
            catch(FormatException e)
            {
                throw new IncompatibleDataTypeFormulaException("Unexpected data format.", e);
            }
        }

        internal static double Add(IEnumerable<double> values)
        {
            return Handle(() => values.Sum());
        }

        internal static double Subtract(IEnumerable<double> values)
        {
            return Handle(() =>
                              {
                                  var valueArray = values.ToArray();
                                  var result = valueArray[0];
                                  for (var i = 1; i < valueArray.Length; i++)
                                  {
                                      result -= valueArray[i];
                                  }
                                  return result;
                              });
        }

        internal static double Multiply(IEnumerable<double> values)
        {
            return Handle(() =>
                              {
                                  var valueArray = values.ToArray();
                                  var result = valueArray[0];
                                  for (var i = 1; i < valueArray.Length; i++)
                                  {
                                      result *= valueArray[i];
                                  }
                                  return result;
                              });
        }

        internal static double Divide(IEnumerable<double> values)
        {
            return Handle(() =>
                              {
                                  var valueArray = values.ToArray();
                                  var result = valueArray[0];
                                  for (var i = 1; i < valueArray.Length; i++)
                                  {
                                      result /= valueArray[i];
                                  }
                                  return result;
                              });
        }

        internal static double SquareRoot(IEnumerable<double> values)
        {
            return Handle(() =>
                              {

                                  var valueArray = values.ToArray();
                                  if (valueArray.Length > 1)
                                  {
                                      var msg = string.Format("Expect valueArray of length 1 but was {0}.", valueArray.Length);
                                      throw new UnexpectedCalculationException(msg);
                                  }

                                  return System.Math.Sqrt(valueArray[0]);
                              });
        }

        internal static double Pow(IEnumerable<double> values)
        {
            return Handle(() =>
                              {
                                  var valueArray = values.ToArray();
                                  var result = valueArray[0];
                                  for (var i = 1; i < valueArray.Length; i++)
                                  {
                                      result = System.Math.Pow(result, valueArray[i]);
                                  }
                                  return result;
                              });
        }

        internal static double Reciproc(IEnumerable<double> values)
        {
            return Handle(() =>
                              {
                                  var valueArray = values.ToArray();
                                  if (valueArray.Length > 1)
                                  {
                                      var msg = string.Format("Expect valueArray of length 1 but was {0}.", valueArray.Length);
                                      throw new UnexpectedCalculationException(msg);
                                  }

                                  return 1/valueArray[0];
                              });
        }

        internal static double Percent(IEnumerable<double> values)
        {
            return Handle(() =>
                              {
                                  var valueArray = values.ToArray();
                                  if (valueArray.Length != 2)
                                  {
                                      var msg = string.Format("Expect valueArray of length 2 but was {0}.", valueArray.Length);
                                      throw new UnexpectedCalculationException(msg);
                                  }
                                  return valueArray[0]*valueArray[1]/100;
                              });
        }
    }

In the unit tests, we expected two different kinds of exceptions as such: -
    [Serializable]
    public class IncompatibleDataTypeFormulaException : Exception
    {
        public IncompatibleDataTypeFormulaException(string message, Exception innerException)
            :base(message, innerException)
        {
        }
    }

    /// <summary>
    /// <see cref="UnexpectedCalculationException"/> - Exception indicates that there were some unexpected internal calculation error.
    /// </summary>
    [Serializable]
    public class UnexpectedCalculationException:ApplicationException
    {
        public UnexpectedCalculationException(string message)
            :base(message)
        {
        }
    }

Now that I don't want to make the case complicated by implementing a FormulaParser and FormulaToken all that sorts just for a simple calculator, I opted for the static class implementation named Math. This supporting class is never in my mind that will be shared. It is my internal calculation logic that is for abstraction. It has very high chances that I want improve it in the future.

This class should never be unit tested.

If you do, you will quickly find you do owe yourself for a blame. Unit testing your abstraction logic will pressurize on your responsibility of maintaining highest potential to change API, process workflow, class design and etc. You ended do a lot of work and then undone many work that you were spending day and night maintaining it.

Best practice of TDD said, let those highly changeable code (abstraction logic) automatically covered by just unit testing your public API. If it was not covered automatically, you have written unplanned code and that is the risk for defects or bugs. You will be helped by the coverage to uncover the statistic for this area of risk to you.


Note: The only place that is not well-tested is at UI.

Next, look at the abstraction logic again, there is a pattern of repeatedly throwing exception is needed for most of the function like Add, Divide, Multiply etc, this just comes naturally that I will need some aspect oriented programming for exception handling in this function. Hey, now I need to use AOP design patterns with Code Injection facility.

This is awesomely beautiful isn't it, doing it TDD?

No comments:

Post a Comment