Software Design Blog

Simple solutions to solve complex problems

Kill the switch with the strategy pattern

Chain of Responsibility

The previous post described the challenge of validating credit card numbers based on the credit card type such as visa or master card. Many if conditional checks were performed which was improved using the switch statement.

Why was the switch statement bad?

Let's say you have 5 cases and 10 lines each.

  • What if we had to add, remove or modify a case? The class has 7 reasons to change, which violates the open/close principle.
  • At least 5 tests are required to test each case. The class does 5 things, so it violates the single responsibility principle.
  • The class is at least 50 lines long without the possibility of reusing anything.

This post will provide an illustration of using the strategy design pattern with table control flow to replace the switch statement.

The problem is how we can define a family of algorithms (credit card validators), encapsulate each one, and make them interchangeable?

The solution is to use the strategy design pattern that allows the algorithm to vary independently from clients that use it.

Download Source Code

Setup

The following classes from the previous post will be reused.
  
    public class CreditCard
    {
        public string Type { get; set; }
        public string Name { get; set; }
        public string Number { get; set; }
        public string Expiry { get; set; }
    }

    public interface ICreditCardValidator
    {
        void Validate(CreditCard creditCard);
    }

Strategy Pattern

Let's get started with the strategy pattern in 5 easy steps.

1. Specify the signature for the algorithm in an interface

We need to validate a credit card number so we need a method that takes a number as shown below.

  
    public interface ICreditCardNumberValidator
    {
        void Validate(string number);
    }
2. Bury the algorithm implementation details in derived classes

Each credit card validator implementation can be different. In this example, all credit card validators will use a regular expression so we only need one class.

  
    public class RegExCreditCardNumberValidator : ICreditCardNumberValidator
    {
        private readonly Regex _regex;

        public RegExCreditCardNumberValidator(Regex regex)
        {
            if (regex == null) throw new ArgumentNullException("regex");
            _regex = regex;
        }

        public void Validate(string number)
        {
            if (!_regex.IsMatch(number)) throw new InvalidCreditCardException();
        }
    }

A new instance of the class will be created for each credit card type. For example:

  
  var visaRegEx = new Regex("^4[0-9]{6,}$", RegexOptions.Compiled);
  var visaValidator = new RegExCreditCardNumberValidator(visaRegEx);
3. Identify an algorithm that the client would prefer to access

The credit card validation algorithm is based on the credit card type. The credit card type is supplied in the credit card model/DTO. Therefore, we can perform an algorithm lookup based on the credit card type.

A dictionary is great way to perform a lookup, where the key represents the credit card type and the value represents the validator.

  
  var strategies = new Dictionary<string, ICreditCardNumberValidator>
                         (StringComparer.OrdinalIgnoreCase);

The strategy validator is shown below.

  
    public class CreditCardValidatorStrategy : ICreditCardValidator
    {
        private readonly IDictionary<string, ICreditCardNumberValidator> _strategies;

        public CreditCardValidatorStrategy(
                        IDictionary<string, ICreditCardNumberValidator> strategies)
        {
            if (strategies == null) throw new ArgumentNullException("strategies");
            _strategies = strategies;
        }

        public void Validate(CreditCard creditCard)
        {
            if (creditCard == null) throw new ArgumentNullException("creditCard");
            if (string.IsNullOrWhiteSpace(creditCard.Type)) 
               throw new ArgumentException(Resource.MissingCreditCardType);
            if (!_strategies.ContainsKey(creditCard.Type)) 
               throw new InvalidCardException(
                   string.Format(Resource.UnsupportedCreditCard, creditCard.Type));
            _strategies[creditCard.Type].Validate(creditCard.Number);
        }
    }
4. Building the table control lookup

We can build the dictionary lookup table based on a repository such as a database or a configuration file.

The credit card types and regular expressions are defined in the App.config as shown below.

 
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="creditcardnumberexpressions" 
             type="System.Configuration.DictionarySectionHandler" />
  </configSections>  
  <creditcardnumberexpressions>
      <add key="visa" value="^4[0-9]{6,}$" />
      <add key="mastercard" value="^5[1-5][0-9]{5,}$" />
  </creditcardnumberexpressions>
</configuration>

We can compose the dictionary of strategies from the App.config as shown below.

  
    public class ConfigDictionaryLoader : ISettingsLoader
    {
        private readonly string _sectionName;

        public ConfigDictionaryLoader(string sectionName)
        {
            if (sectionName == null) throw new ArgumentNullException("sectionName");
            _sectionName = sectionName;
        }

        public IDictionary<string, string> Load()
        {
            var settings = ConfigurationManager.GetSection(_sectionName) as Hashtable;
            if (settings == null) {
               throw new Exception(string.Format(Resource.MissingConfig, _sectionName));
            }
            return settings.Cast<DictionaryEntry>()
                           .ToDictionary(n => n.Key.ToString(), n => n.Value.ToString());
        } 
    }

    public class CreditCardValidatorFactory : ICreditCardValidatorFactory
    {
        private readonly ISettingsLoader _loader;

        public CreditCardValidatorFactory(ISettingsLoader loader)
        {
            if (loader == null) throw new ArgumentNullException("loader");
            _loader = loader;
        }

        public IDictionary<string, ICreditCardNumberValidator> CreateStrategies()
        {
            var cardPairs = _loader.Load();
            var strategies = new Dictionary<string, ICreditCardNumberValidator>
                                   (StringComparer.OrdinalIgnoreCase);
            foreach (var pair in cardPairs)
            {
                var regEx = new Regex(pair.Value, RegexOptions.Compiled);
                strategies[pair.Key] = new RegExCreditCardNumberValidator(regEx);
            }
            return strategies;
        }
    }
5. Running the solution
The entire solution is wired up as shown below.
 
            var configLoader = new ConfigDictionaryLoader("creditcardnumberexpressions");
            var creditCardFactory = new CreditCardValidatorFactory(configLoader);
            var strategies = creditCardFactory.CreateStrategies();
            var validator = new CreditCardValidatorStrategy(strategies);
           
            var creditCard = new CreditCard()
            {
                Type = "ViSa",
                Number = "4111111111111111"
            };

            validator.Validate(creditCard);

Summary

The strategy pattern provides a great alternative to the switch statement.

The advantages of the strategy design pattern are:

  • Reduces long lists of conditions such as If and Switch statements
  • Avoids duplicate code
  • Changes in one class does not require changes in other classes
  • The Open-close principle is achieved since the class is open for extension but closed for modification
  • The Single responsibility principle is achieved since the complexity is encapsulated per strategy
  • Unit testing is simplified since each strategy can be tested in isolation