The Open-Closed Principle of the SOLID principles acronym states that software entities such as classes and methods should be open for extension but closed for modification which encourages the use of abstraction and ensures developers can add new functionality without altering the current codebase and minimizing the risk of breaking existing features.
The main purpose of the Open-Closed principle is to ensure that developers write code that does not require modification everytime a customer comes back and changes the request (as developers know this can happen many times). Following the OCP can help us achieve the goal of extending the class’ behaviour with the customers request without modifying it.
Abstraction
Abstraction is a coding technique which enables developers to hide internal parts of a class and only expose the necessary features. With this enabled developers can create flexible designs and classes that can inherit the abstract class and implement their own interpretation of the abstract method.
An example of implementing the Open-Closed principle could be a salary calculator for developers at a company.
OCP C# Implementation
So now we will begin implementing the OCP in C#, create a new console application using your favourite IDE and create a file at the root of the project called EmployeeReport with the various properties.
public class EmployeeReport
{
public int EmployeeID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string JobTitle { get; set; }
public int WorkHours { get; set; }
public double HourlyRate { get; set; }
}
After you have finished creating the above class create another class at the root of the project called EmployeeSalaryCalculator
public class EmployeeSalaryCalculator
{
private readonly IEnumerable<EmployeeReport> _employeeReports;
public EmployeeSalaryCalculator(List<EmployeeReport> employeeReports)
{
_employeeReports = employeeReports;
}
public double CalculateSalaries()
{
double total = 0D;
foreach (var empReport in _employeeReports)
{
total += empReport.HourlyRate * empReport.WorkHours;
}
return total;
}
}
So in the preceding code we have a _employeeReports field of IEnumerable which has a type of EmployeeReport that is then set equal to what is passed in the constructor when we make an instance of the class.
In addition, we have a method called CalculateSalaries which loops over the _employeeReports field/list and multiplies the currently looped over employee’s hourly rate with the work hours and then returns the variable total.
Now open the Program.cs file and create a list of developer data that we can pass as an argument into the EmployeeSalaryCalculator instance.
var employeeReports = new List<EmployeeReport>
{
new EmployeeReport
{
EmployeeID = 1,
FirstName = "John",
LastName = "Doe",
JobTitle = "Junior .NET Developer",
WorkHours = 40,
HourlyRate = 14
},
new EmployeeReport
{
EmployeeID = 2,
FirstName = "Jane",
LastName = "Doe",
JobTitle = ".NET Developer",
WorkHours = 40,
HourlyRate = 16
},
new EmployeeReport
{
EmployeeID = 3,
FirstName = "James",
LastName = "Smith",
JobTitle = "Senior .NET Developer",
WorkHours = 40,
HourlyRate = 30
},
new EmployeeReport
{
EmployeeID = 4,
FirstName = "Josh",
LastName = "Williamson",
JobTitle = "Senior .NET Developer",
WorkHours = 50,
HourlyRate = 50
}
};
We then create an instance of EmployeeSalaryCalculator and pass in the above list variable.
var calculate = new EmployeeSalaryCalculator(employeeReports);
We will then Console.WriteLine the output of all of the developers salaries.
Console.WriteLine($"Sum of all developer salaries: {calculate.CalculateSalaries()} pounds");
However, as developers will know requirements can change very quickly and project managers can ask for an additional feature to be added as part of a customer request which may require us to change the above code, an example being that we need a different calculation for the junior, mid and senior developers at a 5%, 10% and 20% bonus increase respectively.
To make this change we could easily add an if and else if statement inside of the loop within the CalculateSalaries function to add a bonus to the roles.
public double CalculateSalaries()
{
double total = 0D;
foreach (var empReport in _employeeReports)
{
if (empReport.JobTitle == ".NET Developer")
{
total += empReport.HourlyRate * empReport.WorkHours * 0.6;
}
else if (empReport.JobTitle == "Senior .NET Developer")
{
total += empReport.HourlyRate * empReport.WorkHours * 1.2;
}
else
{
total += empReport.HourlyRate * empReport.WorkHours;
}
}
return total;
}
However, what if an additional role gets introduced and then we have to add another calculation to the for loop? We would have to modify the class and the calculation, this goes against the OCP as it is against the modification of a class.
To solve this issue and implement the OCP correctly we will create an abstract class with 1 field, a constructor and an abstract method. We will also create 3 classes that will inherit from this class and implement the abstract method.
Below is what our new base class will look like.
public abstract class BaseEmployeeSalaryCalculator
{
protected EmployeeReport _employeeReport { get; private set; }
public BaseEmployeeSalaryCalculator(EmployeeReport employeeReport)
{
_employeeReport = employeeReport;
}
public abstract double CalculateSalary();
}
As you can see we have an abstract class that cannot be instantiated and inside a protected field meaning that it can only be accessed within the same class or in a class that inherits the BaseEmployeeSalaryCalculator, and as you’ll see we will use this field in the inherited classes to calculate the developer types salary. In addition, we have an abstract function called CalculateSalary which is just the body since it is abstract and the implementation of the function can only happen in the derived classes. The method being abstract allows us to define our own implementation of the salary calculation for all the roles we need to.
So we can create a new file called DotNetDeveloperSalary.cs which inherits from the BaseEmployeeSalaryCalculator class, this will have a constructor inside of it with the EmployeeReport as a parameter and this parameter will be passed to the base as an argument.
public class DotNetDeveloperSalary: BaseEmployeeSalaryCalculator
{
public DotNetDeveloperSalary(EmployeeReport employeeReport) : base(employeeReport)
{
}
public override double CalculateSalary()
{
return _employeeReport.HourlyRate * _employeeReport.WorkHours * 0.6;
}
}
In addition, we will need to create a JuniorDotNetDeveloper.cs file that just returns a 5% bonus.
public class JuniorDotNetDeveloper : BaseEmployeeSalaryCalculator
{
public JuniorDotNetDeveloper(EmployeeReport employeeReport) : base(employeeReport)
{
}
public override double CalculateSalary()
{
return _employeeReport.HourlyRate * _employeeReport.WorkHours * 0.3;
}
}
The reason we are using the base keyword here is that it plays a vital role in constructor inheritance. It enables the derived class to call a constructor from the base class providing the required arguments, hence why in our derived class we have a constructor that also passes in EmployeeReport as a parameter as it does the same in the base class.
This ensures that the base class will initialize its members before the derived class completes its own initialization. So in our example : base(employeeReport) calls the base class constructor and ensures that the _employeeReport field is correctly initialized before proceeding with initializing the derived classes.
Furthermore, we override the abstract method from the base class to return the calculation that we had originally written in the CalculateSalaries method. The override keyword is there to extend/modify the abstract method set in the base class.
Apologies for the long explaination but I thought it was worth going through in case you hadn’t seen it before!
Now we will create a class that will also inherit from the base class and override the CalculateSalary method for the Senior .NET developer role.
public class SeniorDotNetDeveloperSalary : BaseEmployeeSalaryCalculator
{
public SeniorDotNetDeveloperSalary(EmployeeReport employeeReport) : base(employeeReport)
{
}
public override double CalculateSalary()
{
return _employeeReport.HourlyRate * _employeeReport.WorkHours * 1.2;
}
}
The next thing we will need to do is open the EmployeeSalaryCalculator class and alter the _employeeReport to be of type BaseEmployeeSalaryCalculator and change the field name to _employeeCalculation and make the same change in the constructor.
Furthermore, you will then need to alter the CalculateSalaries to loop over the _employeeCalculation field and then increment the total variable against the CalculateSalary function which will return the calculation of the HourlyRate, WorkHours and bonus of the currently looped over employee type.
public class EmployeeSalaryCalculator
{
private readonly IEnumerable<BaseEmployeeSalaryCalculator> _employeeCalculation;
public EmployeeSalaryCalculator(List<BaseEmployeeSalaryCalculator> employeeCalculation)
{
_employeeCalculation = employeeCalculation;
}
public double CalculateSalaries()
{
double total = 0D;
foreach (var empCalc in _employeeCalculation)
{
total += empCalc.CalculateSalary();
}
return total;
}
}
This may seem like a lot of changes but in the long run it is a lot more flexible as we no longer have to edit our current code if your manager comes in and says there are 2 or 3 more changes needed to the calculator. All we will need to do is create another class that has its own calculation logic and enforce the same type that we have done for the previous 3 inherited classes.
This implements the OCP as the EmployeeSalaryCalculator is closed for modification but open for extension.
Our final changes to make is to open the Program.cs file and change the list type to be BaseEmployeeSalaryCalculator.
After this we will create a new instance of a JuniorDotNetDeveloper and within this pass a parameter value of a newly initialized EmployeeReport with values set.
If you remember from the start we had 1 junior, 1 medium and 2 seniors so within this list we need the correct amount of instances of our newly created classes like below.
var employeeCalculations = new List<BaseEmployeeSalaryCalculator>
{
new JuniorDotNetDeveloper(new EmployeeReport()
{
EmployeeID = 1,
FirstName = "John",
LastName = "Doe",
JobTitle = "Junior .NET Developer",
WorkHours = 140,
HourlyRate = 20
}),
new DotNetDeveloperSalary(new EmployeeReport()
{
EmployeeID = 2,
FirstName = "Jane",
LastName = "Doe",
JobTitle = ".NET Developer",
WorkHours = 150,
HourlyRate = 25
}),
new SeniorDotNetDeveloperSalary(new EmployeeReport()
{
EmployeeID = 3,
FirstName = "James",
LastName = "Smith",
JobTitle = "Senior .NET Developer",
WorkHours = 160,
HourlyRate = 55
}),
new SeniorDotNetDeveloperSalary(new EmployeeReport(){
EmployeeID = 4,
FirstName = "Josh",
LastName = "Williamson",
JobTitle = "Senior .NET Developer",
WorkHours = 160,
HourlyRate = 55
})
};
var calculate = new EmployeeSalaryCalculator(employeeCalculations);
Console.WriteLine($"Sum of all developer salaries: {calculate.CalculateSalaries()} pounds");
You can freely edit the work hours and hourly rate as you see fit and run the program.
Thank you for reading my article, I hope you learned something, feel free to look at the references below and don’t forget to follow, share and clap!
References