Los Techies : Blogs about software and anything tech!

Refactoring Day 11 : Switch to Strategy


Todays refactoring doesn't come from any one source, rather I've used different versions over the years and I'm sure other have different variations of the same aim.

This refactoring is used when you have a larger switch statement that continually changes because of new conditions being added. In these cases it’s often better to introduce the strategy pattern and encapsulate each condition in it’s own class. The strategy refactoring I’m showing here is refactoring towards a dictionary strategy. There is several ways to implement the strategy pattern, the benefit of using this method is that consumers needn’t change after applying this refactoring.

   1: namespace LosTechies.DaysOfRefactoring.SwitchToStrategy.Before
   2: {
   3:     public class ClientCode
   4:     {
   5:         public decimal CalculateShipping()
   6:         {
   7:             ShippingInfo shippingInfo = new ShippingInfo();
   8:             return shippingInfo.CalculateShippingAmount(State.Alaska);
   9:         }
  10:     }
  11:  
  12:     public enum State
  13:     {
  14:         Alaska,
  15:         NewYork,
  16:         Florida
  17:     }
  18:  
  19:     public class ShippingInfo
  20:     {
  21:         public decimal CalculateShippingAmount(State shipToState)
  22:         {
  23:             switch(shipToState)
  24:             {
  25:                 case State.Alaska:
  26:                     return GetAlaskaShippingAmount();
  27:                 case State.NewYork:
  28:                     return GetNewYorkShippingAmount();
  29:                 case State.Florida:
  30:                     return GetFloridaShippingAmount();
  31:                 default:
  32:                     return 0m;
  33:             }
  34:         }
  35:  
  36:         private decimal GetAlaskaShippingAmount()
  37:         {
  38:             return 15m;
  39:         }
  40:  
  41:         private decimal GetNewYorkShippingAmount()
  42:         {
  43:             return 10m;
  44:         }
  45:  
  46:         private decimal GetFloridaShippingAmount()
  47:         {
  48:             return 3m;
  49:         }
  50:     }
  51: }

To apply this refactoring take the condition that is being tested and place it in it’s own class that adheres to a common interface. Then by passing the enum as the dictionary key, we can select the proper implementation and execute the code at hand. In the future when you want to add another condition, add another implementation and add the implementation to the ShippingCalculations dictionary. As I stated before, this is not the only option to implement the strategy pattern. I bold that because I know someone will bring this up in the comments :)  Use what works for you. The benefit of doing this refactoring in this manner is that none of your client code will need to change. All of the modifications exist within the ShippingInfo class.

Jayme Davis pointed out that doing this refactoring really only ceates more classes because the binding still needs to be done via the ctor, but would be more beneficial if the binding of your IShippingCalculation strategies can be placed into IoC and that allows you to wire up strategies more easily.

   1: using System.Collections.Generic;
   2:  
   3: namespace LosTechies.DaysOfRefactoring.SwitchToStrategy.After
   4: {
   5:     public class ClientCode
   6:     {
   7:         public decimal CalculateShipping()
   8:         {
   9:             ShippingInfo shippingInfo = new ShippingInfo();
  10:             return shippingInfo.CalculateShippingAmount(State.Alaska);
  11:         }
  12:     }
  13:  
  14:     public enum State
  15:     {
  16:         Alaska,
  17:         NewYork,
  18:         Florida
  19:     }
  20:  
  21:     public class ShippingInfo
  22:     {
  23:         private IDictionary<State, IShippingCalculation> ShippingCalculations { get; set; }
  24:  
  25:         public ShippingInfo()
  26:         {
  27:             ShippingCalculations = new Dictionary<State, IShippingCalculation>
  28:             {
  29:                 { State.Alaska, new AlaskShippingCalculation() },
  30:                 { State.NewYork, new NewYorkShippingCalculation() },
  31:                 { State.Florida, new FloridaShippingCalculation() }
  32:             };
  33:         }
  34:  
  35:         public decimal CalculateShippingAmount(State shipToState)
  36:         {
  37:             return ShippingCalculations[shipToState].Calculate();
  38:         }
  39:     }
  40:  
  41:     public interface IShippingCalculation
  42:     {
  43:         decimal Calculate();
  44:     }
  45:  
  46:     public class AlaskShippingCalculation : IShippingCalculation
  47:     {
  48:         public decimal Calculate()
  49:         {
  50:             return 15m;
  51:         }
  52:     }
  53:  
  54:     public class NewYorkShippingCalculation : IShippingCalculation
  55:     {
  56:         public decimal Calculate()
  57:         {
  58:             return 10m;
  59:         }
  60:     }
  61:  
  62:     public class FloridaShippingCalculation : IShippingCalculation
  63:     {
  64:         public decimal Calculate()
  65:         {
  66:             return 3m;
  67:         }
  68:     }
  69: }

