SOLID Principles — 2: Open-Closed Principle

SOLID Principles — 2: Open-Closed PrincipleThe first part of this article series provided an overview of SOLID and the single-responsible principle. Now, it is time to examine the second principle, which is the open-closed principle.Bertrand Meyer coin…


This content originally appeared on Level Up Coding - Medium and was authored by Vinod Madubashana

SOLID Principles — 2: Open-Closed Principle

The first part of this article series provided an overview of SOLID and the single-responsible principle. Now, it is time to examine the second principle, which is the open-closed principle.

Bertrand Meyer coined the term and introduced this principle in his book Object-oriented Software Construction. This principle states that software modules(components, classes, etc.) should be open for extension but closed for modification. This may not seem very clear at first. The simple idea is that opening for an extension means it should be easy to change a module's behavior, and closing for modification means doing it without changing the source code of that module. In another view, we should not have to change much of the existing code, but we should be able to add small pieces of new code to extend the software. Don't worry if you still need to get it; we will discuss whether it is possible to do that and, if possible, how to do that and what the tradeoffs are.

An Example Code Violate OCP

The best way to understand this is through an example. Let’s consider the simple function below that calculates employee salary and knows how it violates the OCP.


public record Employee(String employeeType,
double baseSalary,
int performanceScore,
int overtimeHours,
int leaveDays) {

public double calculateTotalSalary() {
double salary = 0.0;
double bonus = 0.0;
double overtimePay = 0.0;
double leaveDeduction = 0.0;
double taxDeduction = 0.0;

// Base salary logic
if (employeeType.equals("FullTime")) {
salary = baseSalary + (baseSalary * 0.2); // 20% bonus for full-time employees
bonus = (performanceScore() > 6) ? 1000 : 800;
overtimePay = overtimeHours() * 50; // $50 per overtime hour
leaveDeduction = leaveDays() * 100; // Deduct $100 per leave day
} else if (employeeType().equals("PartTime")) {
salary = baseSalary(); // No bonus for part-time employees
bonus = 500; // Fixed bonus for part-time employees
overtimePay = overtimeHours() * 30; // $30 per overtime hour
leaveDeduction = leaveDays() * 50; // Deduct $50 per leave day
} else if (employeeType().equals("Contract")) {
salary = baseSalary() - (baseSalary() * 0.1); // 10% deduction for contract employees
bonus = 0; // No bonus for contract employees
overtimePay = overtimeHours() * 20; // $20 per overtime hour
leaveDeduction = leaveDays() * 0; // No leave deduction for contract employees
}

// Tax deduction logic
if (salary > 5000) {
taxDeduction = salary * 0.1; // 10% tax for salaries above 5000
}

return salary + bonus + overtimePay - leaveDeduction - taxDeduction;
}

}

Let’s now think we have a new requirement to add a new employee type, Intern, which might have its own logic to calculate values like bonus and overtime pay. The option is simple: add another else if the code blocks. Yes, it is simple and quick. What happens over time is lots of if statements start to creep in, sometimes two to three levels of nested if statements and this code grows to the point where it becomes a mess.

This code may only start with one employee type, where the code is simple and elegant. When they get the second employee type, the quickest option is to add an if block. Think about the requirements. It is to add a new employment type, not to change anything, so why do we need to change our existing employee class? That is what OCP tells you. It requires you to extend your system without modifying already developed components, but only if the requirement is to add a new feature as an extension to the existing behaviors. Of course, if the requirement is to change existing behavior, then we have to change the module, and it is not violating OCP because that is the requirement of the system, and it has changed. This is the first thing you have to understand to understand OCP better.

Make it follow OCP

How can behavior be extended without changing the existing component logic in object-oriented programming? I know now you are screaming, “Use strategy pattern.” I see strategy pattern as a nice name for polymorphism when used to switch strategies in runtime. So, the answer is to use polymorphism.

First, we need to define interfaces for each strategy to calculate different parts of the salary.

// Base salary calculation strategy
public interface SalaryCalculatorStrategy {
double calculateSalary(Employee employee);
}

// Bonus calculation strategy
public interface BonusCalculatorStrategy {
double calculateBonus(Employee employee);
}

// Overtime pay strategy
public interface OvertimePayCalculatorStrategy {
double calculateOvertimePay(Employee employee, int overtimeHours);
}

// Leave deduction strategy
public interface LeaveDeductionCalculatorStrategy {
double calculateLeaveDeduction(Employee employee, int leaveDays);
}

