What is the first method that comes to mind when we want to add a new requirement to our code that implements a certain business logic? It is usually to open a condition block and write our code in it. This approach which does not follow the Open-Closed Principle (OCP), reduces the readability of the code over time and increases development and testing costs. It becomes very difficult to add new requirements as well as remove existing ones.
In this article, we will be comparing a sample application of the Rule Design Pattern, which is one of the methods that minimizes the above disadvantages and ensures adherence to OCP, with the application coded with the traditional method.
Example: Discount Calculation
Suppose we have a discount calculation method that implements the following business rules.
Ø Student citizens should receive an additional 20% discount.
Ø Married citizens should receive an additional 25% discount.
Ø Disaster victims should receive a net 50% discount. (No limit controls should be applied).
Ø Limit checks should be made at the end of the transactions. (First the rate and then the amount should be checked.)
o The maximum discount rate can be 40%.
o The maximum discount amount can be 10,000.
Let's imagine that over time it is necessary to make changes to existing business rules, to add new business rules.
Ø Citizenship checks should be removed from student discounts.
Ø Married citizens -up to a maximum of 5 children- should receive an additional 2% discount for each child.
Traditional Method
When new changes are applied to our method coded with traditional methods, the codes that are removed and added are given below. Since we are going through a simple example, it may not seem too inconvenient to make these changes. However, in methods with more complex business rules, the aforementioned difficulties will be very noticeable.
RulesEngine Library
Before realizing the above example with the Rule Design Pattern, let's examine the library I wrote to make it easier to implement this pattern. In order not to extend the article too much, I will not include some codes here, but if you want to examine it, you can access the codes of the library and related examples on github. The class diagram below shows the basic working principle of the library. To explain briefly, rules implementing the IRule interface are found and these rules are executed in the appropriate order.
The files contained in the library are as follows.
- IRule: It is the basic interface that each rule must implement. - IRuleRequest, IRuleResponse: These are the interfaces that must be implemented by the classes that will hold the input or output values. - RuleAttribute: It is an attribute that allows to set the execution order (ExecutionOrder) and the parent rule (ParentRule) of the rule. - RuleHelper: A helper class containing methods to find the rules to be executed. - RuleExecutor: It is the class that undertakes the conducting of the orchestra. It contains methods to ensure that the relevant rules are carried out under appropriate conditions. Let's examine RuleHelper and RuleExecutor classes in more detail to understand the general logic.
The GetRules method finds concrete types that implement or inherit the TRule rule type in the relevant assemblies and returns the objects of these types in the appropriate order.
GetConcretesWithAttribute and HasParameterlessConstructor methods are special extension methods written for the library. If a rule is assigned a ParentRule via RuleAttribute, it is considered as a child rule, otherwise it is considered as a main rule. GetMainRules method finds the main rules, GetChildRulesOf method finds the child rules of the given rule.
The ExecuteRules method executes the rules given as parameters in order. After the relevant rule and its child rules are executed, the next rule is passed. While the rules are executed, the CanStopRulesExecution property in the response object is checked. If the value is true, rule execution is terminated. CanStopRulesExecution property is defined in the IRuleResponse interface. The Execute method has multiple overload methods. Basically what it does is to find the main rules in the relevant assemblies and pass them to the ExecuteRules method. Rule Pattern Method In the discount calculation example there are 5 requirements for the first version. Suppose we realize them separately in the following rule classes.
- VictimDiscountRule
- StudentDiscountRule
- MarriedDiscountRule
- RateLimitDiscountRule
- AmountLimitDiscountRule Once the rules are defined, the only thing that needs to be done in the method that calculates the discount is to run the method that will execute these rules.
Now let's take a closer look at the rule classes that have been changed or added to implement the changes in version
ChildDiscountRule
As seen above, StudentDiscountRule has been modified. In order to apply discounts to children, a new rule class called ChildDiscountRule has been defined. This rule is a child rule of the MarriedDiscountRule rule. At this point, I would like to point out that the actual method that calculates the discount (See Figure 5) has not been changed. When the Rule Design Pattern is applied, any requested changes to business rules affect the rule classes, not the main method. It is enough to create a new rule class for a new requirement and delete the relevant rule class to remove an existing requirement. Summary We have achieved a much more understandable and manageable structure with the Rule Design Pattern. We can understand which requirement is applied just by looking at the name of the rule class. When we can divide the requirements into rule classes in a good way, we can also get advantages such as maintenance and testing can be much easier, and a logging structure to be established within the rule classes makes the application much more detailed. Even if not always, I think that using this pattern in appropriate places should be in the back of our minds. Thank you for reading this far. References
- https://github.com/furkanisitan/rules-engine - Open Closed principle and Rule engine design pattern (Access Date: 29.04.2024)
- Design Patterns: Learn More about the Rules Engine Pattern (Access Date: 29.04.2024)