Strategy Pattern :Jazz Up!

Strategy Pattern :Jazz Up!

One of the quite annoying things I have had to deal with in my journey as a software developer is the fact that requirements change over time and there is nothing I can do about it, even more frequently in enterprise apps πŸ™„πŸ˜ͺ.

I found the fact that whatever module I could be working on at the moment, could surprise me with new requirements the following day - and I was somehow supposed to accommodate this -very frustratingπŸ˜‘πŸ˜‘. This resulted in my search for what I needed to know to build apps that could morph quite easily as these inevitable changes in requirements reared their ugly headsπŸ˜’.

Fortunately, this frustration wasn't peculiar to me. Erich Gamma, John Vlissides, Ralph Johnson, Richard Helm (the Gang of Four), had experienced it and documented a series of steps that had proven over time to be solutions to a good number of the issues that ever-changing requirements for software applications birthed πŸ₯Ί.

I intend to write about all I have learnt from my study of the GOF design patterns as it looks like it could help me with my frustration, beginning with the Strategy Pattern 😁.

Strategy Pattern

In OOP terms, the strategy pattern creates objects which represent various strategies and a context object whose behaviour varies as per its strategy object. The strategy object changes the executing algorithm of the context object.

It is not as complicated as it sounds. We'll apply the strategy pattern in this article to show that.

Problem

Say for instance in a cool application you are developing for a bank, that only allowed customers to withdraw money by withdrawal slips at the time you received the requirement specifications. Being the badass programmer you are, you had that done in no time and ready to move on to the next feature. Maybe something looking like this...

public class Withdrawal
    {

        public string AccountNumber { get; set; }
        public string AmountInWords { get; set; }

    }
class Program
    {
        static void Main(string[] args)
        {

            var newWithdrawalRequest = new Withdrawal();

            Console.WriteLine("Enter your account Number");
            newWithdrawalRequest.AccountNumber = Console.ReadLine();
            Console.WriteLine("Enter amount to be withdrawn in words");
            newWithdrawalRequest.AmountInWords = Console.ReadLine();

            Console.WriteLine("Are you sure you want to proceed with this withdrawal...Enter (Y/N)");
            bool shouldProceed = Console.ReadLine() == "Y" ? true : false;
            if (shouldProceed)
            {
                Console.WriteLine("..........Counting your Money........................................");
                Task.Delay(3000);
                Console.WriteLine("Enjoy your money");
            }

            else
            {

                Console.WriteLine("Withdrawal Terminated");
                Console.WriteLine("........................Closing this transaction..........................");
                Task.Delay(1000);
                Console.WriteLine("........................Thanks for banking with us..........................");
                Task.Delay(1000);
                Console.ReadLine(); 
            }



        }
    }

You're then told that the bank now allows ATM withdrawals and you find a way to fix that in. With little thinking, you refactor and get this done using an enum to indicate the desired withdrawal type.

 public enum WithdrawalType
    {
        WithdrawalSlip = 1,
        ATMCard,
    }
 public class RefactoredWithdrawal : Withdrawal
    {
        public virtual string Withdraw(WithdrawalType withdrawalType)
        {
            string result;
            switch (withdrawalType)
            {
                case WithdrawalType.WithdrawalSlip:
                    result = $"Withdraw from {AccountNumber} of {AmountInWords} at {DateTime.Now.ToShortDateString()} by withdrawal slip";
                    break;
                case WithdrawalType.ATMCard:
                    result = $"Withdraw from {AccountNumber} of {AmountInWords} at {DateTime.Now.ToShortDateString()} by ATMCard";

                    break;
                default:
                    result = $"Withdraw from {AccountNumber} of {AmountInWords} at {DateTime.Now.ToShortDateString()} cannot be processed!";
                    break;
            }
            return result;
        }
    }
 class Program
    {
        static void Main(string[] args)
        {
            var newWithdrawalRequest = new RefactoredWithdrawal();

            Console.WriteLine("Enter your account Number");
            newWithdrawalRequest.AccountNumber = Console.ReadLine();

            Console.WriteLine("Enter amount to be withdrawn in words");
            newWithdrawalRequest.AmountInWords = Console.ReadLine();

            Console.WriteLine($"Select Withdrawal Type {Environment.NewLine} Enter 1 for Withdrawal Slip {Environment.NewLine} Enter 2 for ATM Withdrawal Slip");

            var selectedWithdrawalType = (WithdrawalType)Int32.Parse(Console.ReadLine());
            Console.WriteLine("Are you sure you want to proceed with this withdrawal...Enter (Y/N)");


            bool shouldProceed = Console.ReadLine() == "Y" ? true : false;
            if (shouldProceed)
            {
                Console.WriteLine("..........Processing........................................");
                Task.Delay(3000);
                Console.WriteLine(newWithdrawalRequest.Withdraw(selectedWithdrawalType));
                Console.ReadLine();
            }
            else
            {
                Console.WriteLine("Withdrawal Terminated");
                Console.WriteLine("........................Closing this transaction..........................");
                Task.Delay(1000);
                Console.WriteLine("........................Thanks for banking with us..........................");
                Task.Delay(1000);
                Console.ReadLine();
            }
        }
    }

