In Depth Look: Strategy Design Pattern, Dependency Injection (DI), Open/Closed ...

:

Introduction

As most of us who involved in the key architectural design must have looked over the web to identify and understand core designing principles. I have been recently reading various papers over the web discussion various principle good scalable architectural patterns to address several of designing challenges. Most of the papers, well written and with example elaborating various principle but took me while to get the depth of it. The reason for challenges that I faced with understanding some of the concepts was not with “How that has been implemented?” but rather “Why that implementation is better?”. This is one of the reason forced me to write down this article. 

Why are we discussing again?

As title of this article suggests, we will be discussing Strategy Design Pattern, Dependency Injection, Open/Closed principle and Loose Coupling. But then my intention is not just to show you how that has been implemented, but rather why that implementation makes sense. And best way to

  • Take a real life Scenario
  • Write a program to implement cover the real life scenario
  • As cause effect of SDLC, we will introduce a change by extending some of the features and will see that whatever we have implemented at initial stage is scoring good on OCP (Open/Close Principle) or not.
  • Then we will revise the design and inspect the scalability
  • And will make final conclusive note.

Important Note: The idea here is to get the concept right and not the so called “Real Life Scenario” to be practically exists or not. Also, Throughout the discussion, Some of the implementation may not stand right with respect to other principles because I don’t want to make the actual discussion much complex by introducing the stuffs that is not applicable to this discussion. For better understanding, I would also encourage you to download the working solution to understand the code better.

Problem Statement

According to Statements

“A Renowned Corporate Group is planning to promote their employees in one of their Organization. In this Organization, Employees having their ID, Name, YearsSinceLastPromotion, NoOfSkillCertificate and PercentageOfGoalAchieved prepopulated. Management has asked HR department to list the Employees based on Years since Last Promoted greater than 4.”

We have tried to make the scenario much simple.

Implementation 1

As the request arrived to us IT Department, we build the small Program to list the items. Let’s have a look

In simple Implementation, we have Employee class, HumanResource class responsible for holding List of Employee and will have method called GetPromotionEligibleEmployees() which will internally create the object of class BasedOnYearsSinceLastPromotion to identify the Employee eligible for promotion. Let’s take a look at actual Code

/// <summary>
/// Employee Class
/// </summary>
public class Employee
{
    public int ID { get; set; }
    public string Name { get; set; }
    public int YearsSinceLastPromotion { get; set; }
    public int NoOfSkillCertificate { get; set; }
    public int PercentageOfGoalAchieved { get; set; }
}

/// <summary>
/// Human Resource Class
/// </summary>
public class HumanResource
{
    private List<Employee> _employees;
    private BasedOnYearsSinceLastPromotion _promotionStategy;

    public HumanResource()
    {
        _employees = new List<Employee>();
        _promotionStategy = new BasedOnYearsSinceLastPromotion();
    }

    public void AddEmployee(Employee employee)
    {
        _employees.Add(employee);
    }

    /// <summary>
    /// Uses Promotion Strategy classes to find out list of Employee Eligible
    /// </summary>
    /// <returns>List of Eligible Employee</returns>
    public List<Employee> GetPromotionEligibleEmployees()
    {
        return _promotionStategy.IdentifyPromotionEligibleEmployees(_employees);
    }
}

Our Promotion Strategy class

class BasedOnYearsSinceLastPromotion
{
    public List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees)
    {
        return employees.Where(e => e.YearsSinceLastPromotion >= 4).ToList();
    }
}

While the Main() program

/// <summary>
/// Client Program
/// </summary>
static void Main()
{
    Console.WriteLine("Employees Eligible for Promotion");
    Console.WriteLine();

    HumanResource hr = new HumanResource();

    hr.AddEmployee(new Employee { ID = 1, Name = "Steve", YearsSinceLastPromotion = 8, NoOfSkillCertificate = 6, PercentageOfGoalAchieved = 75 });
    hr.AddEmployee(new Employee { ID = 2, Name = "John", YearsSinceLastPromotion = 3, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 60 });
    hr.AddEmployee(new Employee { ID = 3, Name = "Todd", YearsSinceLastPromotion = 5, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 80 });
    hr.AddEmployee(new Employee { ID = 4, Name = "Lisa", YearsSinceLastPromotion = 6, NoOfSkillCertificate = 3, PercentageOfGoalAchieved = 87 });
    hr.AddEmployee(new Employee { ID = 5, Name = "Smith", YearsSinceLastPromotion = 2, NoOfSkillCertificate = 1, PercentageOfGoalAchieved = 73 });
    hr.AddEmployee(new Employee { ID = 6, Name = "Debbie", YearsSinceLastPromotion = 5, NoOfSkillCertificate = 3, PercentageOfGoalAchieved = 82 });
    hr.AddEmployee(new Employee { ID = 7, Name = "Kate", YearsSinceLastPromotion = 1, NoOfSkillCertificate = 4, PercentageOfGoalAchieved = 79 });
    hr.AddEmployee(new Employee { ID = 8, Name = "Rehana", YearsSinceLastPromotion = 3, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 91 });

    //Client default strategy BasedOnYearsSinceLastPromotion
    List<Employee> employeeYOS = hr.GetPromotionEligibleEmployees();
    Print(employeeYOS, "Years Since Last Promotion");

    Console.ReadLine();
}

