An object-oriented approach to FizzBuzz

FizzBuzz is described as:

Write a program that prints the integers 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”

It takes a couple of minutes to write a few lines of C++ code that satisfy the requirements:

{codecitation brush: cpp; ruler: true;}#include <iostream>

using namespace std;
int main () {

int i;
for (i = 0; i <= 100; i++) {
if ((i % 15) == 0)
cout << “FizzBuzz” << endl;
else if ((i % 3) == 0)
cout << “Fizz” << endl;
else if ((i % 5) == 0)
cout << “Buzz” << endl;
else
cout << i << endl;
}
return 0;
} {/codecitation}

But then the examiner may ask you to add some more checks, such as printing a different string when the number is a multiple of 11 or any other number, and the code quickly becomes ugly. In fact, the major mistake that you can make with FizzBuzz is using the wrong sequence of checks, for example checking for modulus of 3 before that of 15, so that FizzBuzz is never printed; this is obvious with only 3 simple checks, but gets more complicated with a longer list of more complicated checks.

So why not spend a few minutes more than those required to write a trivial solution, and propose a fully object-oriented solution that is designed for extensibility?

FizzBuzzClass1

The proposed solution packs each check in a class that is derived by an abstract base class (BaseCheck), and the checking is encapsulated in the RunCheck method. Finally, another class named NumCheckCollection maintains a list of checks and runs through them for each number in the given list.

{CODE brush: cpp; ruler: true;}#include <list>
#include <iostream>

using namespace std;

class BaseCheck {
public:
virtual bool RunCheck(const int CheckValue) = 0;
};

class Num3Check : public BaseCheck {
bool RunCheck(const int CheckValue);
};

class Num5Check : public BaseCheck {
bool RunCheck(const int CheckValue);
};

class Num15Check : public BaseCheck {
bool RunCheck(const int CheckValue);
};

class NumCheckCollection {
public:
NumCheckCollection();
~NumCheckCollection();
void AddNumCheck(BaseCheck *NewCheck);
bool RunChecks(const int CheckValue);
private:
list<BaseCheck *> ChecksList;
};

bool Num3Check::RunCheck(const int CheckValue)
{
if ((CheckValue % 3) == 0)
{
cout << “Fizz” << endl;
return true;
} else
return false;
}

bool Num5Check::RunCheck(const int CheckValue)
{
if ((CheckValue % 5) == 0)
{
cout << “Buzz” << endl;
return true;
} else
return false;
}

bool Num15Check::RunCheck(const int CheckValue)
{
if ((CheckValue % 15) == 0)
{
cout << “FizzBuzz” << endl;
return true;
} else
return false;
}

void NumCheckCollection::AddNumCheck(BaseCheck *NewCheck)
{
ChecksList.push_back(NewCheck);
}

bool NumCheckCollection::RunChecks(const int CheckValue)
{
bool SuccessfulCheck = false;
for (auto ChecksListIterator = ChecksList.begin(); ChecksListIterator != ChecksList.end(); ++ChecksListIterator)
{
if ((*ChecksListIterator)->RunCheck(CheckValue))
{
SuccessfulCheck = true;
break;
}
}
return SuccessfulCheck;
}

NumCheckCollection::NumCheckCollection()
{

}

NumCheckCollection::~NumCheckCollection()
{
for (auto ChecksListIterator = ChecksList.begin(); ChecksListIterator != ChecksList.end(); ++ChecksListIterator)
delete *ChecksListIterator;
ChecksList.clear();
}

int _tmain(int argc, _TCHAR* argv[])
{
NumCheckCollection CheckCollection;
CheckCollection.AddNumCheck(new Num15Check());
CheckCollection.AddNumCheck(new Num5Check());
CheckCollection.AddNumCheck(new Num3Check());

for (int Counter = 1; Counter <= 100; Counter++)
{
if (!CheckCollection.RunChecks(Counter))
{
cout << Counter << endl;
}
}

getchar();
return 0;
}{/CODE}

This must be the longest FizzBuzz implementation ever… But let’s have a look at the pros:

  • each check is encapsulated in a clearly-named class, so the declaration of the sequence of checks becomes much clearer, as the calls to the AddNumCheck method show the exact sequence of checks performed
  • this class design can support any other check, just inherit another class from BaseCheck and define the RunCheck method, without messing with the existing code
  • it gives you an opportunity to discuss several OO design choices,  such as abstract base classes

FizzBuzzClass2

Still, there is a lot of similar code in the 3 concrete classes, so we can refactor the code to move the bulk of the RunCheck method in a base class, and greatly simplify the implementation of the derived classes:

{CODE brush: cpp; ruler: true;}#include <list>
#include <iostream>
#include <string>

using namespace std;

class BaseCheck {
public:
virtual bool RunCheck(const int CheckValue) = 0;
virtual ~BaseCheck() {};
};

class ModulusCheck : public BaseCheck {
public:
ModulusCheck(const int ModulusValue, const string MessageValue);
bool RunCheck(const int CheckValue);
~ModulusCheck() {};
private:
int Modulus;
string Message;
};

class Modulus3Check : public ModulusCheck {
public:
Modulus3Check() : ModulusCheck(3, “Fizz”) {};
~Modulus3Check() {};
};

class Modulus5Check : public ModulusCheck {
public:
Modulus5Check() : ModulusCheck(5, “Buzz”) {};
~Modulus5Check() {};
};

class Modulus15Check : public ModulusCheck {
public:
Modulus15Check() : ModulusCheck(15, “FizzBuzz”) {};
~Modulus15Check() {};
};

