Open-Closed Principle
Achieving Open-Closed through Strategy Pattern
Open-Closed principle is to me one of the hardest SOLID principles to understand just by reading the definition. According to Wikipedia, the open/closed principle states "software entities should be open for extension, but closed for modification".
If you are like me, this definition doesn’t make sense, it means that the behaviour of the code has to have the ability to change without actually changing it. This still seems somewhat impossible without an example. Consider the code below: (It doesn’t conform to OCP)
function printForm(questions) { questions.forEach(question => { console.log(question.description); switch (question.type) { case 'closed': console.log("1. Yes"); console.log("2. No"); break; case 'open': console.log("______________"); break; } }); } questions = [ { "description": "What is your name?", "type": "open" }, { "description": "Are you married?", "type": "closed" } ] printForm(questions);
The function printForm takes a list of questions and based on the question type; open-ended or closed-ended (this has nothing to do with open-closed principles) the function logs an appropriate response field to the console. The questions are defined below the function and passed through as parameters.
Now say you want to add a new question to the form, if the question has a different type from the ones already present i.e open and closed, then we would have to go into the printForm() function and add a case to handle the new question.
function printForm(questions) { case 'objective': console.log("1. 1"); console.log("2. 2-3"); console.log("3. more than 4"); console.log("4. never"); break; } questions = [ { "description": "How many interviews have you been to", "type": "objective" } ] printForm(questions)
The mere fact that you have to change the code in the printQuestions() function to cater for adding a new question shows that it doesn’t conform to the OCP.
Adding a new item to the list of questions is the part of extension and in this case it is already open for extension.
The act of changing the function violates the closed for modification bit. It means that we shouldn’t be allowed to change the code inside the function. A lot of times, when you come across a switch or if else statements, it could be a violation of the open-closed principle.
In order to achieve the closed for modification feature, we could break down the printForm() function into different classes with each type of question having its own class, such that the printForm() function doesn’t need to know all the different types of questions. Each class handles the printing of its own options.
class OpenQuestion { constructor(description) { this.description = description } printQuestionOptions() { console.log("________________________"); } } class ClosedQuestion { constructor(description) { this.description = description } printQuestionOptions() { console.log("1. Yes"); console.log("2. No"); } } function printForm(questions) { questions.forEach(question => { console.log(question.description); question.printQuestionOptions() }); } questions = [ new OpenQuestion("What is your name?"), new ClosedQuestion("Are you married?"), ] printForm(questions)
The code above now conforms to the OCP in that if we want to add a new question to the list with a different behaviour than the existing ones (open for extension), we add the item to the list and create a new class to handle that behaviour. All this is done without changing the printForm() (closed for modification).
class ObjeectiveQuestion { constructor(description) { this.description = description } printQuestionOptions() { console.log("1. 1"); console.log("2. 2-3"); console.log("3. more than 4"); console.log("4. never"); } } questions = [ new ObjeectiveQuestion("How many interviews have you been to") ]
The act of creating the new classes to handle each behaviour separately is known as the strategy pattern (policy pattern). Which according to wikipedia is a behavioural software design pattern that enables selecting an algorithm at runtime. Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.
The downside to this approach is that you have to create different classes for each behaviour and can lead to creating a lot of files. Though it is significantly better in terms of readability and scalability to use this approach than. having multiple if else statements, sometimes it’s not worth going through the trouble especially if the if else had few branches (probably 2 or 3) and there’s no possibility of adding a new branch later on.