and Utility function Print() to display employees

/// <summary>
/// Utility function to Print Employees
/// </summary>
/// <param name="employees">List of Employees</param>
/// <param name="criterion">Criterion</param>
public static void Print(List<Employee> employees, string criterion)
{
    Console.WriteLine(" Based On '{0}'", criterion);
    Console.WriteLine(" ID\tName");
    foreach (Employee e in employees)
    {
        Console.WriteLine(" {0}\t{1}", e.ID, e.Name);
    }
    Console.WriteLine();
}

 

and finally we run the code. Bingo! 

We have got the output and code works well. We run through the approval cycle and without much hurdle we were able to push the code to production. 

Meanwhile in the senior management meeting, 

“We decided, we still want to identify the employee with Years since Last Promotion but we may revisit this decision to promote employees based on No of Skill Certificate that they have as it will help to bring most skilled organization image in the market and we want to encourage that.”

In sort, It is eminent that IT department has (we have) to modify the program to support the current strategy (i.e. based on Years Since Last Promotion) as well as open to adopt the new strategy (i.e. based on No of skill certificate completed). And hence we will modify our program. Result of which is the Implementation no 2.

Implementation 2

We still have all the properties (especially NoOfSkillCertificate) that we need for implementing the new features that management is deciding to implement in the current Program. Hence we don’t need any change in the Employee class. 

As you can see we have introduces two new items. 

  1. New BasedOnNoOfSkillCertificate Strategy class which will use NoOfSkillCertificate Property to Employee object to determine the Promotion eligibility of Employee.
  2. Enumeration that will help client function to choose between different promotions strategies such as BasedOnYearsSinceLastPromotion or BasedOnNoOfSkillCertificate.
    public enum PromotionStrategy
    {
        BasedOnYearSinceLastPromotion = 0,
        BasedOnNoOfSkillCertificate = 1
    }

    public class BasedOnYearsSinceLastPromotion
    {
        public List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees)
        {
            return employees.Where(e => e.YearsSinceLastPromotion >= 4).ToList();
        }
    }

    public class BasedOnNoOfSkillCertificate
    {
        public List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees)
        {
            return employees.Where(e => e.NoOfSkillCertificate >= 3).ToList();
        }
    }

And we are modifying 2 new existing items HumanResource class and the Main() function to support new strategies based on Promotion Strategy Enumeration.

    /// <summary>
    /// Human Resource Class
    /// </summary>
    public class HumanResource
    {
        private List<Employee> _employees;
        private BasedOnYearsSinceLastPromotion _promotionStategyOne;
        private BasedOnNoOfSkillCertificate _promotionStategyTwo;

        public HumanResource()
        {
            _employees = new List<Employee>();
            _promotionStategyOne = new BasedOnYearsSinceLastPromotion();
            _promotionStategyTwo = new BasedOnNoOfSkillCertificate();
        }

        public void AddEmployee(Employee employee)
        {
            _employees.Add(employee);
        }

        /// <summary>
        /// Uses Promotion Strategy classes to find out list of Employee Eligible
        /// </summary>
        /// <returns>List of Eligible Employee</returns>
        public List<Employee> GetPromotionEligibleEmployees(PromotionStrategy strategy)
        {
            if (strategy == PromotionStrategy.BasedOnYearSinceLastPromotion)
                return _promotionStategyOne.IdentifyPromotionEligibleEmployees(_employees);
            else if (strategy == PromotionStrategy.BasedOnNoOfSkillCertificate)
                return _promotionStategyTwo.IdentifyPromotionEligibleEmployees(_employees);
            else
                throw new ApplicationException("Unknown Strategy!");
        }
    }