class NumCheckCollection {
public:
NumCheckCollection();
~NumCheckCollection();
void AddNumCheck(BaseCheck *NewCheck);
bool RunChecks(const int CheckValue);
private:
list<BaseCheck *> ChecksList;
};

ModulusCheck::ModulusCheck(const int ModulusValue, const string MessageValue)
{
Modulus = ModulusValue;
Message = MessageValue;
}

bool ModulusCheck::RunCheck(const int CheckValue)
{
if ((CheckValue % Modulus) == 0)
{
cout << Message << endl;
return true;
} else
return false;
}

void NumCheckCollection::AddNumCheck(BaseCheck *NewCheck)
{
ChecksList.push_back(NewCheck);
}

bool NumCheckCollection::RunChecks(const int CheckValue)
{
bool SuccessfulCheck = false;
for (auto ChecksListIterator = ChecksList.begin(); ChecksListIterator != ChecksList.end(); ++ChecksListIterator)
{
if ((*ChecksListIterator)->RunCheck(CheckValue))
{
SuccessfulCheck = true;
break;
}
}
return SuccessfulCheck;
}

NumCheckCollection::NumCheckCollection()
{
}

NumCheckCollection::~NumCheckCollection()
{
for (auto ChecksListIterator = ChecksList.begin(); ChecksListIterator != ChecksList.end(); ++ChecksListIterator)
delete *ChecksListIterator;
ChecksList.clear();
}

int _tmain(int argc, _TCHAR* argv[])
{
NumCheckCollection CheckCollection;
CheckCollection.AddNumCheck(new Modulus15Check());
CheckCollection.AddNumCheck(new Modulus5Check());
CheckCollection.AddNumCheck(new Modulus3Check());

for (int Counter = 1; Counter <= 100; Counter++)
{
if (!CheckCollection.RunChecks(Counter))
{
cout << Counter << endl;
}
}

getchar();
return 0;
}{/CODE}

The NumCheckCollection directly deletes the instances of ModulusXCheck in its destructor calling a pointer to the base class, so it is necessary to create a virtual destructor to ensure that proper clean-up is performed.

But are derived classes (Modulus15Check, Modulus5Check and Modulus3Check) really necessary? Actually, they are not, as you can replace

{CODE brush: cpp; ruler: true;} CheckCollection.AddNumCheck(new Modulus15Check()); CheckCollection.AddNumCheck(new Modulus5Check()); CheckCollection.AddNumCheck(new Modulus3Check()); {/CODE}

with

{CODE brush: cpp; ruler: true;} CheckCollection.AddNumCheck(new ModulusCheck(15, “FizzBuzz”));
CheckCollection.AddNumCheck(new ModulusCheck(5, “Buzz”));
CheckCollection.AddNumCheck(new ModulusCheck(3, “Fizz”)); {/CODE}

so that the declaration of concrete classes is not needed anymore. But I think this is a mistake, as it mixes the declaration of the behaviour of each class with the creation of class instances, requiring the user to know how each is implemented instead of just using them. Please note that these examples are trivial, as the checks in FizzBuzz are extremely simple, but in a real-world solution these checks may be a lot more complex, using far more parameters, so the importance of adding a layer of abstraction becomes clearer.

Still, are these classes really necessary to model such a simple behaviour? In fact, we can get rid of them and just use function pointers. The following solution is still scalable and can be easily extended, as additional behaviours can be modeled with more functions sharing the calling parameters (as detailed in the CheckFunction typedef), but does not use any OOD:

{CODE brush: cpp; ruler: true;}#include <list>
#include <iostream>
#include <string>

using namespace std;

bool GenericModulusCheck(const int CheckValue, const int RefValue, const string MessageValue)
{
if ((CheckValue % RefValue) == 0)
{
cout << MessageValue << endl;
return true;
} else
return false;
}

bool Modulus3Check(const int CheckValue)
{
return GenericModulusCheck(CheckValue, 3, “Fizz”);
}

bool Modulus5Check(const int CheckValue)
{
return GenericModulusCheck(CheckValue, 5, “Buzz”);
}

bool Modulus15Check(const int CheckValue)
{
return GenericModulusCheck(CheckValue, 15, “FizzBuzz”);
}

typedef bool (*CheckFunction)(const int CheckValue);

class NumCheckCollection {
public:
NumCheckCollection() {};
~NumCheckCollection() {};
void AddNumCheck(CheckFunction NewCheck);
bool RunChecks(const int CheckValue);
private:
list<CheckFunction> ChecksList;
};

void NumCheckCollection::AddNumCheck(CheckFunction NewCheck)
{
ChecksList.push_back(NewCheck);
}

bool NumCheckCollection::RunChecks(const int CheckValue)
{
bool SuccessfulCheck = false;
for (auto ChecksListIterator = ChecksList.begin(); ChecksListIterator != ChecksList.end(); ++ChecksListIterator)
{
if ((*ChecksListIterator)(CheckValue))
{
SuccessfulCheck = true;
break;
}
}
return SuccessfulCheck;
}

int _tmain(int argc, _TCHAR* argv[])
{
NumCheckCollection CheckCollection;
CheckCollection.AddNumCheck(&Modulus15Check);
CheckCollection.AddNumCheck(&Modulus5Check);
CheckCollection.AddNumCheck(&Modulus3Check);

for (int Counter = 1; Counter <= 100; Counter++)
{
if (!CheckCollection.RunChecks(Counter))
{
cout << Counter << endl;
}
}

getchar();
return 0;
} {/CODE}

Leave a Reply

Your email address will not be published.