loading...

FizzBuzz Typescript & SOLID Principles

st0ik profile image Dimitris Stoikidis ・4 min read

"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:

Discussion

markdown guide
 

Object oriented programming is so much bloat. A lot simpler:

type Rule = (n: number)  => string;

function mkRule(div: number, word: string): Rule {
    return n => n % div === 0 ? word : '';
}

function range(start: number, end: number) {
    return [...Array(end - start).keys()]
        .map(x => x + start);
}

function fizzbuzz(rules: Rule[], end = 100): string[] {
    return range(1, end)
        .map(i => {
            const str = rules
                .reduce((word, rule) => word + rule(i), '');
            return str === '' ? `${i}` : str;
        });
}

const answer = fizzbuzz([
    mkRule(3, 'Fizz'),
    mkRule(5, 'Buzz')
]);

console.log(answer.join(', '));
 

Nice, single responsibility in functional style. Even more clear.

 
 

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 have Bazz , FizzBazz, BuzzBazz , and FizzBuzzBazz and the order of rule is important. 🤔🤔🤔🤔

 

good catch there. just missing a setRules(Rule []) method

 

Yes single responsibility clearly manifest. Great post.