Open Closed Principle In C#

Open Closed Principle

We will be discussing the Open-Closed Principle also known as OCP, as one of the SOLID principles of object-oriented programming and how to implement it when designing our software.

This principle said that should be open for extension but closed for modification. We should write a code that does not require modification every time a customer changes its request.

Example

Let us define a class that contains salary calculations.

Let us imagine that we have a task where we need to calculate the total cost of all the developer salaries in a single company.

To get started, we are going to create the model class first,

public class EmployeeReport {
    public int Id {
        get;
        set;
    }
    public string Name {
        get;
        set;
    }
    public string Level {
        get;
        set;
    }
    public int WorkingHours {
        get;
        set;
    }
    public double HourlyRate {
        get;
        set;
    }
}

We can design the salary calculation method in another class.

public class SalaryCalculator {
    private readonly IEnumerable < EmployeeReport > _employeeReports;
    public SalaryCalculator(List < EmployeeReport > employeeReports) {
        _employeeReports = employeeReports;
    }
    public double CalculateTotalSalaries() {
        double totalSalaries = 0 D;
        foreach(var devReport in _employeeReports) {
            totalSalaries += devReport.HourlyRate * devReport.WorkingHours;
        }
        return totalSalaries;
    }
}

Then call this method on the main program.

class OCP {
    public OCP() {
        Run();
    }
    public void Run() {
        var devReports = new List < EmployeeReport > {
            new EmployeeReport {
                Id = 1,
                    Name = "Amit Kumar",
                    Level = "Senior developer",
                    HourlyRate = 30.5,
                    WorkingHours = 160
            },
            new EmployeeReport {
                Id = 2,
                    Name = "Nirmal Dayal",
                    Level = "Junior developer",
                    HourlyRate = 20,
                    WorkingHours = 150
            },
            new EmployeeReport {
                Id = 3,
                    Name = "Sheeba John",
                    Level = "Senior developer",
                    HourlyRate = 30.5,
                    WorkingHours = 180
            }
        };
        var calculator = new SalaryCalculator(devReports);
        Console.WriteLine($ "Sum of all the developer salaries is {calculator.CalculateTotalSalaries()} Rupees");
        Console.ReadLine();
    }
}

So, all this solution is working great, but now our team leader comes to our desk and says that we need a different calculation for the senior and junior developers. The senior developers should have a bonus of 20% on a salary.

Of course, to satisfy this requirement, we are going to modify our CalculateTotalSalaries method like this,

public double CalculateTotalSalaries() {
    double totalSalaries = 0 D;
    foreach(var empReport in _ employeeReports) {
        if (empReport.Level == "Senior developer") {
            totalSalaries += empReport.HourRate * empReport.WorkingHours * 1.2;
        } else {
            totalSalaries += empReport.HourRate * empReport.WorkingHours;
        }
    }
    return totalSalaries;
}

Mainly, because we had to modify our existing class behavior which worked perfectly. Another thing is that if our team leader comes again and asks us to modify the calculation for the junior devs as well, we would have to change our class again. This is totally against what OCP stands for.

Again, Modify the code as per OCP Principle,

public abstract class BaseSalaryCalculator {
    protected EmployeeReport EmployeeReport {
        get;
        private set;
    }
    public BaseSalaryCalculator(EmployeeReport employeeReport) {
        EmployeeReport = employeeReport;
    }
    public abstract double CalculateSalary();
}

Define another class for SeniorDevSalaryCalculator.cs,

public class SeniorDevSalaryCalculator: BaseSalaryCalculator {
    public SeniorDevSalaryCalculator(EmployeeReport report): base(report) {}
    public override double CalculateSalary() => EmployeeReport.HourlyRate * EmployeeReport.WorkingHours * 1.2;
}

Conclusion

Even though the name of the principle is self-explanatory, we can see how easy it is to implement incorrectly. Make sure to distinguish the logic of every class -- it does not need to change the current class if you have more categories, but you must extend the abstract class and perform the logic and implementation as per requirement comes.