"FizzBuzz" is a well-known programming assignment, often used as a little test to see if a candidate for a programming job could manage to implement a set of requirements, usually on the spot. The requirements are these:
- Given a list of numbers from 1 to n.
- If a number is divisible by 3 should be replaced with Fizz.
- If a number is divisible by 5 should be replaced with Buzz.
- If a number is divisible by 3 and by 5 should be replaced with FizzBuzz.
Applying these rules, the resulting list would become:
1, 2, Fizz, 4, Buzz … 13, 14, FizzBuzz, 16, 17 …
A simple solution often found online could be something like this:
class FizzBuzz
{
generate(number: number) {
let output: string[];
for (let i = 1; i <= n; i++) {
output.push(this.getReplacement(i));
}
return output;
}
getReplacement(number: number): string {
if (number%3 === 0 && number%5 === 0) return "FizzBuzz";
if (number%3) return "Fizz";
if (number%5) return "Buzz";
else return n;
}
}
const fizzBuzz = new FizzBuzz();
const result = fizzBuzz.generate(100);
console.log(result.join(", "));
The Class above does the job. And solves the problem, BUT... what if we wanted to introduce a new rule?
For example:
If a number is divisible by 7 should be replaced with Bazz.
FizzBuzz Class and the Open/Closed Principle
If numbers divisible by 7 should one day be replaced with Bazz
, it will be impossible to implement this change without actually modifying the code of the FizzBuzz class.
Currently the FizzBuzz class is not open for extension, nor closed for modification. Our current implementation violates the Open/Closed principle
The Open/Closed Principle says that the code should be open for extension but closed for modification. In other words, the code should be organized in such a way that new modules can be added without modifying the existing code.
FizzBuzz Class and the Singe Responsibility Principle
A class should have one, and only one, reason to change.
Our class has two responsibilities, therefore two reasons to change.
- it generates a list of numbers and
- it generates replacement for each number based on the FizzBuzz Rules
If we think about it. Every responsibility that a class has... is a reason to change.
Re-designing our Class
How can we make our FizzBuzz class more Flexible? What is likely to change?
The FizzBuzz rules are liable to change. And if we want to follow the Open/Closed principle, we should not need to modify the FizzBuzz Class
itself.
Lets try to think about the problem we are trying to solve here.
We want to generate a list of numbers, replacing certain numbers with strings, based on a flexible set of "rules".
Lets start by introducing a RuleInterface
interface RuleInterface {
matches(number: number): boolean;
getReplacement(): string;
}
and lets extract the rules we need to solve the FizzBuzz
challenge into their own classes and have them implement the RuleInterface
class FizzRule implements RuleInterface {
matches(number: number): boolean {
return number % 3 === 0;
}
getReplacement(): string {
return "Fizz";
}
}
class BuzzRule implements RuleInterface {
matches(number: number): boolean {
return number % 5 === 0;
}
getReplacement(): string {
return "Buzz";
}
}
class FizzBuzzRule implements RuleInterface {
matches(number: number): boolean {
return number % 3 === 0 && number % 5 === 0;
}
getReplacement(): string {
return "FizzBuzz";
}
}
And finally, lets make our FizzBuzz Class Open For Extension.
We allow our class to get a list of rules, and build the replacements based on them. These rules must implement the RuleInterface
making our code flexible & extensible.
class FizzBuzz {
rules: RuleInterface[] = [];
addRule(rule: RuleInterface) {
this.rules.push(rule);
}
generate(number: number) {
const output: string[] = [];
for (let i = 1; i <= number; i++) {
output.push(this.getReplacement(i));
}
return output;
}
getReplacement(number: number): string {
for (const rule of this.rules) {
if (rule.matches(number)) {
return rule.getReplacement();
}
}
return String(number);
}
}
const fizBuzz = new FizzBuzz();
fizBuzz.addRule(new FizzBuzzRule());
fizBuzz.addRule(new FizzRule());
fizBuzz.addRule(new BuzzRule());
const result = fizBuzz.generate(20);
// 1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz, 16, 17, Fizz, 19, Buzz
console.log(result.join(", "));
FizzBuzz Class and the Dependency Inversion Principle
The last of the SOLID principles of class design focuses on class dependencies. It tells you what kinds of things a class should depend on:
Depend on abstractions, not on concretions.
The principle tells we should always depend on abstractions(Interfaces, Abstract Classes) and not on concrete implementations.
Applying the Dependency Inversion principle in your code will make it easy for users to swap out certain parts of your code with other parts that are tailored to their specific situation. At the same time, your code remains general and abstract and therefore highly reusable.
By introducing the RuleInterface
and adding specific rule classes that implemented this interface, the FizzBuzz class started to depend on more abstract things, called "rules".
When creating a new FizzBuzz
instance, concrete implementations of RuleInterface
have to be injected in the right order. This will result in the correct execution of the FizzBuzz algorithm.
The FizzBuzz class itself is no longer concerned about the actual rules, which is why the class ends up being more flexible with regard to changing requirements.
Now the hardest part... Naming things!
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
Now we have a highly generic piece of code, which “generates a list of numbers, replacing certain numbers with strings, based on a flexible set of rules”.
There is nothing FizzBuzz specific about our class anymore!
Our class is generic and it should be renamed. Maybe something like NumberListReplacer
, not ideal, but more generic.
class NumberListReplacer
{
rules: RuleInterface[] = [];
addRule(rule: RuleInterface) {
this.rules.push(rule);
}
generate(number: number) {
let output: string[] = [];
for (let i = 1; i <= number; i++) {
output.push(this.getReplacement(i));
}
return output;
}
getReplacement(number: number): string {
for (let rule of this.rules) {
if (rule.matches(number)) {
return rule.replacement();
}
}
return String(number);
}
}
const fizBuzz = new NumberListReplacer();
fizBuzz.addRule(new FizzBuzzRule());
fizBuzz.addRule(new FizzRule());
fizBuzz.addRule(new BuzzRule());
const result = fizBuzz.generate(100);
// ex. replace all even numbers with a 'text'
const evenNumberReplacer = new NumberListReplacer();
evenNumberReplacer.addRule(new EvenNumberRule());
const result = evenNumberReplacer.generate(100000);
console.log(result.join(", "));
Links:
- Principles of Package Design: Creating Reusable Software Components is a brilliant book by Matthias Noback, it explains the SOLID principles brilliantly(and not only), the idea for the FizzBuzz implementation on example above is taken from there. https://matthiasnoback.nl/book/principles-of-package-design/
- https://khalilstemmler.com/articles/solid-principles/solid-typescript/
Top comments (8)
Object oriented programming is so much bloat. A lot simpler:
Nice, single responsibility in functional style. Even more clear.
Now even more functional :P
Awesome! I like how you explain how to apply the SOLID principle. :)
I'm curious that how to add the
Bazz
rule since it will haveBazz
,FizzBazz
,BuzzBazz
, andFizzBuzzBazz
and the order of rule is important. 🤔🤔🤔🤔good catch there. just missing a setRules(Rule []) method
I have a scenario in mind where this code could find a limit:
What if the rules are defined by a user and not by the programmer?
The user won't have access to the code to create a new class for a new rule. How could you adapt this code to accept user-defined rules (from a form for example)?
Yes single responsibility clearly manifest. Great post.
Doesn't
RuleInterface
have two responsibilities?First: matching numbers to rules
Second: getting a replacement string
Am I wrong?
Some comments have been hidden by the post's author - find out more