A while ago I was attending the Global day of Coderetreat in Zurich. It is a day long coding practice event. Attendees work together in groups of two. In each session we coded the kata “game of life” from scratch puttygen , each time with different constraints. The constraint I found very interesting was this: Code game of life without if-statement or loop. In one of the sessions we tried to implement the the solution using a rules based approach. The sessions were not long enough to totally finish the code. It was more to see how a particular approach work. By the way it was amazing how many ways there are to code this simple program. Anyway I was fascinated by this rules based approach so later I tried this approach on the kata Fizzbuzz.
Fizzbuzz
Fizzbuzz is simple: Write a method that prints the number you put in except print “fizz” if the number is dividable by 3, and “buzz” if it is dividable by 5, and “fizzbuzz” if it is dividable by 3 and 5.
Simple Solution
The first solution that comes to mind would probably be something like this:
public class FizzBuzzer { public static string Evaluate(int number) { if (number % 15 == 0) { return "fizzbuzz"; } else if (number % 3 == 0) { return "fizz"; } else if (number % 5 == 0) { return "buzz"; } else { return number.ToString(); } } }
For this simple requirement this solution is completely fine. However what would you do if you wanted to be able to add new rules to the program without having to modify places where the exiting rules are coded?
Rule Interface: IRule
One could hide the behavior of a rule behind an interface. DoesApply() tells if the rule applies. Apply() applies the rule and executes it.
public interface IRule { string Apply(int number); bool DoesApply(int number); }
The implementation of this interface for the Fizz-rule looks like this:
public class FizzRule : IRule { public bool DoesApply(int number) { return number % 3 == 0; } public string Apply(int number) { return "fizz"; } }
Basically the if part of the first solution has moved to DoesApply() and the “calculation” has moved to Apply().
How to Use the Rules
Now we could create a list with all the rules and apply the rules in a foreach loop.
public class FizzBuzzer { private List<IRule> _rules; public FizzBuzzer() { _rules = new List<IRule>(); _rules.Add(new FizzBuzzRule()); _rules.Add(new FizzRule()); _rules.Add(new BuzzRule()); _rules.Add(new CatchAllRule()); } public string Evaluate(int number) { foreach (var rule in _rules) { if (rule.DoesApply(number)) { return rule.Apply(number); } } return null; }
Chain of Responsibility Pattern
However the constraint says no if and no loop. Lets use the Chain of Responsibility pattern to get rid of the loop.
SetNextHandler() is used to create the chain of concrete handlers and HandleRequest() uses the methods in IRules interface to either handle the request directly or pass it to the next handler.
public abstract class Handler : IRule { Handler _nextHandler; public void SetNextHandler(Handler nextHandler) { _nextHandler = nextHandler; } public string HandleRequest(int number) { return DoesApply(number) ? Apply(number) : _nextHandler.HandleRequest(number); } // IRules interface public abstract string Apply(int number); public abstract bool DoesApply(int number); }
Because the class hierarchy changed a bit the rules are renamed to handlers and derive from Handler. Here’s the FizzHandler as example.
public class FizzHandler : Handler { public override bool DoesApply(int number) { return number % 3 == 0; } public override string Apply(int number) { return "fizz"; } }
Now we need to construct the objects which is done in the class FizzBuzzer. Here it is:
public class FizzBuzzer { private Handler _firstHandler = null; public FizzBuzzer() { AddHandler(new CatchAllHandler()); AddHandler(new BuzzHandler()); AddHandler(new FizzHandler()); AddHandler(new FizzBuzzHandler()); } private void AddHandler(Handler previousHandler) { previousHandler.SetNextHandler(_firstHandler); _firstHandler = previousHandler; } public string Evaluate(int number) { return _firstHandler.HandleRequest(number); } }
You can find the complete FizzBuzz solution on Github. 615-544-6650