and

        /// <summary>
        /// Client Program
        /// </summary>
        static void Main()
        {
            Console.WriteLine("Employees Eligible for Promotion");
            Console.WriteLine();

            HumanResource hr = new HumanResource();


            hr.AddEmployee(new Employee { ID = 1, Name = "Steve", YearsSinceLastPromotion = 8, NoOfSkillCertificate = 6, PercentageOfGoalAchieved = 75 });
            hr.AddEmployee(new Employee { ID = 2, Name = "John", YearsSinceLastPromotion = 3, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 60 });
            hr.AddEmployee(new Employee { ID = 3, Name = "Todd", YearsSinceLastPromotion = 5, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 80 });
            hr.AddEmployee(new Employee { ID = 4, Name = "Lisa", YearsSinceLastPromotion = 6, NoOfSkillCertificate = 3, PercentageOfGoalAchieved = 87 });
            hr.AddEmployee(new Employee { ID = 5, Name = "Smith", YearsSinceLastPromotion = 2, NoOfSkillCertificate = 1, PercentageOfGoalAchieved = 73 });
            hr.AddEmployee(new Employee { ID = 6, Name = "Debbie", YearsSinceLastPromotion = 5, NoOfSkillCertificate = 3, PercentageOfGoalAchieved = 82 });
            hr.AddEmployee(new Employee { ID = 7, Name = "Kate", YearsSinceLastPromotion = 1, NoOfSkillCertificate = 4, PercentageOfGoalAchieved = 79 });
            hr.AddEmployee(new Employee { ID = 8, Name = "Rehana", YearsSinceLastPromotion = 3, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 91 });

            //Client default strategy BasedOnYearsSinceLastPromotion
            List<Employee> employeeYOS = hr.GetPromotionEligibleEmployees(PromotionStrategy.BasedOnYearSinceLastPromotion);
            Print(employeeYOS, "Years Since Last Promotion");

            //Client default strategy BasedOnNoOfSkillCertificate
            List<Employee> employeeNOS = hr.GetPromotionEligibleEmployees(PromotionStrategy.BasedOnNoOfSkillCertificate);
            Print(employeeNOS, "Based on No of Skill Certificate");

            Console.ReadLine();
        }

With these changes (2 new additions and 2 modifications) we run the program and code works fine. So the Output

We have created the code that compiles fine, executes fine and gives reasonably decent output. But for our discussion, only output is not our intention. As mentioned earlier, we want to re-evaluate our program scalability. In the Implementation 2, there are several problems.

Really? What’s wrong with Implementation 2? 

  • In order to implement the change, we’ve added the new strategy class and invoked strategy from the client Program and that is absolutely fine. But why do we need to change the HumanResource class. The reason for the change in the HumanResource class is because of its Tight Coupling with Strategy classes. Having said that it utilizes the concrete implementation of Strategies.
  • Second, if we want to add new strategies in the future, we will again modify the HumanResource class as we discussed in the first point but this leads to expand HumanResource class unnecessarily without any new feature addition. Please note that no matter how many promotion strategies we plug to HumanResource class, the feature is still single. That is to identify the Eligibility for promotion. So ideally that class should not grow. But with our current implementation, with every new strategy, it will grow.

We want to target the problem that we have with implementation 2 with some of core software development principles. But before that, let’s have a look at these principles.

Open/Closed principle (OCP)

According to Definition at Wikipedia

“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”

 

In our example, HumanResource class should be able to support additional promotion strategies without any modification. 

Dependency Injection (DI)

According to Wikipedia

“Dependency injection is a software design pattern in which one or more dependencies (or services) are injected, or passed by reference, into a dependent object (or client) and are made part of the client's state.”

To simplify, we have to provide the reference to dependencies from outside then actual dependency creation from inside. And in our case, we have to provide reference to Strategies from client program and isolating the responsibility from HumanResource class.

How to fix the problem?

We need to rewrite the our code component following way

  • Unify the strategy classes (BasedOnYearsSinceLastPromotion and BasedOnNoOfSkillCertificate) by implementing the common interface to bring it to family of classes.
  • And separate the logic of object creation of strategy classes (BasedOnYearsSinceLastPromotion and BasedOnNoOfSkillCertificate) from HumanResource class. To achieve that, we need to introduce Dependency Injection (DI). To be more precise, we need to loosely couple the HumanResource class by providing the Interface reference compare to earlier implementation of Tight coupling

The outcome of this is a Strategy Pattern.

Final implementation with Strategy Pattern