// Tax deduction strategy
public interface TaxDeductionStrategy {
double calculateTax(double salary);
}

Then, we can implement them for each employment type.

// Full-time salary calculation
public class FullTimeEmployeeSalaryCalculator implements SalaryCalculatorStrategy {
@Override
public double calculateSalary(Employee employee) {
return employee.getBaseSalary() + (employee.getBaseSalary() * 0.2); // 20% bonus for full-time employees
}
}

// Full-time bonus calculation
public class FullTimeEmployeeBonusCalculator implements BonusCalculatorStrategy {
@Override
public double calculateBonus(Employee employee) {
int performanceScore = employee.getPerformanceScore();
return (performanceScore > 6) ? 1000 : 800; // Bonus based on performance score
}
}

// Full-time overtime calculation
public class FullTimeOvertimePayCalculator implements OvertimePayCalculatorStrategy {
@Override
public double calculateOvertimePay(Employee employee, int overtimeHours) {
return overtimeHours * 50; // $50 per overtime hour
}
}

// Full-time leave deduction calculation
public class FullTimeLeaveDeductionCalculator implements LeaveDeductionCalculatorStrategy {
@Override
public double calculateLeaveDeduction(Employee employee, int leaveDays) {
return leaveDays * 100; // Deduct $100 per leave day
}
}
// Part-time salary calculation
public class PartTimeEmployeeSalaryCalculator implements SalaryCalculatorStrategy {
@Override
public double calculateSalary(Employee employee) {
return employee.getBaseSalary(); // No bonus for part-time employees
}
}

// Part-time bonus calculation
public class PartTimeEmployeeBonusCalculator implements BonusCalculatorStrategy {
@Override
public double calculateBonus(Employee employee) {
return 500; // Fixed bonus for part-time employees
}
}

// Part-time overtime calculation
public class PartTimeOvertimePayCalculator implements OvertimePayCalculatorStrategy {
@Override
public double calculateOvertimePay(Employee employee, int overtimeHours) {
return overtimeHours * 30; // $30 per overtime hour
}
}

// Part-time leave deduction calculation
public class PartTimeLeaveDeductionCalculator implements LeaveDeductionCalculatorStrategy {
@Override
public double calculateLeaveDeduction(Employee employee, int leaveDays) {
return leaveDays * 50; // Deduct $50 per leave day
}
}
// Contract employee salary calculation
public class ContractEmployeeSalaryCalculator implements SalaryCalculatorStrategy {
@Override
public double calculateSalary(Employee employee) {
return employee.getBaseSalary() - (employee.getBaseSalary() * 0.1); // 10% salary deduction for contract employees
}
}

// Contract employee bonus calculation
public class ContractEmployeeBonusCalculator implements BonusCalculatorStrategy {
@Override
public double calculateBonus(Employee employee) {
return 0; // No bonus for contract employees
}
}

// Contract employee overtime calculation
public class ContractOvertimePayCalculator implements OvertimePayCalculatorStrategy {
@Override
public double calculateOvertimePay(Employee employee, int overtimeHours) {
return overtimeHours * 20; // $20 per overtime hour
}
}

// Contract employee leave deduction calculation
public class ContractLeaveDeductionCalculator implements LeaveDeductionCalculatorStrategy {
@Override
public double calculateLeaveDeduction(Employee employee, int leaveDays) {
return 0; // No leave deduction for contract employees
}
}

Now, let's refactor our Employee class.

public record Employee(String employeeType,
double baseSalary,
int performanceScore,
int overtimeHours,
int leaveDays,
SalaryCalculatorStrategy salaryCalculatorStrategy,
BonusCalculatorStrategy bonusCalculatorStrategy,
OvertimePayCalculatorStrategy overtimePayCalculatorStrategy,
LeaveDeductionCalculatorStrategy leaveDeductionCalculatorStrategy,
TaxDeductionStrategy taxDeductionStrategy) {

public double calculateTotalSalary(Employee employee) {
double salary = salaryCalculatorStrategy.calculateSalary(employee);
double bonus = bonusCalculatorStrategy.calculateBonus(employee);
double overtimePay = overtimePayCalculatorStrategy.calculateOvertimePay(employee, employee.overTimeHours());
double leaveDeduction = leaveDeductionCalculatorStrategy.calculateLeaveDeduction(employee, employee.leaveDays());
double tax = taxDeductionStrategy.calculateTax(salary + bonus + overtimePay);
return salary + bonus + overtimePay - leaveDeduction - tax;
}

}

