I recently came across an article about FizzBuzz which aims to solve the FizzBuzz problem with Open/Closed Principle.
You can browse the article here:FizzBuzz Refactoring Challenge with Open/Closed Principle
The problem is often seen that procedural programming paradigm causes the programmers to write bad code. Then OOP and some principles come to rescue.
I think, the issue here is not related to type of programming paradigm but us; developers.
So I decided to write this post to prove that we can write "real" maintainable code with procedural programming as well.
I'll use JavaScript (ES5) but it can be written almost every language that lets you write standalone function without dummy class on top of it :)
The requirements
Given a list of numbers from 1 to n.
When a number is divisible by 3 should be replaced with Fizz.
When a number is divisible by 5 should be replaced with Buzz.
When a number is divisible by 3 and by 5 should be replaced with FizzBuzz.
Implementation
Our flow is simple, we'll start with imperative way. When we see the pattern, we'll generalize and decouple it as a useful function.
So we'll start with a simple function named labelDivisibleNumbers.
Why didn't we name it as something that includes fizzBuzz?
We could do it, but what we do here is indeed replacing number with label if any.
Most of the time it's better starting with more generic name if possible.
function labelDivisibleNumbers(options) {
for (var n = options.start; n < options.end; n++) {
if (n % 3 === 0 && n % 5 === 0) {
console.log("FizzBuzz");
continue;
}
if (n % 3 === 0) {
console.log("Fizz");
continue;
}
if (n % 5 === 0) {
console.log("Buzz");
continue;
}
console.log(n);
}
}
We can invoke it as follows:
labelDivisibleNumbers({start: 1, end: 100});
We provided start
and end
as an option so that we no longer need to any refactor if requirement changes for start and end.
Point here is it's always better to avoiding hard-coding.
let's focus on here now.
for (var n = options.start; n < options.end + 1; n++) {
}
This is commonly known as a range function. Let's make it then.
function range(options, callback) {
for (let number = options.start; number < options.end; number++) {
callback(number);
}
}
We make it similar to forEach where second param is callback which let us do whatever we want.
So we can even go further make this function as a module and use in other part of our project if needed or publish in npm etc.
Okay, great! Now we can focus on callback section.
function labelDivisibleNumbers(options) {
range(options, function(n) {
if (n % 3 == 0 && n % 5 == 0) {
console.log("FizzBuzz");
return;
}
if (n % 3 == 0) {
console.log("Fizz");
return;
}
if (n % 5 == 0) {
console.log("Buzz");
return;
}
console.log(n);
})
}
I don't know if you realize but we use n % x == 0 section a lot.
This is also something common indeed. Let make it a function as well.
function divisibleBy(dividend, divisor) {
return dividend % divisor === 0;
}
So we can replace n % x == 0 with divisibleBy
function labelDivisibleNumbers(options) {
range(options, function(n) {
if (divisibleBy(n, 3) && divisibleBy(n, 5)) {
console.log("FizzBuzz");
return;
}
if (divisibleBy(n, 3)) {
console.log("Fizz");
return;
}
if (divisibleBy(n, 5)) {
console.log("Buzz");
return;
}
console.log(n);
})
}
Now we can focus on console.log("FizzBuzz"), would it be awesome if we provide number into a function with a list of label representation of number?
divisibleBy(n, 5) => Buzz
divisibleBy(n, 3) => Fizz
which means
5 => Buzz
3 => Fizz
Our data could be like this in JS
var list = [
{
divisor: 3,
label: "Fizz",
},
{
divisor: 5,
label: "Buzz"
}
];
So, what we need it is to input list and number, output concatenated label.
Given the list above if n is 15, we expect FizzBuzz
Here we need a reduce indeed. JS has built-in reduce method but let's create our own reduce function with simple for loop, so that we can understand what's going on better.
function reduce(array, callback, accumulator, start) {
for (var i = 0; i < array.length; i++) {
accumulator = accumulator == undefined ? start : accumulator;
accumulator = callback(accumulator, array[i], i, array)
}
return accumulator;
}
So let's concat all labels into one single string
reduce(list, function(acc, curr){
return acc + curr.label
}, '')
This is great start but we want to have label depends on number provided.
So we need to concat if number is divisible by divisors in the list
function getLabel(list, n) {
return reduce(
list,
function (acc, curr) {
return divisibleBy(n, curr.divisor)
? acc + curr.label
: acc;
},
""
);
}
To wrap up everything we did so far:
function labelDivisibleNumbers(options, list) {
range(options, function (n) {
console.log(getLabel(list, n) || n);
});
}
Requirement Update
As a client, I would like to add new number with its label to current implementation.
When a number is divisible by 7 should be replaced with Bazz.
When a number is divisible by 5 and by 7 should be replaced with BuzzBazz.
When a number is divisible by 3 and by 7 should be replaced with FizzBazz.
All we need to do is add Bazz to our list with corresponding number.
var list = [
{
divisor: 3,
label: "Fizz",
},
{
divisor: 5,
label: "Buzz",
},
{
divisor: 7,
label: "Bazz",
}
];
There's still room for improvement but we need to stop somewhere to deliver things just in time.
Hope you enjoy it :)
Top comments (0)