Let’s have look at the code to clear understanding

    /// <summary>
    /// Promotion Strategy Template
    /// </summary>
    public interface IPromotionStrategy
    {
        List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees);
    }

    /// <summary>
    /// Concrete Implementation of Promotion Strategy based on 'Years Since Last Promoted'
    /// </summary>
    public class BasedOnYearsSinceLastPromotion : IPromotionStrategy
    {
        public List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees)
        {
            return employees.Where(e => e.YearsSinceLastPromotion >= 4).ToList();
        }
    }

    /// <summary>
    /// Concrete Implementation of Promotion Strategy based on 'No Of Skill Certificate Completed'
    /// </summary>
    public class BasedOnNoOfSkillCertificate : IPromotionStrategy
    {
        public List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees)
        {
            return employees.Where(e => e.NoOfSkillCertificate >= 3).ToList();
        }
    }

As you can see the, both BasedOnYearsSinceLastPromotion and BasedOnNoOfSkillCertificate implements IPromotionStrategy. And hence they will be loosely coupled with HumanResource

    /// <summary>
    /// Human Resource Class
    /// </summary>
    public class HumanResource
    {
        private List<Employee> _employees;
        private IPromotionStrategy _promotionStategy;

        public HumanResource()
        {
            _employees = new List<Employee>();
        }

        public void AddEmployee(Employee employee)
        {
            _employees.Add(employee);
        }

        public void AddPromotionStrategy(IPromotionStrategy NewPromotionStrategy)
        {
            _promotionStategy = NewPromotionStrategy;
        }

        public List<Employee> GetPromotionEligibleEmployees()
        {
            if (_promotionStategy == null)
                throw new ApplicationException("Promotion Strategy is not Provided.");

            return _promotionStategy.IdentifyPromotionEligibleEmployees(_employees);
        }
    }

And my favorite part, The amount clutter that we have cleaned up from GetPromotionEligibleEmployees() function.

Also in Main() client function much mature

        /// <summary>
        /// Client Program
        /// </summary>
        /// <param name="args"></param>
        static void Main()
        {
            Console.WriteLine("Employees Eligible for Promotion");
            Console.WriteLine();

            HumanResource hr = new HumanResource();

            hr.AddEmployee(new Employee { ID = 1, Name = "Steve", YearsSinceLastPromotion = 8, NoOfSkillCertificate = 6, PercentageOfGoalAchieved =75 });
            hr.AddEmployee(new Employee { ID = 2, Name = "John", YearsSinceLastPromotion = 3, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 60 });
            hr.AddEmployee(new Employee { ID = 3, Name = "Todd", YearsSinceLastPromotion = 5, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 80 });
            hr.AddEmployee(new Employee { ID = 4, Name = "Lisa", YearsSinceLastPromotion = 6, NoOfSkillCertificate = 3, PercentageOfGoalAchieved = 87 });
            hr.AddEmployee(new Employee { ID = 5, Name = "Smith", YearsSinceLastPromotion = 2, NoOfSkillCertificate = 1, PercentageOfGoalAchieved = 73 });
            hr.AddEmployee(new Employee { ID = 6, Name = "Debbie", YearsSinceLastPromotion = 5, NoOfSkillCertificate = 3, PercentageOfGoalAchieved = 82 });
            hr.AddEmployee(new Employee { ID = 7, Name = "Kate", YearsSinceLastPromotion = 1, NoOfSkillCertificate = 4, PercentageOfGoalAchieved = 79 });
            hr.AddEmployee(new Employee { ID = 8, Name = "Rehana", YearsSinceLastPromotion = 3, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 91 });

            //Client invocation of BasedOnYearsSinceLastPromotion
            hr.AddPromotionStrategy(new BasedOnYearsSinceLastPromotion());
            List<Employee> employeeYOS = hr.GetPromotionEligibleEmployees();
            Print(employeeYOS, "Years Since Last Promotion");

            //Client invocation of BasedOnNoOfSkillCertificate
            hr.AddPromotionStrategy(new BasedOnNoOfSkillCertificate());
            List<Employee> employeeSK = hr.GetPromotionEligibleEmployees();
            Print(employeeSK, "No Of Skill Certificate");

            Console.ReadLine();
        }

And the output, Why not!

An Important Question

Is new strategy Design pattern approach addresses the concerns that we raised earlier in this discussion? 

Simple Answer “Let’s try”. We will introduce another promotion strategy. Isn’t it Insane!

“We also want to identify the employee eligibility based on the Percentage of Goal achieved.”