How clean is that calculateTotalSalary method? Now, if we need to add a new employee type, such as an Intern, we don't need to change the code in this component, which means this code is open for extension and closed for modification.

Isn't this cheating?

Some might think this is cheating because I am not showing the whole picture here. This object needs to be initialized somewhere, and we have to write these logics to initialize the employee object with the correct concrete implementation based on the employee type.

Hmm, yes, it is kind of cheating, but let's think about where we will initialize it. It can be the client code that initiates, a factory method, the constructor, or your framework will inject the correct objects. Those are not the codes with business logic, so we are protecting our domain model where the actual business logic is, which is the most valuable part of any codebase.

What is the benefit of following OCP?

If you can extend your system without changing already implemented parts, that means you won't break any existing behaviors. The code will become more accessible to read and composed of well-written components that also follow the single responsibility principle. This will also make your code easily testable. You will not have huge methods that do too much logic in one place.

Isn’t this over-engineering?

It depends!!! If you start replacing all of your logic with strategies, your design might become overengineered and hard to maintain. So, this is something other than a silver bullet or a template you can use. You have to make your own decision. That's what engineering is.

In this industry, no template can be applied everywhere; it all depends on the context. So, next time you extend your system, make your decision wisely. If you make a wrong decision, the next developer will follow you, and the code will become more fragile in the long run. Making the correct decision at the proper time is really important.

To make the correct decision, you need a good mental model of these principles and experience applying them. The chances that you will know this principle first through my article are meager. So, you only read this article to strengthen your mental model, see how others apply it, and think about what you know. So learn from many sources and use them when necessary, and then you will know whether it is over-engineering.

The example I showed you is straightforward, and you might feel it is overengineered. But try to get the idea I am trying to convey and see it through your codebase. You will see many places that might improve your codebase following the OCP.

Can we keep our codebase always adhere to OCP?

This is the tricky part. You can only make your code extensible if you know the extension points. However, the problem is that developers need to gain more knowledge about the domain at the start of a project. So now, if you try to make the system extensible in a way that you think the system will evolve, it can be very hard to change when the actual requirements are very different from what you expected. It also does not mean that upfront design is bad; we need some upfront design, but the most important characteristic is the ability to evolve your solution. So again, there are no silver bullet solutions to this problem. You have to make your decision. On the one hand, you need to make your system extensible, and on the other hand, it is bad to over-engineer the solutions.

I hope this article helps you improve your mental model of OCP. But remember, there are no templates. You have to really learn these principles the hard way by applying them when necessary.

Let’s meet next time with the third principle of SOLID principles, the Liskov substitution principle. Until then, happy coding!!!!


SOLID Principles — 2: Open-Closed Principle was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Level Up Coding - Medium and was authored by Vinod Madubashana


Print Share Comment Cite Upload Translate Updates
APA

Vinod Madubashana | Sciencx (2024-10-24T01:05:47+00:00) SOLID Principles — 2: Open-Closed Principle. Retrieved from https://www.scien.cx/2024/10/24/solid-principles-2-open-closed-principle/

MLA
" » SOLID Principles — 2: Open-Closed Principle." Vinod Madubashana | Sciencx - Thursday October 24, 2024, https://www.scien.cx/2024/10/24/solid-principles-2-open-closed-principle/
HARVARD
Vinod Madubashana | Sciencx Thursday October 24, 2024 » SOLID Principles — 2: Open-Closed Principle., viewed ,<https://www.scien.cx/2024/10/24/solid-principles-2-open-closed-principle/>
VANCOUVER
Vinod Madubashana | Sciencx - » SOLID Principles — 2: Open-Closed Principle. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2024/10/24/solid-principles-2-open-closed-principle/
CHICAGO
" » SOLID Principles — 2: Open-Closed Principle." Vinod Madubashana | Sciencx - Accessed . https://www.scien.cx/2024/10/24/solid-principles-2-open-closed-principle/
IEEE
" » SOLID Principles — 2: Open-Closed Principle." Vinod Madubashana | Sciencx [Online]. Available: https://www.scien.cx/2024/10/24/solid-principles-2-open-closed-principle/. [Accessed: ]
rf:citation
» SOLID Principles — 2: Open-Closed Principle | Vinod Madubashana | Sciencx | https://www.scien.cx/2024/10/24/solid-principles-2-open-closed-principle/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.