📜 ⬆️ ⬇️

Multiple Branches and Rules Template

Hello, dear habrachiteli. In this article, I would like to share knowledge about one small and simple, but useful template, about which usually is not written in books (perhaps because it is a special case of the “Team” template). This is the Rules Pattern. Probably for many he will be very familiar, but it will be interesting to someone to meet him.




Essence of the question

Very often, when developing complex logic, a tree of nested ifs arises, which may look, for example, like this:
Horrible code
public double CalculateSomething(Condition condition) { //   if(condition.First...) ... //   if(condition.Second...) ... //    if(condition.AnotherFirst...) { //      if(condition.First) ... else... } //    if(condition.AnotherSecond...) { //      if(condition.Second) ... else... } //    if(condition.YetAnotherFirst) { //... if(condition.AnotherFirst && condition.Second) ... else { ... } } // O_o } 


Familiar? So what are the problems here?
')
Problem 1: Growing cyclomatic complexity. To put it simply, cyclomatic complexity is the depth of nesting of if-s and cycles taking into account logical operators. Code analysis tools allow you to evaluate this parameter for all parts of the code. It is considered that the parameter of cyclomatic complexity for a separate piece of code should not exceed 10. Of this problem, the following grows.

Problem 2: Adding new logic. With the passage of time and the addition of new conditions, it becomes difficult to understand exactly where to add new logic and how.

Problem 3: Code duplication. If the condition tree is ramified, then sometimes it is impossible to get rid of the situation when the same code is present in several branches.

This is where the “Rules” template comes to the rescue. Its structure is very simple:
Structure uml diagram


Here, the Evaluator class contains a collection of IRule interface implementations. The valuator fulfills the rules and decides which rule to use to get the result. To understand how this works and looks like in code, consider a small example in C #.

Example. Dice (like Tali)

Rules of the game:

The player rolls 5 dice at the same time, and depending on their combination gets a certain number of points.
Combinations may be as follows:
1 XXXX - 100 points
5 XXXX - 50 points
1 1 1 XX - 1000 points
2 2 2 XX - 200 points
3 3 3 XX - 300 points
4 4 4 XX - 400 points
5 5 5 XX - 500 points
6 6 6 XX - 600 points

Examples of combinations:
[1,1,1,5,1] - 1150 points
[2,3,4,6,2] - 0 points
[3,4,5,3,3] - 350 points

Of course, they can all be different, and there may be much more, but more on that later.

Do it once! No templates.

Let's try to describe the logic of the game without using the “Rules” template, as we would write in an informatics class in the 8th grade (naturally, without providing our bad code with comments - who needs them!)
Bad, Bad Game Class
 public class Game { public int Score(int[] roles) { int score = 0; for(int i=1; i<7; i++) { int count = CountDiceWithValue(roles, i); count = ScoreSetOfN(count, GetSetSize(i), SetSetScore(i), ref score); score += count * GetSingleDieScore(i); } return score } private int GetSingleDieScore(int val) { if(val==1) return 100; if(val==5) return 50; return 0; } private int GetSetScore(int val) { if(val==1) return 1000; return val*100; } private int GetSetSize(int val) { return 3; } private int ScoreSetOfN(int count, int setSize, int setScore, ref int score) { if(count>=setSize) { score += setScore; return count - 3; } return count; } private int CountDiceWithValue(int[] roles, int val) { int count = 0; foreach (int r in roles) { if (r == val) count++; } return count; } } 


Do two! Adding rules? Unit Tests.

It seems that 50 lines of code is very small. But what will happen if the rules of the game are changed and added?
For example, we add rules for different combinations of cubes:

1 1 1 1 X - 2000
1 1 1 1 1 - 4000
1 2 3 4 5 - 8000
2 3 4 5 6 - 8000
AABBX - 4000
and so on.

In this case, the code runs the risk of becoming very confusing. To avoid this, we will rewrite the code using the "Rules" template.
(Here, I would also have to say that before refactoring, it is necessary to cover all cases with modular tests, rumble about their importance and need for refactoring code)

Do three! Apply the template "Rules"

1. We define the IRule interface with the Eval method, which is needed to estimate the number of points for a certain set of cubes.
IRule.cs
 public interface IRule { ScoreResult Eval(int[] dice); } 


2. Create a RuleSet class that will define a set of rules, logic to add a rule, and logic to select the best rule to apply to this set of cubes:
RuleSet.cs
 public class RuleSet { //  private List<IRule> _rules = new List<IRule>(); //  public void Add(IRule rule) { _rules.Add(rule); } //   - ,      public IRule BestRule(int[] dice) { ScoreResult bestResult = new ScoreResult(); foreach(var rule in _rules) { var result = rule.Eval(dice); if(result.Score > bestResult.Score) { bestResult = result; } return bestResult.RuleUsed; } } } 


3. Of course, a small class assistant
ScoreResult.cs
 public class ScoreResult { //   public int Score {get;set;} //    (       ) public int[] DiceUsed {get;set;} //   ,  ,     (  BestRule) public IRule RuleUsed {get;set;} } 


4. And define the rules themselves.
ConcreteRules.cs
 //    public class SingleDieRule : IRule { private readonly int _value; private readonly int _score; public SingleDieRule(int dieValue, int score) { _dieValue = dieValue, _score = score } //   -      public ScoreResult Eval(int[] dice) { //- var result = new ScoreResult(); //    (   ) -    result.DiceUsed = dice.Where(d=>d == dieValue).ToArray(); //   result.Score = result.DiceUsed.Count() * _score; //  -       result.RuleUsed = this; return result; } } //      


5. In our case, the Evaluator class from the scheme will be the Game class, it will contain almost nothing but the logic of adding rules and the scoring logic.
Game.cs - Evaluator
 public class Game { private readonly RuleSet _ruleSet = new RuleSet(); public Game(bool useAllRules) { //  _ruleSet.Add(new SingleDieRule(1,100)); _ruleSet.Add(new SingleDieRule(5,50)); _ruleSet.Add(new TripleDieRule(1,1000)); for(int i=2; i<7; i++) { _ruleSet.Add(new TripleDieRule(i, i*100)); } //  if(useAllRules) { _ruleSet.Add(new FourOfADieRule(1,2000)); _ruleSet.Add(new SetOfADieRule(5,1,4000)); _ruleSet.Add(new StraightRule(8000)); _ruleSet.Add(new TwoPairsRule(6000)); for(int i=2; i<7; i++) { _ruleSet.Add(new FourOfADieRule(i,i*200)); _ruleSet.Add(new SetOfADieRule(i,i*400)); //... } } } //       public void AddScoringRule(IRule rule) { _ruleSet.Add(rule); } //  public int Score(int[] dice) { int score = 0; var remainingDice = new List<int>(dice); var bestRule = _ruleSet.BestRule(remainingDice.ToArray()); //             while(bestRule!=null) { var result = bestRule.Eval(remainingDice.ToArray()); foreach(var die in result.DiceUsed) { remainingDice.Remove(die); } score+=result.Score; bestRule = _ruleSet.BestRule(remainingDice.ToArray()); } return score; } } 


Hooray! Problem solved! Now each class is doing what it is supposed to; the cyclomatic complexity does not grow,
and new rules are added easily and simply. Rule selection is now done using the RuleSet class, which contains a set of rules, and adding rules and scoring - the Game class.

What you need to remember?

When designing a program containing logic based on rules, it is useful to keep in mind the following questions:
- Should the rules be read-only in relation to the system so as not to change its state?
- Should there be dependencies between the rules? Should I pay attention to the order of execution of the rules, in the case where one rule may require the result of the work of another rule for the work.
- Should the order of execution of the rules be strictly defined?
- Should there be priorities in the implementation of the rules?
- Should end-users be allowed to edit rules?
and many others.

A couple of words about Business Rules Engines systems

The concept of Business Rules Engines is very close to the idea of ​​the “Rules” template - these are systems that allow
define rule systems for business logic. Usually they have some kind of graphical interface and allow users to define rules and hierarchies of rules that can be stored in a database or file system. In particular, this functionality has the Workflow Foundation from Microsoft.

Summary

1) Use the "rules" template when you need to get rid of the complexity of conditions and branching
2) We place the logic of each rule and its effects in our classes.
3) Separate the selection and processing of rules into a separate class - Evaluator
4) We know that there are ready-made "engine" solutions for business logic

Thank you very much for your attention, I hope my creative processing of this educational material will help someone.
* The source of inspiration for this article was the “Pattern Pattern” lesson from the “Design Patterns” course on pluralsight.com by Steve Smith.

Source: https://habr.com/ru/post/179069/


All Articles