Design Patterns - Strategy

Strategy pattern is also called Policy pattern

Strategy Pattern Characteristics

  • Context: has a reference to a strategy and invokes it
    • Calls IStrategy.Method(object);
  • IStrategy: Defines the interface for the given strategy
    • Defines the contract Method(object)
  • Strategy: A concrete implementation of the strategy
    • Implementation of Method(object)

Select an implementation at runtime based on user input without having to extend the class.

Example: ISalesTaxStrategy is an interface. We have multiple different implementations of Strategies to calculate tax. They all implement the ISalesTaxStrategy interface.

The code below doesn’t need to know what Strategy is chosen at this step. It only needs to invoke the GetTaxFor() Method.

1
2
3
4
5
6
public ISalesTaxStrategy SalesTaxStrategy { get; set; }

public decimal GetTax()
{
return SalesTaxStrategy == null ? 0m : SalesTaxStrategy.GetTaxFor(this);
}

What did we achieve?

  • A more extensible, object oriented and dynamic implementation
  • Easily add new strategies without affecting existing ones
  • Cleaner approach with single responsiblity in mind

Another thing we could do is to pass the interface to the GetTax() method.

1
2
3
public decimal GetTax(ISalesTaxStrategy salesTaxStrategy) {
return salesTaxStrategy == null ? 0m : salesTaxStrategy.GetTaxFor(this);
}

And the concrete implementation of the strategy could be determined when we invoke the GetTax() Method

1
order.GetTax(new SwedenSalesTaxStrategy()

This is still meaning we have a hard dependency between the Order and the SalesTaxStrategy

Strategy Pattern with Dependency Injection

Pass the already created SalesTaxStrategy to the Order Contructor will help us remove the hard dependency between the Order and the Strategy.

1
2
3
4
5
6
7
8
9
10
private ISalesTaxStrategy _salesTaxStrategy;
private IInvoiceStrategy _invoiceStrategy;
private IShippingStrategy _shippingStrategy;

public Order(ISalesTaxStrategy salesTaxStrategy, IInvoiceStrategy invoiceStrategy, IShippingStrategy shippingStrategy)
{
_salesTaxStrategy = salesTaxStrategy;
_invoiceStrategy = invoiceStrategy;
_shippingStrategy = shippingStrategy;
}

Then Order(Context in Strategy Pattern) just need to invoke Strategy implementations without having to know which imeplementation it is invoking.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public decimal GetTax()
{
return _salesTaxStrategy == null ? 0m : _salesTaxStrategy.GetTaxFor(this);
}

public void FinalizeOrder()
{
if (SelectedPayments.Any(x => x.PaymentProvider == PaymentProvider.Invoice) && AmountDue > 0 && ShippingStatus == ShippingStatus.WaitingForPayment)
{
_invoiceStrategy.Generate(this);
ShippingStatus = ShippingStatus.ReadyForShippment;
}
else if (AmountDue > 0)
{
throw new Exception("Unable to finalize order");
}

_shippingStrategy.Ship(this);
}

On Application start we create different Strategies based on user input

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
switch (origin)
{
case EnumTaxStrategy.Sweden:
salesTaxStrategy = new SwedenSalesTaxStrategy();
break;
case EnumTaxStrategy.USA:
salesTaxStrategy = new USAStateSalesTaxStrategy();
break;
default:
salesTaxStrategy = new SwedenSalesTaxStrategy();
break;
}

switch (inputInvoiceStrategy)
{
case EnumInvoiceStrategy.Email:
invoiceStrategy = new EmailInvoiceStrategy();
break;
case EnumInvoiceStrategy.File:
invoiceStrategy = new FileInvoiceStrategy();
break;
case EnumInvoiceStrategy.PrintOnDemand:
invoiceStrategy = new PrintOnDemandInvoiceStrategy();
break;
default:
invoiceStrategy = new FileInvoiceStrategy();
break;
}

switch (inputShippingStrategy)
{
case EnumShippingStrategy.DHL:
shippingStrategy = new DHLShippingStrategy();
break;
case EnumShippingStrategy.Fedex:
shippingStrategy = new FedexShippingStrategy();
break;
case EnumShippingStrategy.SwedishPostalService:
shippingStrategy = new SwedishPostalServiceShippingStrategy();
break;
case EnumShippingStrategy.UPS:
shippingStrategy = new UPSShippingStrategy();
break;
case EnumShippingStrategy.USPS:
shippingStrategy = new UnitedStatesPostalServiceShippingStrategy();
break;
default:
shippingStrategy = new DHLShippingStrategy();
break;
}

Summary

  • One of the most commonly used patterns
  • Decouple the context and the concrete implementation
  • Allows for a cleaner implementation in the context
  • Easily extend with additional startegies without affecting current implementations
  • Makes testing a lot easier as you can write mocked implementations to inject
  • Identify existing implementations and where you have used the pattern before