In this article, we will learn about the Open-Closed Principle. There are a few basic questions around this principle as below.
- Why use this principle?
- Is it necessary to implement this principle?
- What happens if it is not implemented
We will try to find answers to these questions.
What Open Closed Principle (OCP) says – “Software entities (such as classes, modules, functions, etc.) should be open for extension, but closed for modification"
In a simple way, the meaning of this principle is that if we want to add any new functionality, then there must not be a need to change the existing class or method. Instead of modifying an existing class or method, we should implement our change by adding new functionality by deriving the existing class or method.
Bob Martin says this principle is “the most important principle of object-oriented design”.
The simplest way is to implement OCP while adding functionality by newly-derived (sub) classes that inherit the original class implementation. So, whenever there is a requirement change, instead of touching the existing functionality, it’s always suggested to create new classes and leave the original implementation unchanged.
By doing this, we need not test our existing functionality as we did not touch the existing code. We just need to test the new functionality which we added. It will save time and cost and we can modularize code. Also, the Single Responsibility Principle will not get violated. This is the answer to the first question.
SOLID are the principles not rules. You definitely can build an application without using SOLID; however, there are many drawbacks which you will face in the long term.
The biggest advantage of implementing SOLID are maintainability, flexibility, and testability. And to achieve this Open Closed Principle is most important. This is the answer to the second question.
Let’s see the below code example for the user which will show the name of the user and calculate the salary of the user.
Create a class User with the below code. Here, I am creating one simple console application.
- public class User
- {
- public int ID { get; set; }
- public string Name { get; set; }
-
- public User ()
- {
- }
- public User (int id,string name)
- {
- this.ID = id;this.Name = name;
- }
- public decimal CalculateSalary(decimal BasicPay)
- {
- return BasicPay * 2.5M;
- }
- public override string ToString()
- {
- return string.Format("ID : {0} Name: {1}", this.ID, this.Name);
- }
- }
And, call this class in our main method.
- static void Main(string[] args)
- {
- User userDinesh = new User(1, "Dinesh");
-
- Console.WriteLine(string.Format("User {0} Salary : {1}",
- userDinesh.ToString(), userDinesh.CalculateSalary(40000).ToString()));
- Console.ReadLine();
- }
Below is the output.
User ID : 1 Name: Dinesh Salary : 100000.0
Now, if we need to calculate the salary for the manager and trainee users, we need to do some changes in the existing class as below.
- public class User
- {
- public int ID { get; set; }
- public string Name { get; set; }
- public string UserType { get; set; }
-
- public User()
- {
- }
- public User(int id,string name,string userType)
- {
- this.ID = id;this.Name = name;this.UserType = userType;
- }
- public decimal CalculateSalary(decimal BasicPay)
- {
- if (this.UserType == "Manager")
- return BasicPay * 2.5M;
- else
- return BasicPay * 1.5M;
- }
- public override string ToString()
- {
- return string.Format("ID : {0} Name: {1}", this.ID, this.Name);
- }
- }
And, the code for the main program is below.
- static void Main(string[] args)
- {
- User userDinesh = new User(1, "Dinesh", "Manager");
- User userGanesh = new User(2, "Ganesh", "Trainee");
- Console.WriteLine(string.Format("User {0} Salary : {1}",
- userDinesh.ToString(), userDinesh.CalculateSalary(40000).ToString()));
- Console.WriteLine(string.Format("User {0} Salary : {1}",
- userGanesh.ToString(), userGanesh.CalculateSalary(40000).ToString()));
- Console.ReadLine();
- }
Below is the output.
User ID : 1 Name: Dinesh Salary : 100000.0
User ID : 2 Name: Ganesh Salary : 60000.0
Now, the above output is as expected and will satisfy the requirement; however, we changed the original class to accommodate the requirement. If there is another type of User that gets introduced called “Engineer”, then again, we need to change the code in the User class. If we do every time, we get the change request. We need to do testing of whole functionality because it will impact on the original implemented functionality as we did changes in the existing class.
To avoid this, OCP will come into the picture. With the help of OCP, we can avoid these changes easily to decrease the time of testing and ultimately the cost.
So, check the below code. I have introduced a few classes.
The User class will have only concrete definitions. I made it abstract so that we can inherit in the Manager and Trainee and inherit User class.
- public abstract class User
- {
- public int ID { get; set; }
- public string Name { get; set; }
- public abstract string UserType { get; set; }
-
- public User()
- {
- }
- public User(int id,string name,string userType)
- {
- this.ID = id;this.Name = name;this.UserType = userType;
- }
-
- public override string ToString()
- {
- return string.Format("ID : {0} Name: {1}", this.ID, this.Name);
- }
- }
Now create Manager class which will represent Permanent User
- public class Manager: User
- {
- public override string UserType { get; set; }
- public Manager()
- {
-
- }
- public Manager(int id, string name, string UserType) : base(id, name, UserType)
- {
-
- }
-
- }
Now create Trainee class which will represent Trainee User
- public class Trainee: User
- {
- public override string UserType { get; set; }
- public Trainee()
- {
-
- }
- public Trainee(int id, string name, string UserType) : base(id, name, UserType)
- {
-
- }
- }
I have introduced a SalaryCalculator Class which would take in the abstract User Type as parameter and uses the Salary and the Salary rate in the final calculation. The type of User used would be decided at run-time.
- public class SalaryCalculator: User
- {
- public override string UserType { get; set; }
- public SalaryCalculator(string userType)
- {
- this.UserType = userType;
-
- }
-
- public decimal CalculateSalary(decimal basicPay, decimal rate)
- {
- return basicPay * rate;
- }
- }
And use all these classes in our Main program and run it:
- static void Main(string[] args)
- {
- User userDinesh = new Manager(1, "Dinesh", "Manager");
- User userGanesh = new Trainee(2, "Ganesh", "Trainee");
- SalaryCalculator objMSalary = new SalaryCalculator("Manager");
- SalaryCalculator objTSalary = new SalaryCalculator("Trainee");
-
- Console.WriteLine(string.Format("User {0} Salary : {1}",
- userDinesh.ToString(), objMSalary.CalculateSalary (40000,2.5M).ToString()));
- Console.WriteLine(string.Format("User {0} Salary : {1}",
- userGanesh.ToString(), objTSalary.CalculateSalary (40000,1.5M).ToString()));
- Console.ReadLine();
- }
Below is the Output,
User ID : 1 Name: Dinesh Salary : 100000.0
User ID : 2 Name: Ganesh Salary : 60000.00
This is the implementation of OCP. Class is Open for extension and closed for modification
Now the third and final question is, what if I do not follow Open Closed principle during a requirement enhancement in the development process.
In that case, we need to face the following disadvantages.
- If existing class allows the addition of new logic, as a developer we need to do testing the entire functionality along with the new requirement.
- Also, as a developer, we need to ensure we communicate the scope of the changes to the QA team in advance so that they can prepare for enhanced regression testing along with the new feature testing. And this is a time consuming and costly process.
- Not following the Open-Closed Principle breaks the Single Responsibility Principle since the class might have to do multiple tasks.
- If the changes are implemented on the same class, Maintenance of the class becomes difficult since the code of the class increases by thousands of unorganized lines.
--Happy Coding--