In Part 1 and Part 2 of my Intro to Test Driven Development posts, we worked our way through a Greed Kata exercise and discovered how we can use TDD to help design our code in a more generic and testable way. If you’re new to TDD, feel free to start with the first two parts of this series before continuing.
In this third (and final) post, I’ll discuss using the Rules Pattern to accomplish the same task (completing the Greed Kata). We’ll start with our final, working code from Part 2 of the series, and slowly refactor it (this time, without touching our tests) to provide the same results in a different way.
The rules pattern provides a way to help alleviate your program from conditional complexity (many if statements, for example) by creating separate classes for each rule. Each rule is then run through some type of processor (called a Rules Engine) which evaluates each rule in the collection, and performs some logic to determine which rule to use.
If you’re a SOLID developer (more on that in a future post), you’ll see that using the rules pattern helps you adhere to the O, or the Open / Closed Principle which states that your classes should be open for extension, but closed for modification. Once the rules pattern is implemented, you can add functionality to your program without modifying the main code.
Using this pattern also helps adhere to the S, or Single Responsibility Principle, in that your method should only have one reason for concern. By splitting this logic out into separate pieces, we’re also moving toward a more SOLID program. In its simplest form, the pattern consists of:
1 2 3 4 |
public interface IRule { int Eval(dieCounts); } |
1 2 3 4 5 6 7 8 |
public class SingleDieRule : IRule { public int Eval(dieCounts) { // do some logic to determine the score of a single die roll return score; } } |
1 2 3 4 5 6 7 8 |
// Add rule to collection _rules.add(new SingleDieRule); // Process the rules foreach(var rule in _rules) { // do some logic with each rule to determine which one to take } |
Now, let’s gather our thoughts and take a look at this more in depth!
First, make sure your code is in the state given at the end of Part 2. Then, let’s start out by first creating an interface in a new file called IRule.cs:
1 2 3 4 |
public interface IRule { int Eval(Dictionary<int, int> dieCounts); } |
This interface specifies that all rules that implement it must contain an Eval method that requires a Dictionary<int, int> parameter.
Next, let’s convert the ScoreSingleDie() method into a rule by creating a new rule in another new file, called SingleDieRule.cs:
1 2 3 4 5 6 7 8 9 10 |
public class SingleDieRule : IRule { public int Eval(Dictionary<int, int> dieCounts) { var dieValue = 0; dieValue += dieCounts[1] * 100; dieValue += dieCounts[5] * 50; return dieValue; } } |
Then, we’ll want to add the rule to a rule collection, and then create the rule engine. Let’s create a new private variable to hold the collection in our Game.cs file (this will go directly under the private _dieCounts variable declaration:
1 |
private List<IRule> _rules = new List<IRule>(); |
Finally, in the CalculateScore() method, we’ll comment out the current ScoreSingleDie() method, add the rule to a collection, and run the engine:
1 2 3 4 5 6 |
//score += ScoreSingleDie(); _rules.Add(new SingleDieRule()); foreach (var rule in _rules) { score += rule.Eval(_dieCounts); } |
This code adds your new SingleDieRule to the _rules collection, and the foreach loop loops through each rule in the collection, adding to the score each time a rule is matched. CalculateScore should now look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public int CalculateScore(params int[] dieValues) { if (dieValues.Length > 6 || dieValues.Length < 1) throw new System.Exception("You may only throw between 1 and 6 dice."); var score = 0; PopulateDieCounts(dieValues); score += ScoreMoreThanThreeOfAKind(); score += ScoreTriplePairs(); score += ScoreStraight(); score += ScoreTripleOnes(); score += ScoreTripleOthers(); //score += ScoreSingleDie(); _rules.Add(new SingleDieRule()); foreach (var rule in _rules) { score += rule.Eval(_dieCounts); } return score; } |
Give your tests a run – they should all still pass!
So now we’ve got a SingleDieRule, but it currently contains logic to only work on ones and fives. What if a rule is introduced to assign a point value to another number? We’d have to modify the rule each time a new single die rule is added. We can fix this problem by instead adding a constructor to the rule, and giving it parameters for the die number and value. Let’s modify the SingleDieRule to accept parameters:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class SingleDieRule : IRule { private readonly int _dieValue; private readonly int _score; public SingleDieRule(int dieValue, int score) { _dieValue = dieValue; _score = score; } public int Eval(Dictionary<int, int> dieCounts) { var dieValue = 0; dieValue += dieCounts[_dieValue] * _score; return dieValue; } } |
Here, we have added two private readonly variables to hold the values of what is passed into the constructor. We have also modified the Eval function to use the variables instead of the hard-coded point values we previously had. The only thing left to do is update the CalculateScore() method where we added the rule to include the new parameters (we’ll update the first one for a score of 100 for a die roll of 1, and add a new one for a score of 50 for a die roll of 5):
1 2 |
_rules.Add(new SingleDieRule(1, 100)); _rules.Add(new SingleDieRule(5, 50)); |
Now, any time a new scoring requirement is added (for example, score of 45 for rolling a 4), you can easily update the program by just adding another rule! Go ahead and run your tests – they should all still pass!
If we work our way up the chain, our next rules would be to score triple others, and then triple ones. If we use the constructor approach like we just did in step 2, we should be able to consolidate these into one rule. Let’s make a rule in a new file called TripleDiceRule and use some similar logic from the ScoreTripleOthers method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class TripleDiceRule : IRule { private readonly int _dieValue; private readonly int _score; public TripleDiceRule(int dieValue, int score) { _dieValue = dieValue; _score = score; } public int Eval(Dictionary<int, int> dieCounts) { if (dieCounts[_dieValue] >= 3) { return _score; } return 0; } } |
We’ll then add a new rule to the CalculateScore() method (let’s start with just triple ones for now), and comment out the ScoreTripleOnes(); call.
Add the rule:
1 |
_rules.Add(new TripleDiceRule(1, 1000)); |
Now run your tests…we have a failure! 🙁
You may have noticed the bug already if you’ve been coding along, but given the actual result of 1,300 vs. 1,000, it appears that dice are being counted more than once. In our original methods, we were removing dice from the dictionary once they were counted. However, in the rule system, I prefer to not have additional logic inside the rule definitions themselves. It’s better to have the rule engine handle any additional logic.
In order to make this work properly, the rules will have to return more information than just the integer value of the score. There can be various ways to handle this, but my approach will be to have the rule return an object that includes the score, and a dictionary of the dice that were used. Then, the rule processor will take that information and use it to remove any dice from the original dieCounts collection. Let’s step away from the rules for a few minutes and handle this.
Go ahead and make a new ScoreResult.cs class file, and add the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class ScoreResult { public int Score { get; set; } public Dictionary<int, int> DiceUsed { get; set; } public ScoreResult() { } public ScoreResult(int score, Dictionary<int, int> diceUsed) { Score = score; DiceUsed = diceUsed; } } |
Now that we have this, we will need to update our rules to return a ScoreResult instead of an int. Let’s start by updating the interface:
1 2 3 4 |
public interface IRule { ScoreResult Eval(Dictionary<int, int> dieCounts); } |
Then the SingleDieRule:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class SingleDieRule : IRule { private readonly int _dieValue; private readonly int _score; public SingleDieRule(int dieValue, int score) { _dieValue = dieValue; _score = score; } public ScoreResult Eval(Dictionary<int, int> dieCounts) { var score = 0; score += dieCounts[_dieValue] * _score; var diceUsed = new Dictionary<int, int>(); diceUsed.Add(_dieValue, 1); return new ScoreResult(score, diceUsed); } } |
Next, the TripleDiceRule:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class TripleDiceRule : IRule { private readonly int _dieValue; private readonly int _score; public TripleDiceRule(int dieValue, int score) { _dieValue = dieValue; _score = score; } public ScoreResult Eval(Dictionary<int, int> dieCounts) { if (dieCounts[_dieValue] >= 3) { var diceUsed = new Dictionary<int, int>(); diceUsed.Add(_dieValue, 3); var scoreResult = new ScoreResult(_score, diceUsed); return scoreResult; } return new ScoreResult(0, new Dictionary<int, int>()); } } |
And finally, our rule engine must be updated to process the new information received from the rules:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
foreach (var rule in _rules) { // Store the scoreResult into a variable ScoreResult scoreResult = rule.Eval(_dieCounts); // Add the resulting score to the score counter score += scoreResult.Score; // Loop through each value in the diceUsed collection and // subtract those dice from the _dieCounts collection foreach (var dieUsed in scoreResult.DiceUsed) { _dieCounts[dieUsed.Key] -= dieUsed.Value; } } |
Run the tests again… argh! Another failure!
This time we got 300 instead of 1000. After some debugging, it appears that the system is taking the first rule that matches, and not necessarily the best rule. There are a couple ways to handle this. One way is to assign a priority to each rule and have them processed in a specific order. The other way (the way I’ll demonstrate) is to have the rule evaluator determine which rule is the best, and automatically apply it.
We’ll have to re-work our rule evaluator a little bit so that it can determine which rule is the best. Here’s my pseudocode on how I’ll handle it:
1 2 3 4 5 6 7 8 |
-Loop through procedure as long as there are dice in the collection -Loop through each rule -if the resulting score is larger than the current score, save the result -Update the score -If diceUsed is empty, return the score -Otherwise, remove the used dice from the collection -Return score |
Translating that into real code, let’s update our CalculateScore() method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
public int CalculateScore(params int[] dieValues) { if (dieValues.Length > 6 || dieValues.Length < 1) throw new System.Exception("You may only throw between 1 and 6 dice."); var score = 0; PopulateDieCounts(dieValues); score += ScoreMoreThanThreeOfAKind(); score += ScoreTriplePairs(); score += ScoreStraight(); //score += ScoreTripleOnes(); score += ScoreTripleOthers(); //score += ScoreSingleDie(); _rules.Add(new SingleDieRule(1, 100)); _rules.Add(new SingleDieRule(5, 50)); _rules.Add(new TripleDiceRule(1, 1000)); while (_dieCounts.Any(x => x.Value > 0)) { ScoreResult bestResult = new ScoreResult(); foreach (var rule in _rules) { ScoreResult scoreResult = rule.Eval(_dieCounts); if (scoreResult.Score > bestResult.Score) { bestResult = scoreResult; } } score += bestResult.Score; if (bestResult.DiceUsed == null) { return score; } foreach (var dieUsed in bestResult.DiceUsed) { _dieCounts[dieUsed.Key] -= dieUsed.Value; } } return score; } |
Now give your tests a run – back to all green (they should be, at least)!
Now that we’re back to all green, let’s refactor the TripleDiceRule. If we really think about it, now that we have a rule for Triple dice, it should be fairly simple to modify it to handle any set of dice, whether it be 3, 4, 5 or 6. I’m going to rename the rule to SetOfDiceRule, add an additional parameter called setSize. Rename the TripleDiceRule.cs file to SetOfDiceRule.cs and let’s update the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class SetOfDiceRule : IRule { private readonly int _setSize; private readonly int _dieValue; private readonly int _score; public SetOfDiceRule(int setSize, int dieValue, int score) { _setSize = setSize; _dieValue = dieValue; _score = score; } public ScoreResult Eval(Dictionary<int, int> dieCounts) { if (dieCounts[_dieValue] >= _setSize) { var diceUsed = new Dictionary<int, int>(); diceUsed.Add(_dieValue, _setSize); var scoreResult = new ScoreResult(_score, diceUsed); return scoreResult; } return new ScoreResult(0, new Dictionary<int, int>()); } } |
Now let’s also update the TripleDiceRule in the CalculateScore() method:
1 |
_rules.Add(new SetOfDiceRule(3, 1, 1000)); |
Run your tests again, and we should be all green!
Now that we have a generic rule, we can simply add more rules to the collection to handle all of our other sets. Let’s add those rules now. For the special cases (4 ones is 2000, 5 is 4000 and 6 is 8000) we’ll write those out. But we can create the others using some for loops:
1 2 3 4 5 6 7 8 9 10 |
_rules.Add(new SetOfDiceRule(4, 1, 2000)); for (int i = 2; i < 7; i++) { _rules.Add(new SetOfDiceRule(3, i, i * 100)); _rules.Add(new SetOfDiceRule(4, i, i * 200)); _rules.Add(new SetOfDiceRule(5, i, i * 400)); _rules.Add(new SetOfDiceRule(6, i, i * 800)); } _rules.Add(new SetOfDiceRule(5, 1, 4000)); _rules.Add(new SetOfDiceRule(6, 1, 8000)); |
Since we have all of these rules in here now, we can remove (or temporarily comment out) the existing calls for ScoreMoreThanThreeOfAKind(), and ScoreTripleOthers(). Go ahead and comment or remove those lines, and run your tests. The CalculateScore() method should looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
public int CalculateScore(params int[] dieValues) { if (dieValues.Length > 6 || dieValues.Length < 1) throw new System.Exception("You may only throw between 1 and 6 dice."); var score = 0; PopulateDieCounts(dieValues); score += ScoreTriplePairs(); score += ScoreStraight(); _rules.Add(new SingleDieRule(1, 100)); _rules.Add(new SingleDieRule(5, 50)); _rules.Add(new SetOfDiceRule(3, 1, 1000)); _rules.Add(new SetOfDiceRule(4, 1, 2000)); for (int i = 2; i < 7; i++) { _rules.Add(new SetOfDiceRule(3, i, i * 100)); _rules.Add(new SetOfDiceRule(4, i, i * 200)); _rules.Add(new SetOfDiceRule(5, i, i * 400)); _rules.Add(new SetOfDiceRule(6, i, i * 800)); } _rules.Add(new SetOfDiceRule(5, 1, 4000)); _rules.Add(new SetOfDiceRule(6, 1, 8000)); while (_dieCounts.Any(x => x.Value > 0)) { ScoreResult bestResult = new ScoreResult(); foreach (var rule in _rules) { ScoreResult scoreResult = rule.Eval(_dieCounts); if (scoreResult.Score > bestResult.Score) { bestResult = scoreResult; } } score += bestResult.Score; if (bestResult.DiceUsed == null) { return score; } foreach (var dieUsed in bestResult.DiceUsed) { _dieCounts[dieUsed.Key] -= dieUsed.Value; } } return score; } |
…and your tests should all pass!
At this point, you should have everything you need to know to complete the remaining two rules (ScoreTriplePairs() and ScoreStraight()). I’m going to take care of them both here:
Create a TriplePairsRule.cs file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public class TriplePairsRule : IRule { private readonly int _score; public TriplePairsRule(int score) { _score = score; } public ScoreResult Eval(Dictionary<int, int> dieCounts) { int numberOfPairs = 0; var diceUsed = new Dictionary<int, int>(); for (int i = 1; i <= 6; i++) { if (dieCounts[i] == 2) { numberOfPairs++; diceUsed.Add(i, 2); } } if (numberOfPairs == 3) { var scoreResult = new ScoreResult(_score, diceUsed); return scoreResult; } return new ScoreResult(0, new Dictionary<int, int>()); } } |
Create a StraightRule.cs file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class StraightRule : IRule { private readonly int _score; public StraightRule(int score) { _score = score; } public ScoreResult Eval(Dictionary<int, int> dieCounts) { var diceUsed = new Dictionary<int, int>(); if (dieCounts.All(x => x.Value == 1)) { foreach (var dieValue in dieCounts.Keys.ToList()) { diceUsed.Add(dieValue, 1); } return new ScoreResult(_score, diceUsed); } return new ScoreResult(0, new Dictionary<int, int>()); } } |
Don’t forget to update your CalculateScore() method to remove the old methods, and add the new rules.
At this point, all of your tests should pass, and all the conditions of the kata are now met! Congrats! One final thing I would do is refactor the code that adds the rules into its own method. Once you do that, you final Game.cs class should look like this (download it from GitHub):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
public class Game { private Dictionary<int, int> _dieCounts = new Dictionary<int, int>(); private List<IRule> _rules = new List<IRule>(); private void PopulateDieCounts(int[] dieValues) { for (int i = 1; i <= 6; i++) { _dieCounts.Add(i, dieValues.Count(d => d == i)); } } private void AddRules() { _rules.Add(new SingleDieRule(1, 100)); _rules.Add(new SingleDieRule(5, 50)); _rules.Add(new SetOfDiceRule(3, 1, 1000)); _rules.Add(new SetOfDiceRule(4, 1, 2000)); for (int i = 2; i < 7; i++) { _rules.Add(new SetOfDiceRule(3, i, i * 100)); _rules.Add(new SetOfDiceRule(4, i, i * 200)); _rules.Add(new SetOfDiceRule(5, i, i * 400)); _rules.Add(new SetOfDiceRule(6, i, i * 800)); } _rules.Add(new SetOfDiceRule(5, 1, 4000)); _rules.Add(new SetOfDiceRule(6, 1, 8000)); _rules.Add(new TriplePairsRule(1800)); _rules.Add(new StraightRule(1200)); } public int CalculateScore(params int[] dieValues) { if (dieValues.Length > 6 || dieValues.Length < 1) throw new System.Exception("You may only throw between 1 and 6 dice."); var score = 0; PopulateDieCounts(dieValues); AddRules(); while (_dieCounts.Any(x => x.Value > 0)) { ScoreResult bestResult = new ScoreResult(); foreach (var rule in _rules) { ScoreResult scoreResult = rule.Eval(_dieCounts); if (scoreResult.Score > bestResult.Score) { bestResult = scoreResult; } } score += bestResult.Score; if (bestResult.DiceUsed == null) { return score; } foreach (var dieUsed in bestResult.DiceUsed) { _dieCounts[dieUsed.Key] -= dieUsed.Value; } } return score; } } |