Andy Crouch - Code, Technology & Obfuscation ...

A Clean Fizzbuzz Solution

Water Splash

Photo: Unsplash

Fizzbuzz is an interesting little test based on a maths game used in some schools. Used to teach division, the Wikipedia entry states:

“Fizz buzz is a group word game for children to teach them about division.[1] Players take turns to count incrementally, replacing any number divisible by three with the word “fizz”, and any number divisible by five with the word “buzz”.”

Developers have used this game for many years to test interview candidates. It is also used for general programming and TDD practice (know as Kata’s). The profession has slightly altered the game and the version generally used is:

“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”.”

I have been using the Fizzbuzz problem as a Kata for one of my junior developers. They are very junior. Their training is focusing not only on language and framework features but also agile and clean approaches to coding. A lot of developers use Fizzbuzz to improve their TDD skills. Given this, I’ve always been quite surprised by the basic implementations found online.

My developer spent a couple of weeks creating slightly different versions. At each training session, we would discuss the approach taken and I would suggest changes. The developer was interested to see what my solution might look like. So I sat down and created what I consider to be an acceptable solution to the problem. This is very much a C# solution so please bear this in mind.

The full solution can be viewed on Github.

A lot of the solutions I see implement the logic in some fixed manner. For example:

public void DoFizzBuzz()
{
    for (int i = 1; i <= 100; i++)
    {
        bool fizz = i % 3 == 0;
        bool buzz = i % 5 == 0;
        if (fizz && buzz)
            Console.WriteLine ("FizzBuzz");
        else if (fizz)
            Console.WriteLine ("Fizz");
        else if (buzz)
            Console.WriteLine ("Buzz");
        else
            Console.WriteLine (i);
    }
}

It would be hard to test this code and no way to extend it without changing the implementation. If we had to change the logic for printing “Buzz” in place of numbers divisible by 4 this code would need to change. This is a clear violation of Bertrand Meyer’s Open/Closed principle. A clear explanation of the principle can be found here. To adhere to the principle I took a simple approach using an abstraction and reflection.

My FizzbuzzGenerator class was straightforward to write following a test first approach:

public class FizzbuzzGenerator
{
    List<INumberParser> _numberParsers;

    public List<string> GenerateResultsBetween(int rangeStart, int rangeEnd)
    {
        var resultSet = new List<string>();
        var inputRange = Enumerable.Range(rangeStart, rangeEnd).ToList();
        
        inputRange.ForEach(i =>
        {
            INumberParser numberParser 
                = Parsers.First(p => i % p.Divisor == 0);

            resultSet.Add(numberParser.Parse(i));
        });
        
        return resultSet;
    }
    
    private List<INumberParser> Parsers
    {
        get
        {
            if (_numberParsers != null)
                return _numberParsers;
        
            var interfaceType = typeof(INumberParser);
            return 
                _numberParsers 
                   = AppDomain.CurrentDomain.GetAssemblies()
                         .SelectMany(x => x.GetTypes())
                         .Where(x => interfaceType.IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract)
                         .Select(x => Activator.CreateInstance(x))
                         .Cast<INumberParser>()
                         .OrderByDescending(p => p.Divisor)
                         .ToList();
        }
    }
}

I defined an INumberParser interface to outline the behaviour of any object that could parse an integer to a result string. This means that I have multiple INumberParser based objects to implement the Fizzbuzz rules. My generator class is then just used to find all of the instances of INumberParser and process each number in the range specified before adding the result of the rule to the resultSet list.

Each rule is implemented as follows:

public class BuzzNumberParser : INumberParser
{
    public int Divisor => 5;
    
    public string Parse(int number)
    {
        if (number % Divisor == 0)
            return "Buzz";
        
        return number.ToString();
    }
}

This is straight forward to read. We specify the Divisor that the rule will handle and provide a mechanism to parse an input number and return the result of the rule.

I do appreciate I could actually implement a base class that inherits INuberParser and reduce the parse code down to one instance but in this case I was trying to show agility of code to the developer.

The full solution can be found here.

The benefits of a solution along these lines is clear. Firstly, this code can be easily tested and was the result of using a Test First approach. Secondly, any extensions to the rules or changes of logic are restricted to each INumberParser based rule class. The Generator class never has to change. If I now need to handle a rule that prints “burp” for any number divisible by 50 then I define a new INumberParser based class and implement the logic. I build and run the program and it will work. A single place to update our logic. There are no messy if or switch statements.

The last thing I would say about this solution is that it didn’t take very long to create. Therefore, I would question a reasonable developer not being able to create a solution similar to this when given 20 minutes to code. If I ever used FizzBuzz as an interview question I would certainly rank a candidate higher for implementing a clean, agile solution like this as this is the approach we follow and the standard I’d expect within our codebase.

If you’d like to discuss any of my thoughts here then as always please contact me via twitter or email.