Hello folks, welcome back to the second article in the SOLID principles series.
Make sure you're already going through the first principle in my last article. Not yet?
Check it out! Here is the Link.
Enough talk, let's get started now.
Definition of Open / Closed Principle
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification."
What does this mean π€?
It means you should allow users to add new functionalities (extensions) without changing or modifying the existing code.
To illustrate, let's take a look at the code snippet below
Bad Example
const questions = [
{
type: 'boolean',
description: 'This article is very useful',
},
{
type: 'text',
description: 'Tell me about yourself',
},
{
type: 'multipleChoice',
description: 'What is your favorite language',
options: ['HTML', 'CSS', 'JS', 'Golang'],
},
];
function printQuestions(questions) {
questions.forEach((question) => {
console.log(question.description);
switch (question.type) {
case 'boolean':
console.log('1. True');
console.log('1. False');
break;
case 'text':
console.log('Answer: ___________________');
break;
case 'multipleChoide':
question.options.forEach((option, idx) => {
console.log(`${idx + 1}: ${option}`);
});
break;
}
});
}
// π Execute the function:
printQuestions(questions);
Now you might ask: "This function looks good, what actually is the problem here?"
In fact, we're violating the second rule of the SOLID principle.
I wrote function this way multiple times at the beginning of my career before knowing about the Open-Closed Principle π₯²
Please remember, each time you see a function have a lot of conditional statements like if/else
or switch
handle multiple cases inside. This is a big flag we are violating the second rule.
Imagine each time we want to add a new type of question, we have to go through these 2 steps over and over again.
Firstly, we add the new question object to the questions
array.
const questions = [
{
type: 'boolean',
description: 'This article is very useful',
},
{
type: 'text',
description: 'Tell me about yourself',
},
{
type: 'multipleChoice',
description: 'What is your favorite language',
options: ['HTML', 'CSS', 'JS', 'Golang'],
},
{
type: 'range',
description: 'Describe your favorite JS feature',
},
];
Secondly, we need to create a new case
in the swich
statement of the printQuestions
function to handle output for new question types.
While the printQuestions
is only open for extension but closed for modification.
The process of modifying this function each time we want to extend is considered a bad practice.
Now, what should we do?
Good Example
Now I want my printQuestions
function should automatically know what to do in case I add a new question type without modifying the function body - that is the "closed" portion of the Open/ Closed principle.
Let's break this function into multiple smaller classes.
It seems to me that this technique is a bit redundant and cumbersome, the refactoring is trivial at first. Especially in a very small example, but when we're working in a huge legacy source code, you will get the idea why the SOLID principle exists.
class BooleanQuestion {
constructor(description) {
this.description = description;
}
printQuestionChoices() {
console.log('1. True');
console.log('1. False');
}
}
class MultipleChoiceQuestion {
constructor(description, options) {
this.description = description;
this.options = options;
}
printQuestionChoices() {
this.options.forEach((option, idx) => {
console.log(`${idx + 1}: ${option}`);
});
}
}
class TextQuestion {
constructor(description) {
this.description = description;
}
printQuestionChoices() {
console.log('Answer: ___________________');
}
}
class RangeQuestion {
constructor(description) {
this.description = description;
}
printQuestionChoices() {
console.log('Minimum: ____________');
console.log('Maximum: ____________');
}
}
const questions = [
new BooleanQuestion('This article is awesome'),
new MultipleChoiceQuestion('What is your favorite movie?', [
'The walking dead',
'spiderman',
'avenger',
]),
new TextQuestion('Tell me about yourself?'),
new RangeQuestion('Your min and max pushup?'),
];
function printQuestions(questions) {
questions.forEach((question, idx) => {
console.log(question.description);
question.printQuestionChoices();
console.log('');
});
}
We just break the function with a big switch
statements into multiple smaller and straightforward classes.
Now we have a very clear and easy-to-understand new version of printQuestions
function.
This new function just takes in a list of question and print all of it to the console, without knowing about all the logic behind it.
From now on, we never have to touch and modify printQuestions
every time we want to add a new type of question.
But it still opened to be extended, we just added a new RangeQuestion
to questions
array
new RangeQuestion('Your min and max pushup?')
We make our printQuestions
do more things. Instead of changing code, we add more code that works with the old code to do things.
Personally speaking, I feel that we should not follow this principle all the time to an extreme level of always adding new code and never modifying the existing one.
But keep that in mind, especially it will extremely helpful to you in case you have a function with multiple if else
or switch
statements.
Final words
That's pretty much it, folks.
Thank you very much for reading till the end.
Looking forward to see you all again in the next one soon!
Big shout out to Web Dev Simplified and his great video explanation about SOLID design principle (link attached below) that inspired me to write about this topic.
Reference: