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
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; }
}
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);
}
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
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 });
List<Employee> employeeYOS = hr.GetPromotionEligibleEmployees();
Print(employeeYOS, "Years Since Last Promotion");
Console.ReadLine();
}
and Utility function Print()
to display employees
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.
- New
BasedOnNoOfSkillCertificate
Strategy class which will use NoOfSkillCertificate
Property to Employee
object to determine the Promotion eligibility of Employee. - 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.
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);
}
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
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 });
List<Employee> employeeYOS = hr.GetPromotionEligibleEmployees(PromotionStrategy.BasedOnYearSinceLastPromotion);
Print(employeeYOS, "Years Since Last Promotion");
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
public interface IPromotionStrategy
{
List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees);
}
public class BasedOnYearsSinceLastPromotion : IPromotionStrategy
{
public List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees)
{
return employees.Where(e => e.YearsSinceLastPromotion >= 4).ToList();
}
}
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
.
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
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 });
hr.AddPromotionStrategy(new BasedOnYearsSinceLastPromotion());
List<Employee> employeeYOS = hr.GetPromotionEligibleEmployees();
Print(employeeYOS, "Years Since Last Promotion");
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
public interface IPromotionStrategy
{
List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees);
}
public class BasedOnYearsSinceLastPromotion : IPromotionStrategy
{
public List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees)
{
return employees.Where(e => e.YearsSinceLastPromotion >= 4).ToList();
}
}
public class BasedOnNoOfSkillCertificate : IPromotionStrategy
{
public List<Employee> IdentifyPromotionEligibleEmployees(List<Employee> employees)
{
return employees.Where(e => e.NoOfSkillCertificate >= 3).ToList();
}
}
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
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 });
hr.AddPromotionStrategy(new BasedOnYearsSinceLastPromotion());
List<Employee> employeeYOS = hr.GetPromotionEligibleEmployees();
Print(employeeYOS, "Years Since Last Promotion");
hr.AddPromotionStrategy(new BasedOnNoOfSkillCertificate());
List<Employee> employeeSK = hr.GetPromotionEligibleEmployees();
Print(employeeSK, "No Of Skill Certificate");
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