Learning Open-Closed Principle by Example
Coding Bugs
Posted on July 20, 2021
Welcome to this second article in which I will show the importance of applying the Open-Closed Principle when implementing the code of our application.
In the same way as the previous article talking about the Single Responsibility Principle, the advantages to applying this principle are: code comprehension, component reusability, increased development speed, ease of testability, etc.
What Open-Closed means
The Open-Closed Principle is framed within the set of SOLID principles, a set of rules to guide our code in what it's called Clean Code. In this case, each letter represents a different principle:
- S for Single Responsibility Principle,
- O for Open/Closed Principle,
- L for Liskov Substitution Principle,
- I for Interface Segregation Principle,
- D for Dependency Inversion Principle
This principle was described by Bertrand Meyer in 1988, a long time ago when object-oriented programming (OOP) started to be a reality and not only theory:
A software artifact should be open for extension but closed for modification.
The statement is clear, and there is no doubt about its meaning, or isn't it? We quickly think about interfaces and abstract classes because this is the way to think about extension and modification in OOP and, it is correct but incomplete. If we consider programming paradigms where no interface or abstract classes exist, how to apply this principle?
Some people say that parameters in functions or methods (procedures in some programming languages) are enough to agree with it, but this is not my case. Function parameters are a simple way to provide needed information to make the algorithm work. For example, if we have a method called printCharacterSeveralTimes
probably has two parameters:
- the character to print,
- the number of times to print,
Is this something that will change the behavior of our method? I don't think so.
However, in some programming languages like javascript, we can provide a parameter that is a function and get different behavior depending on it. One of my articles explains it very well, A powerful and proven way to include views on list rendering. It shows how to change the way to render a collection of items - list, table, or inline - in React or create a new method to render them in a new way.
Coding for learning
I use the code of my previous article that prints the sequence of the Natural numbers as the starting point to show how this principle is applied.
function Timer(frequency) {
let intervalId = null;
return {
start: function(job) {
intervalId = setInterval(job, frequency * 1000);
},
stop: function() {
if(null !== intervalId) {
clearInterval(intervalId);
}
}
};
}
function PrintNaturalNumbersJob() {
let number = 0;
return {
next: function() {
console.log(number);
number++;
}
};
}
function App() {
let frequency = 1; // In seconds
let timer = new Timer(frequency);
let job = new PrintNaturalNumbersJob();
timer.start(job.next);
}
new App();
Let's imagine that at this moment, a new requirement arrives:
As a user
I want to print numbers from any number to another number
So I will see the sequence
Most programmers will change the method that prints the sequence of Natural numbers, PrintNaturalNumbersJob
. I think the best approach is to create a new function to cover the requirement. This function will have the same definition as the previous one, it will be a Job
entity with a next
method after instantiating it.
function PrintNumbersStartingAtJob(initialNumber) {
let number = initialNumber;
return {
next: function() {
console.log(number);
number++;
}
};
}
We could say that this new function called PrintNumbersStartingAtJob
is an abstraction of the PrintNaturalNumbersJob
function. We can substitute all of its code calling this new function and, all the tests created should still be green. It means that the old method is a specialization of the new one.
function PrintNaturalNumbersJob() {
return new PrintNumbersStartingAtJob(0);
}
But we just covered the first part of the requirement and, we need to modify our code to stop the sequence at the number specified. Here is where the Open-Closed Principle is applied.
Taking in mind rules like keep high cohesion and loose coupling in our code, we should provide a way to stop printing numbers. In our case, a solution goes through to pass a rule that evaluates the current number generated in the Job
entity. Suddenly, a new entity appeared: Rule
.
function PrintNumbersStartingAtJob(initialNumber, rule) {
let number = initialNumber;
return {
next: function() {
console.log(number);
if(rule) {
rule(number);
}
number++;
}
};
}
You will notice the code checks for the existence of a rule
parameter. This check allows us to keep the old function, PrintNaturalNumbersJob
, still working if we called it in our code.
We could set that our rule needs to stop the timer when reaching a specific number. Our code for the rule is the following:
function StopTimerWhenNumberIsHigherOrEqualToRule(timer, lastNumber) {
return function(number) {
if(number >= lastNumber) {
timer.stop();
}
};
}
Very easy to read and to understand. In addition, very easy to test, in my opinion. The App
entity also needs to be modified and call the proper new entities and functions.
function App(startAt, endsAt) {
let frequency = 1; // In seconds
let timer = new Timer(frequency);
let rule = new StopTimerWhenNumberIsHigherOrEqualToRule(timer, endsAt);
let job = new PrintNumbersStartingAtJob(startAt, rule);
timer.start(job.next);
}
// Our app can initiate this way to count from 5 to 10.
new App(5, 10);
As an example, we could add some more rules to our code based on new requirements just creating new functions: send the current number to another system, increment the number the times specified, reacting based on the number, etc.
It seems to be easy to add more and more code without compromising our app. Newbies will be able to create the proper code without touching core functions unnecessarily. They will focus on the right part of the code.
Wrapping up
This article shows how to improve our code and maintainability by applying the Open-Closed Principle.
It could be something complex to apply at the beginning if you are not used to it but makes you grow up on how to design the components of your app. When you delegate some activities of your method in its parameters you probably apply this principle.
What do you think about the exercise made in this article?
Hope this can be useful to you or just have fun reading it.
Posted on July 20, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024