Finding the correct level of decomposition is sometimes challenging. In this article, I will take decomposition to the extreme creating a wildly convoluted Fizz Buzz solution and share my feelings on the correct level of decomposition.
Fizz Buzz Test
Write a program that prints the numbers from 1 to 100. But for multiples of three print “Fizz” instead of the number and for the multiples of five print “Buzz”. For numbers which are multiples of both three and five print “FizzBuzz”.
const modulo = n => a => a % n;
const equals = a => b => a === b;
const compose = (...a) => x => a.reduceRight((p, fn) => fn(p), x);
const equalsZero = equals(0);
const isDivisibleBy = a =>
compose(
equalsZero,
modulo(a)
);
const isDivisibleBy3 = isDivisibleBy(3);
const isDivisibleBy5 = isDivisibleBy(5);
const isDivisibleBy15 = isDivisibleBy(15);
const ifElse = (condition, onTrue, onFalse) => n => {
return condition(n) ? onTrue(n) : onFalse(n);
};
const identity = x => x;
const always = value => () => value;
const fizzBuzz = ifElse(
isDivisibleBy15,
always("FizzBuzz"),
ifElse(
isDivisibleBy3,
always("Fizz"),
ifElse(isDivisibleBy5, always("Buzz"), identity)
)
);
for (let index = 1; index <= 100; index++) {
console.log(fizzBuzz(index)); // 1, 2, Fizz, 4, Buzz
}
I would hope it is obvious to the reader that I have taken decomposition too far, the question is when should I have stopped and how would I know to stop.
Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away. - Antoine de Saint-Exupery
Let's take this step by step.
Starting Point
const fizzBuzz = n => {
if (n % 15 === 0) {
return "FizzBuzz";
} else if (n % 3 === 0) {
return "Fizz";
} else if (n % 5 === 0) {
return "Buzz";
} else {
return n;
}
};
Pretty concise code, we could make it a little easier to read and remove duplicated logic by extracting the divisible check into its own function.
First Refactor
const isDivisibleBy = (a, n) => n % a === 0;
const fizzBuzz = n => {
if (isDivisibleBy(15, n)) {
return "FizzBuzz";
} else if (isDivisibleBy(3, n)) {
return "Fizz";
} else if (isDivisibleBy(5, n)) {
return "Buzz";
} else {
return n;
}
};
Now we have the reusable isDivisibleBy function, but should we break it down further? Is isDivisibleBy doing two things finding the remainder and a comparison?
Second Refactor
Extract till you Drop. - Uncle Bob
One Thing: Extract till you Drop.
const equalsZero = n => n === 0;
const modulo = (a, n) => n % a;
const isDivisibleBy = (a, n) => equalsZero(modulo(a, n));
const fizzBuzz = n => {
if (isDivisibleBy(15, n)) {
return "FizzBuzz";
} else if (isDivisibleBy(3, n)) {
return "Fizz";
} else if (isDivisibleBy(5, n)) {
return "Buzz";
} else {
return n;
}
};
Should we stop now or push on? How many of you have "dropped"?
I know it when I see it
I shall not today attempt further to define the kinds of material I understand to be embraced within that shorthand description ["hard-core pornography"], and perhaps I could never succeed in intelligibly doing so. But I know it when I see it, and the motion picture involved in this case is not that. - Justice Potter Stewart
Personally, when I get to the point that the refactored code becomes more complex and harder to read than the original I stop or revert the refactor. I believe this happened in this simplified example at the second refactor. Like many things in software development, this is a personal preference, all versions of this code work as well as all the others, actually, I would imagine the starting point code is the fastest.
Let me know what you think in the comments, how far do you take decomposition?
Top comments (0)