Intro to Test Driven Development (TDD) Part 3 – Using a Rules Engine / Rules Pattern

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.

What is the Rules Pattern?

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:

  • An Interface for rules that includes an Evaluating method, for example:
  • A Class (rule) that implements the IRule interface:
  • A collection of rules, and an engine to process the rules:

Now, let’s gather our thoughts and take a look at this more in depth!

Step 1

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:

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:

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:

Finally, in the CalculateScore() method, we’ll comment out the current ScoreSingleDie() method, add the rule to a collection, and run the engine:

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:

Give your tests a run – they should all still pass!

 Step 2

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:

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):

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!

Step 3

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:

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:

Now run your tests…we have a failure! 🙁

Test failure after adding a new TripleDiceRule

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.

Step 4

Create a ScoreResult Class

Go ahead and make a new ScoreResult.cs class file, and add the following code:

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:

Then the SingleDieRule:

Next, the TripleDiceRule:

And finally, our rule engine must be updated to process the new information received from the rules:

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.

Step 5

Having the rule evaluator decide which rule is best

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:

Translating that into real code, let’s update our CalculateScore() method:

Now give your tests a run – back to all green (they should be, at least)!

Step 6

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:

Now let’s also update the TripleDiceRule in the CalculateScore() method:

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:

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:

…and your tests should all pass!

Step 7

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:

Create a StraightRule.cs file:

Don’t forget to update your CalculateScore() method to remove the old methods, and add the new rules.

Final Result

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):

Game.cs

References

About the Author

FranksBrain

Frank is an experienced IT director of 15+ years who has been working with computers since the age of 13. While hardware and networking were the base of his career, he also enjoys programming, and is currently focused on .NET Core, Angular, and Blazor. He enjoys constantly learning something new, and helping others do the same.

Leave a Reply

Your email address will not be published.