Tuesday, December 10, 2013

Understanding and Implementing Design Patterns - Strategy Pattern

Extending Functionality with the Strategy Pattern


The strategy pattern enables the functionality of an application to be easily extended or modified to cope with new or changed requirements, as shown in the following example.
The code below shows the OrderProcess class which is responsible for processing the payment of an order as shown in the ProcessOrderPayment method
    public class OrderProcess
    {
        private readonly IOrderRepository repository;

        public OrderProcess(IOrderRepository repository)
        {
            this.repository = repository;
        }

        public void ProcessOrderPayment(Order order, PaymentDetails paymentDetails)
        {
            //Work out the order's price
            CalculateOrderPrice(order);

            //Process the payment
            switch (paymentDetails.Method)
            {
                case PaymentMethod.CreditCard:
                    ProcessCreditCardPayment(order.Price, paymentDetails);
                    order.PaymentTaken = true;
                    break;
                case PaymentMethod.Cheque:
                    ProcessChequePayment(order.Price, paymentDetails);
                    order.PaymentTaken = true;
                    break;
            }

            //Persist changes to the order
            repository.Save(order);
        }

        private void CalculateOrderPrice(Order order)
        {
            //Code to calculate order price
            order.Price = 500;
        }

        private void ProcessCreditCardPayment(int amount, PaymentDetails paymentDetails)
        {
            //Code to process credit card payment
        }

        private void ProcessChequePayment(int amount, PaymentDetails paymentDetails)
        {
            //Code to process cheque payment
        }
    }
The current implementation has the following issues
  • If a payment method is added the OrderProcess class will have to be changed, therefore violating the open closed principal
  • If the implementation of a payment method is changed, e.g. a new payment service provider used to process credit card payments the OrderProcess class is not isolated from this change.
  • The class is hard to unit test as it has numerous responsibilities and therefore it is difficult to setup tests to ensure all possible code paths are tested
  • The OrderProcess class will get very large and complex if more payment methods are added
To solve the problems listed above we need to isolate the functionality that is most likely to change, in our example this is the processing of payments, this can be achieve by using the strategy pattern; below is the description from the GoF book.
“Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently”
Shown below is the PaymentStrategyBase class which is the base class for the various payment mechanisms.
    public abstract class PaymentStrategyBase
    {
        protected PaymentDetails PaymentDetails { getprivate set; }

        protected PaymentStrategyBase(PaymentDetails paymentDetails)
        {
            PaymentDetails = paymentDetails;
        }

        public abstract void Process(int amount);
    }
Next shown are the implementations of the strategies for handling credit card payments and cheque payments.
    public class CreditCardPaymentStrategy : PaymentStrategyBase
    {
        public CreditCardPaymentStrategy(PaymentDetails paymentDetails) : base(paymentDetails){}

        public override void Process(int amount)
        {
            //Process credit card payment
        }
    }
    public class ChequePaymentStrategy : PaymentStrategyBase
    {
        public ChequePaymentStrategy(PaymentDetails paymentDetails) : base(paymentDetails) { }

        public override void Process(int amount)
        {
            //Process cheque payment
        }
    }
Below is the modified OrderProcess class which now uses the new classes
    public class OrderProcess
    {
        private readonly IOrderRepository repository;

        public OrderProcess(IOrderRepository repository)
        {
            this.repository = repository;
        }

        public void ProcessOrderPayment(Order order, PaymentStrategyBase payment)
        {
            //Work out the order's price
            CalculateOrderPrice(order);

            //Process the payment
            payment.Process(order.Price);
            order.PaymentTaken = true;

            //Persist changes to the order
            repository.Save(order);
        }

        private void CalculateOrderPrice(Order order)
        {
            //Code to calculate order price
            order.Price = 500;
        }
    }
As you can see the OrderProcess class is now a lot simpler so therefore is much easier to maintain and write unit tests for, also the different strategies are simple to maintain and test. Now the ProcessOrderPayment method has no knowledge of how the payment is being processed so therefore a new payment strategy implementation could be added without modifying the OrderProcess class or the implementation of an existing strategy changed. For example if a new requirement was that the application needs to be able to process payments via PayPal we could implement the class shown below.
    public class PayPalPaymentStrategy : PaymentStrategyBase
    {
        public PayPalPaymentStrategy(PaymentDetails paymentDetails) : base(paymentDetails) { }

        public override void Process(int amount)
        {
            //Process paypal payment
        }
    }
For the application to make use of the new PayPalPaymentStrategy class we would need to ensure that the code that uses the OrderProcess class instantiates the correct strategy to pass to the ProcessOrderPayment method, this creation logic could be encapsulated within a factory.

No comments:

Post a Comment