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: }
</p>
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: }
</p>
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: }
</div> </div>
This is part of the 31 Days of Refactoring series. For a full list of Refactorings please see the original introductory post.