Skip to content

Over Engineering FizzBuzz

Introduction

FizzBuzz is a classic programming challenge for beginners or interviews (much like using factorials to demonstrate loops and recurision). The task is pretty straightforward. Usually it's something like

Print to the screen numbers from 1 to 100, but substitute numbers divisible by 3 with 'Fizz', those divisible by 5 with 'Buzz' and those divisible by both with 'FizzBuzz'.

A pretty straightforward way of doing this goes something like this. Consider the following markup:

<body>
  <div id="container"></div>
</body>

Do a for loop like this

const container = document.getElementById('container');
for (i = 1; i < 100; i++) {
   const p = document.createElement('p')
   if (i % 3 === 0 && i % 5 ===0) {
     p.innerText = 'FizzBuzz';
   } else if (i % 5 === 0) {
     p.innerText = 'Fizz';
   }  else if (i % 3 === 0) {
     p.innerText = 'Buzz';
   } else {
     p.innerText = i
   }
   container.appendChild(p)
}

But this code isn't very maintainable. Sure it solves the problem. But, what if someone needed to modify it by changing 'Buzz' to 'Baz' or the 3 to a 2? Or change the values around? What if it needed to be used over and over? This approach also mixes logic and item creation.

So, let's see if FizzBuzz can be made more flexible! Exciting right!? Right? Right.

Requirements

Consider the requirements of the problem for a moment. What needs to happen?

  • Iterate over a set of numbers
  • Test each of those numbers for its divibility against 2 other numbers
  • Print a result based on that test to a page
  • Select an element to attach the printed element to
  • Select a type of element to print

It seems to me then that there are 4 parts of this problem that could be separated into different functions

  • The for loop itself
  • The values (like 'Fizz' or 5) and elements (like the container) that need to be used.
  • The logic for choosing which value should be printed
  • Creating the markup

Configuration

If we're talking about functions, it would be nice to pass them arguments; and if we're talking about arguments, it would be nice to use an object that we could easily configure. So what if I start like this?

const config = {
  fizzbuzz: {
    fizz: {
      val: 3,
      message: 'Fizz'
    },
    buzz: {
      val: 5,
      message: 'Buzz'
    },
  },
  elements: {
    containerID: 'container',
    itemType: 'p'
  }
}

Next, I want the actual container element to attach to. This only needs to happen once though

const config = {
  fizzbuzz: {
    fizz: {
      val: 3,
      message: 'Fizz'
    },
    buzz: {
      val: 5,
      message: 'Buzz'
    },
  },
  elements: {
    containerID: 'container',
    itemType: 'p'
  }
}

const container = document.getElementById(config.elements.containerID);

// add the container to the config object
config.elements.container = container;

A For Loop and 2 Functions

We've already got an example to work with from the first for loop. But I think it would be nice to have the loop execute a single function. It's going to need two arguments:

  • The current number in the loop
  • The config object
for (i = 1; i < 101; i++) {
  createMarkup(i, config)
}

Create Markup function

The createMarkup has just one job.

Put things on the page.

const createMarkup = (val, config) => {
  // destructure the config object.
  // there's no point passing the elements to the fizzbuzz logic
  const { fizzbuzz, elements } = config
  
  // create a new item of the type defined
  const item = document.createElement(elements.itemType);
  
  // set the text of the item to the value returned by the fizzbuzz function
  item.innerText = fizzBuzz(val, fizzbuzz)
  
  // place the item inside the container
  elements.container.appendChild(item)
}

FizzBuzz function

Now, the fizzBuzz function has only one job as well

Handle the logic.

const fizzBuzz = (val, config) => {
  // destructure the config object
  const { fizz, buzz } = config
  
  // compare the value against the options and return a result
  if (val % (fizz.val * buzz.val) === 0) {
    return fizz.message + buzz.message;
  } else if (val % fizz.val === 0) {
    return fizz.message;
  } else if (val % buzz.val === 0) {
    return buzz.message
  } else {
    return val
  }
}

Putting it together

Here's what the whole thing might look like

const config = {
  fizzbuzz: {
    fizz: {
      val: 3,
      message: 'Fizz'
    },
    buzz: {
      val: 5,
      message: 'Buzz'
    },
  },
  elements: {
    containerID: 'container',
    itemType: 'p'
  }
}

const container = document.getElementById(config.elements.containerID);

config.elements.container = container;

const fizzBuzz = (val, config) => {
  const { fizz, buzz } = config
  
  if (val % (fizz.val * buzz.val) === 0) {
    return fizz.message + buzz.message;
  } else if (val % fizz.val === 0) {
    return fizz.message;
  } else if (val % buzz.val === 0) {
    return buzz.message
  } else {
    return val
  }
}

const createMarkup = (val, config) => {
  const { fizzbuzz, elements } = config

  const item = document.createElement(elements.itemType);

  item.innerText = fizzBuzz(val, fizzbuzz)

  elements.container.appendChild(item)
}

for (i = 1; i < 101; i++) {
  createMarkup(i, config)
}

What's the point?

How is this better!? It's nearly 5 times longer than the first solution! Wel... That's true. But, you could change how the whole thing works by changing the properties of the config object.

It also separates the logic of which message should be used from printing adding that message to the page. This is useful since it makes the code easier to test. Values aren't printing? Well then it's probably an issue with the createMarkup function. Wrong values being printed? Oh. Well that's probably a problem with the fizzBuzz function.

Conclusion

Obviously this is kind of a silly exercise. But, I think the point is serious. We get to choose how we write code. After three years as a web developer, I've seen a lot of code (and probably written some) that solved the problem at the time and then was abandonded because a deadline was coming.

Sure, there are a lot of times when I'll start with the simplest thing I can think of just to get things going. Yet that doesn't mean that's where I stop thinking.