To take this sample full circle, Here is how you would wire up your bindings if you were using Ninject as your IoC container in the ShippingInfo constructor. Quite a few things changed here, mainly the enum for the state now lives in the strategy and ninject gives us a IEnumerable of all bindings to the constructor of IShippingInfo. We then create a dictionary using the state property on the strategy to populate our dictionary and the rest is the same. (thanks to Nate Kohari and Jayme Davis)

   1: public interface IShippingInfo
   2: {
   3:     decimal CalculateShippingAmount(State state);
   4: }
   5:  
   6: public class ClientCode
   7: {
   8:     [Inject]
   9:     public IShippingInfo ShippingInfo { get; set; }
  10:  
  11:     public decimal CalculateShipping()
  12:     {
  13:         return ShippingInfo.CalculateShippingAmount(State.Alaska);
  14:     }
  15: }
  16:  
  17: public enum State
  18: {
  19:     Alaska,
  20:     NewYork,
  21:     Florida
  22: }
  23:  
  24: public class ShippingInfo : IShippingInfo
  25: {
  26:     private IDictionary<State, IShippingCalculation> ShippingCalculations { get; set; }
  27:  
  28:     public ShippingInfo(IEnumerable<IShippingCalculation> shippingCalculations)
  29:     {
  30:         ShippingCalculations = shippingCalculations.ToDictionary(calc => calc.State);
  31:     }
  32:  
  33:     public decimal CalculateShippingAmount(State shipToState)
  34:     {
  35:         return ShippingCalculations[shipToState].Calculate();
  36:     }
  37: }
  38:  
  39: public interface IShippingCalculation
  40: {
  41:     State State { get; }
  42:     decimal Calculate();
  43: }
  44:  
  45: public class AlaskShippingCalculation : IShippingCalculation
  46: {
  47:     public State State { get { return State.Alaska; } }
  48:  
  49:     public decimal Calculate()
  50:     {
  51:         return 15m;
  52:     }
  53: }
  54:  
  55: public class NewYorkShippingCalculation : IShippingCalculation
  56: {
  57:     public State State { get { return State.NewYork; } }
  58:  
  59:     public decimal Calculate()
  60:     {
  61:         return 10m;
  62:     }
  63: }
  64:  
  65: public class FloridaShippingCalculation : IShippingCalculation
  66: {
  67:     public State State { get { return State.Florida; } }
  68:  
  69:     public decimal Calculate()
  70:     {
  71:         return 3m;
  72:     }
  73: }

This is part of the 31 Days of Refactoring series. For a full list of Refactorings please see the original introductory post.

Kick It on DotNetKicks.com
Posted Aug 11 2009, 08:30 AM by schambers

Comments

Mihai wrote re: Refactoring Day 11 : Switch to Strategy
on 08-11-2009 1:36 PM

Finally, something I like. Thanks!

Paco wrote re: Refactoring Day 11 : Switch to Strategy
on 08-11-2009 2:44 PM

Why doesn't a state have a shipping calculation when there is a 1 : 1 relation between state and shipping calculation?

Brad wrote re: Refactoring Day 11 : Switch to Strategy
on 08-12-2009 6:29 PM

@Paco

While the example shows very specific cases, it could be assumed that there are more generic/shared classes that could be used across multiple states, e.g. Region based shipping.

Edin wrote re: Refactoring Day 11 : Switch to Strategy
on 08-14-2009 3:30 AM

Quite good use of strategy pattern.

Arnis L. wrote re: Refactoring Day 11 : Switch to Strategy
on 09-08-2009 9:28 AM

Have done this before i knew it has a name. :)

31 Days of Refactoring « Vincent Leung's .NET Tech Clips wrote 31 Days of Refactoring &laquo; Vincent Leung&#039;s .NET Tech Clips
on 10-28-2009 9:28 AM

Pingback from  31 Days of Refactoring « Vincent Leung's .NET Tech Clips

PetterLiu wrote 31 Days of Refactoring
on 11-27-2009 4:01 AM

Refactoring Day 1 : Encapsulate Collection Refactoring Day 2 : Move Method Refactoring Day 3 : Pull ...

PetterLiu wrote 31 Days of Refactoring
on 11-27-2009 4:03 AM

Refactoring Day 1 : Encapsulate Collection Refactoring Day 2 : Move Method Refactoring Day 3 : Pull ...

Add a Comment

(required)  
(optional)
(required)  
Remember Me?

Enter the numbers above:
Copyright Los Techies 2008, 2009. All rights reserved.
Powered by Community Server (Commercial Edition), by Telligent Systems