Remember we already have field PercentageOfGoalAchieved. So new strategy class

    /// <summary>
    /// Promotion Strategy Template
    /// </summary>
    public interface IPromotionStrategy
    {
        List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees);
    }

    /// <summary>
    /// Concrete Implementation of Promotion Strategy based on 'Years Since Last Promoted'
    /// </summary>
    public class BasedOnYearsSinceLastPromotion : IPromotionStrategy
    {
        public List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees)
        {
            return employees.Where(e => e.YearsSinceLastPromotion >= 4).ToList();
        }
    }

    /// <summary>
    /// Concrete Implementation of Promotion Strategy based on 'No Of Skill Certificate Completed'
    /// </summary>
    public class BasedOnNoOfSkillCertificate : IPromotionStrategy
    {
        public List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees)
        {
            return employees.Where(e => e.NoOfSkillCertificate >= 3).ToList();
        }
    }

    /// <summary>
    /// Concrete Implementation of Promotion Strategy based on 'Percentage of Goal Achieved'
    /// </summary>
    public class BasedOnPercentageOfGoalAchieved : IPromotionStrategy
    {
        public List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees)
        {
            return employees.Where(e => e.PercentageOfGoalAchieved >= 80).ToList();
        }
    }

and in the Main() add call to new promotion strategy call

        /// <summary>
        /// Client Program
        /// </summary>
        /// <param name="args"></param>
        static void Main()
        {
            Console.WriteLine("Employees Eligible for Promotion");
            Console.WriteLine();

            HumanResource hr = new HumanResource();

            hr.AddEmployee(new Employee { ID = 1, Name = "Steve", YearsSinceLastPromotion = 8, NoOfSkillCertificate = 6, PercentageOfGoalAchieved =75 });
            hr.AddEmployee(new Employee { ID = 2, Name = "John", YearsSinceLastPromotion = 3, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 60 });
            hr.AddEmployee(new Employee { ID = 3, Name = "Todd", YearsSinceLastPromotion = 5, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 80 });
            hr.AddEmployee(new Employee { ID = 4, Name = "Lisa", YearsSinceLastPromotion = 6, NoOfSkillCertificate = 3, PercentageOfGoalAchieved = 87 });
            hr.AddEmployee(new Employee { ID = 5, Name = "Smith", YearsSinceLastPromotion = 2, NoOfSkillCertificate = 1, PercentageOfGoalAchieved = 73 });
            hr.AddEmployee(new Employee { ID = 6, Name = "Debbie", YearsSinceLastPromotion = 5, NoOfSkillCertificate = 3, PercentageOfGoalAchieved = 82 });
            hr.AddEmployee(new Employee { ID = 7, Name = "Kate", YearsSinceLastPromotion = 1, NoOfSkillCertificate = 4, PercentageOfGoalAchieved = 79 });
            hr.AddEmployee(new Employee { ID = 8, Name = "Rehana", YearsSinceLastPromotion = 3, NoOfSkillCertificate = 2, PercentageOfGoalAchieved = 91 });

            //Client invocation of BasedOnYearsSinceLastPromotion
            hr.AddPromotionStrategy(new BasedOnYearsSinceLastPromotion());
            List<Employee> employeeYOS = hr.GetPromotionEligibleEmployees();
            Print(employeeYOS, "Years Since Last Promotion");

            //Client invocation of BasedOnNoOfSkillCertificate
            hr.AddPromotionStrategy(new BasedOnNoOfSkillCertificate());
            List<Employee> employeeSK = hr.GetPromotionEligibleEmployees();
            Print(employeeSK, "No Of Skill Certificate");

            //Client invocation of BasedOnPercentageOfGoalAchieved
            hr.AddPromotionStrategy(new BasedOnPercentageOfGoalAchieved());
            List<Employee> employeeGA = hr.GetPromotionEligibleEmployees();
            Print(employeeGA, "Percentage of Goal Achieved");

            Console.ReadLine();
        }

That's all we need! Just two changes. With this addition (right no modification) we compiled, run and Output

Conclusion

Throughout this exercise we run through various change cycle to understand the way software components behaves. We have implemented the strategy design pattern for real life problem and with concise approach we looked at various concepts Dependency Injection (DI), Open/Closed principle (OCP) and Loose Coupling vs Tight coupling. Most importantly we have analyzed the drawback to some of the approaches that do not adhere to these principles and then we finally implemented the Strategy Design Pattern. We have not only learned “What is way to implement strategy design pattern?” but we also looked “Why the implementation such problem with Strategy Design Pattern is better?”. Experts, Let me know if you have any comments that help me refine this article. Your comments most welcome.

Happy Learning!

Other Article(s) by Author