Strategy Pattern
We’ve been taking a closer look at the switch
statement and why we should avoid it in our code, here. And I’ve promised a better way which I will show you today. This way creates pluggable architecture. It is more aligned with the SOLID Principles’ Single Responsibility Principle (SRP) and the Open-Closed Principle (OCP).
We’ll work from our previous example of a Pay() method that uses a switch statement to pick a payment gateway to then pay for a shopping cart.
Again, here is the definition for Pay():
public void Pay(ShoppingCart cart, string paymentGateway) { switch (paymentGateway) { case "Paypal": // ... code to pay for shopping cart with Paypal. break; case "Stripe": // ... code to pay for shopping cart with Stripe. break; default: throw new UnknownPaymentGateway(paymentGateway); } }
If requirements changed to add another payment gateway, called “DPS”, we would need to insert the corresponding case statement for “DPS”.
If you want to know why we should change the implementation of this method to remove the switch, we have previously discussed that here.
Today I’ll show you how to switch dynamically between different payment gateways at runtime without a switch statement! To that end, we’ll use the Strategy Pattern. The simplicity of this pattern makes it one of my favourites.
OK, let’s get into it.
Let’s start by standardising the access to our payment gateways. We’ll need an interface, IPaymentGateway, that all our payment gateway implementations have in common:
public interface IPaymentGateway { string Name { get; } void Pay(ShoppingCart cart); }
Every implementation of IPaymentGateway has the Pay() method, to pay for the passed-in ShoppingCart. It also has a read-only Name property. The Name property is crucial – we’ll get to that next.
Analogously to our original switch statement code, we will have a PaypalPaymentGateway and a StripePaymentGateway. Both implement IPaymentGateway:
public class PaypalPaymentGateway : IPaymentGateway { public string Name => "Paypal"; public void Pay(ShoppingCart cart) { // ... code to pay for cart with Paypal. } } public class StripePaymentGateway : IPaymentGateway { public string Name => "Stripe"; public void Pay(ShoppingCart cart) { // ... code to pay for cart with Stripe. } }
Each IPaymentGateway class ‘knows’ their name. Each is forced to implement the Name property getter.
We also need another class to select the desired IPaymentGateway implementation at runtime. Here’s what PaymentGatewaySelector looks like:
public class PaymentGatewaySelector : IPaymentGatewaySelector { private IEnumerable<IPaymentGateway> PaymentGateways { get; } public PaymentGatewaySelector(IEnumerable<IPaymentGateway> paymentGateways) { PaymentGateways = paymentGateways; } public IPaymentGateway Select(string paymentGatewayName) { return PaymentGateways.SingleOrDefault(x => x.Name == paymentGatewayName); } }
A few things are going on here. Two are worth explaining in detail:
- PaymentGatewaySelector has a constructor that takes a collection of IPaymentGateways – this is how PaymentGatewaySelector receives an instance of PaypalPaymentGateway and StripePaymentGateway. In production code, we would likely inject instances of our concrete payment gateways using an Inversion of Control (IoC) container. Post-construction, a selector will contain in its PaymentGateways property an instance of each version of our IPaymentGateway implementers – one StripePaymentGateway and one PaypalPaymentGateway. If we had a DpsPaymentGateway, it would be in PaymentGateways too – as long as we have configured it in IoC.
- The Select() method is where all the magic happens. In here, we query our collection of IPaymentGateway instances to give us the one that matches on payment gateway name. It’s like the selector asking the group of payment gateways for the one, say, called “Stripe” to put up its ‘hand’. It’s how the selector picks the correct implementer of IPaymentGateway. If a payment gateway matches, it is returned.
Finally, the Pay() method employing the switch, now hosted inside a use case class, boils down to
public class PayForShoppingCartUseCase : IPayForShoppingCartUseCase { private IPaymentGatewaySelector PaymentGatewaySelector { get; } public PayForShoppingCartUseCase(IPaymentGatewaySelector paymentGatewaySelector) { PaymentGatewaySelector = paymentGatewaySelector; } public void Pay(ShoppingCart cart, string paymentGatewayName) { var paymentGateway = PaymentGatewaySelector.Select(paymentGatewayName); if (paymentGateway == null) throw new UnknownPaymentGateway(paymentGatewayName); paymentGateway.Pay(cart); } }
What is so excellent about this code is that it does not need to be modified when we are adding a new IPaymentGateway implementation! For example, when we need a “DPS” version, all we do is write new code in the form of a DpsPaymentGateway:
public class DpsPaymentGateway : IPaymentGateway { public string Name => "DPS"; public void Pay(ShoppingCart cart) { // ... code to pay for cart with DPS. } }
Since we wrote new code and didn’t need to modify existing code, we are (SOLID Principles) Open-Close Principle compliant. We are aligned with the Single Responsibility Principle because we have managed to move the detailed Paypal and Stripe implementations into separate classes.
The Strategy Pattern is a powerful ally in our quest to reduce the number of problematic switch statements.
I hope that helps.
Leave a Reply
Want to join the discussion?Feel free to contribute!