However, you have this spidey sense telling you that there is a good chance that the algorithm for withdrawal may still change in the nearest future, as the bank could begin to support another withdrawal option. How do you make this very easily extensible and flexible?πŸ€”πŸ€”

A Solution

πŸ””The Strategy Pattern proves quite useful when you want to use different variants of an algorithm within an object and be able to switch from one algorithm to another during runtime.πŸ””

What do we need to implement the Strategy Pattern?

  • A strategy interface....(IWithdrawalStrategy Interface).
  • A concrete implementation of the strategy interface (WithdrawalSlipStrategy, ATMCardStrategy).

  • A context in which we would deploy our strategyπŸ’£πŸ”«πŸ”ͺβš”πŸ—‘(Bank).

public interface IWithdrawalStrategy
    {
        string Withdraw(Withdrawal withdrawalDetails);
    }

The interface states the piece of functionality that may change based on the banks requirements and also as time passes on. This is what we aim to achieve.

Next we need a strategy..

   public class WithdrawalSlipStrategy : IWithdrawalStrategy
    {
        public string Withdraw(Withdrawal withdrawal)
        {
           return $"Withdraw from {withdrawal.AccountNumber} of {withdrawal.AmountInWords} at {DateTime.Now.ToShortDateString()} by withdrawal slip";
        }
    }

Lastly, we need a context in which our strategy would be deployed. A Bank class is created to serve this purpose. The context houses the strategy and it commands the execution of the strategy made available.

 public class Bank
    {
        private IWithdrawalStrategy WithdrawalStrategy;

        public void SetWithdrawalStrategy(IWithdrawalStrategy strategy)
        {
            WithdrawalStrategy = strategy;
        }

        public string ExecuteWithdrawalStrategy(Withdrawal withdrawal) => WithdrawalStrategy.Withdraw(withdrawal);

    }

The chosen strategy can be passed into the context class through its constructor, an exposed property or through a method. This example sets the desired strategy to be deployed in this context through the method setWithdrawalStrategy().

Finally, we refactor our code to accommodate the strategy pattern.

 var newWithdrawalRequest = new RefactoredWithdrawal();

            Console.WriteLine("Enter your account Number");
            newWithdrawalRequest.AccountNumber = Console.ReadLine();

            Console.WriteLine("Enter amount to be withdrawn in words");
            newWithdrawalRequest.AmountInWords = Console.ReadLine();

            Console.WriteLine($"Select Withdrawal Type {Environment.NewLine} Enter 1 for Withdrawal Slip {Environment.NewLine} Enter 2 for ATM Withdrawal Slip");

            var selectedOption = (WithdrawalType)int.Parse(Console.ReadLine());
            var result = string.Empty;
            var bank = new Bank();
            switch (selectedOption)
            {
                 case WithdrawalType.WithdrawalSlip:
                    bank.SetWithdrawalStrategy(new WithdrawalSlipStrategy());
                    result = bank.ExecuteWithdrawalStrategy(newWithdrawalRequest);
                    break;
                case WithdrawalType.ATMCard:
                    bank.SetWithdrawalStrategy(new ATMCardStrategy());
                    result = bank.ExecuteWithdrawalStrategy(newWithdrawalRequest);
                    break;
                default:
                    result = "Strategy not yet implemented";
                    break;
            }
            Console.WriteLine("................Processing.......................");
            Task.Delay(1500);
            Console.WriteLine(result);
            Console.ReadLine();

The ATMCardStrategy also implements the IWithdrawalStrategy Interface.

 public class ATMCardStrategy : IWithdrawalStrategy
    {

        public string Withdraw(Withdrawal withdrawal)
        {
            return $"Withdraw from {withdrawal.AccountNumber} of {withdrawal.AmountInWords} at {DateTime.Now.ToShortDateString()} by ATM Card";
        }
    }

Why might you want to consider using the Strategy Pattern?

  • You can swap algorithms used inside an object at runtime.

  • You can isolate the implementation details of an algorithm from the code that uses it.

  • You can introduce new strategies without having to change the context. All you need to do is change what strategies are passed into the context.

  • Strategies can be easily tested before updating already running code.

See you when next I study another design pattern πŸ˜‰πŸ™‹β€β™‚οΈπŸ™‹β€β™‚οΈ.