Using the Rules Pattern to Improve Code Maintainability
Picture this scenario; you have been tasked with writing some code that creates a sales order in the system. You choose to use a standard BAPI provided by SAP. Nice, clean code that is running in production.
Then, a few weeks later, an application consultant informs you that the business needs have changed for one customer only that uses the solution; they need a check first that the material ordered is in stock in a quantity suitable for one delivery – don’t create the order if that is not possible.
So you modify your original solution and add a bit of complexity. Not much, just one ‘IF’ branch and some handling logic for that one customer only. No problem.
The next week a new requirement appears in your email; If the delivering plant is greater that 500 km from the Ship-to party and the order is of a certain type, do not create the order. More checking code goes into your original, elegant solution.
Two weeks later, another check requirement to implement; a month later one more. By now, your code is beginning to ‘smell’ a bit – not your fault, how could you have known these little checks and subsequent implementations were going to add additional complexity to your once clean code, whose only single responsibility originally was to create sales orders from the data provided.
One year down the line of this, and the code has gone from being somewhat inelegant to virtually unmaintainable; the checks have formed a morass of spaghetti code that is hard to read through and understand what all of the business requirements were. Any additional logic may introduce unintended side effects that slip through regression testing, because there are so many paths through the code that testing becomes a nightmare; consultants have moved on, developers have moved on, and understanding the complexity of the code is a tall order indeed.
Not only that, but every time the code is modified, it means that the source code is opened up, edited and transported, adding additional risks that once tested code is inadvertently changed in an unintended manner, with the subsequent post-mortem to discover what actually went wrong after the code has been released to the productive environment.
The Root Cause
What is the root cause of all of this mayhem? In a word, unpredictability. It was impossible to gather all of the requirements at design time, and if working according to agile methodologies this would not even be attempted. Requirements evolve over time, new customers and business conditions come onboard, so even if the requirements analysis had been completely comprehensive at the beginning of the design process, new requirements would invariably crop up.
In any case why leave it to the original code to do all of the checks? After all, it was designed to do one thing: create sales orders. Making it do anything else violates the first principle of SOLID design; the Single responsibility principle, which states that:
“There should never be more than one reason for a class to change.”
What if there was a way to separate all of these code checks out into smaller, independent units of code that could be swapped in and out of use, without recourse to opening up the original code and recompiling it? What if those checks could be re-used, eliminating repeated code? What if some customers needed some checks that others did not.
What if the code could be prevented from the ravages of code rot from the outset?
Enter the Rules Pattern
I am indebted to Steve Smith who is the originator of this design pattern.
The rules pattern was specifically designed to address this scenario:
- Code is becoming excessively complex; additional check logic consistently gets added to the original functionality
- Code with one original responsibility gets tasked wish additional concerns relating to executing additional tasks
- The logic for executing these additional tasks gets intertwined with the original code
My implementation of the rules pattern goes a little further than the original pattern, so I’ll break it down into sections, then see how they all work together at the end. At the core of all of this is a single component conceptually identified as a Rule.
A rule is a fundamental, atomic check for a specific condition or state of a system. In plain English, it’s a check that a certain condition can be fulfilled.
Rules typically need to be set up, i.e. be provided with information about the state of the system, and executed, to evaluate the outcome of the rule.
The basic architecture is shown below.
Let’s have a look at the salient features of the diagram above. We have:
The contract definition for the rules. Contains two methods: PREPARE() and EXECUTE().
PREPARE(): Prepare the rule. Pass in any needed data that is necessary for the EXECUTE method.
Here’s an example of a simple PREPARE() statement:
METHOD zif_cssm_rule~prepare. DATA: r_cssm_dlvrep TYPE REF TO zcl_cssm_dlvrep. * We know this is to recieve a message type ZCL_CSSM_DLVREP, * And we need to access some of it's attributes, so narrow the cast. r_cssm_dlvrep ?= im_message. me->set_sales_order( EXPORTING im_sales_order = r_cssm_dlvrep->order ). me->set_sales_order_item( EXPORTING im_sales_order_item = r_cssm_dlvrep->order_item ). ENDMETHOD.
The cast is due to the fact that I am passing in an object of a known type that needs to be explicitly stated in this example. The important point is that I am just setting a sales order document number and sales order item number in this statement, nothing more.
EXECUTE(): Execute the rule. Note the parameters that the method returns:
EX_STATE returns one of three levels: PASS, FAIL or WARN, referring to the state of the rule after evaluation.
EX_ABORT is an indication that whatever happens with the other rules, setting this to ‘TRUE’ is enough to prevent any further rule checks since whatever happens; it’s a kind of system level ‘all bets are off!’ indicator.
There is a reason for this; in my implementation, if a rule fails, I may wish to carry on processing but perhaps skip an operation (say, creating a sales order, because one already exists); on the other hand, if the method returns EX_ABORT = ‘TRUE‘, it means just give up on processing because whatever happens, the process will not succeed (say, a vendor batch number is passed in that does not match a batch in the system). I could have added another level to EX_STATE, but I find separating the signals out this way clearer to understand in the program.
Here is an example of a simple EXECUTE() statement:
METHOD zif_cssm_rule~execute. * Check that referenced sales order exists DATA: lv_vbak TYPE vbeln_va. ex_state = zif_cssm_rule~c_pass. SELECT SINGLE vbeln FROM vbak INTO lv_vbak WHERE vbeln = sales_order. IF sy-subrc NE 0. ex_state = zif_cssm_rule~c_fail. ex_abort = zif_cssm_rule~c_true. ENDIF. ENDMETHOD.
A couple of points to note:
- Obeying the SOLID Single Responsibility Principle, it should be clear that this rule is doing one thing and one thing only – checking that a sales order exists in the database.
- This code looks almost ridiculously trivial, and that’s exactly how it should be; easy to understand. Imagine 20 rules like this all incorporated into the original source code to create a sales order, along with the conditions to determine if a particular rule should be executed for a particular customer; then the reason for organising the code in this manner perhaps starts to make sense.
Abstract Class ZCL_RULE
Although not strictly needed, since any class can implement the needed interface ZIF_RULE,
the abstract class ZCL_RULE can be used to implement the interface and all other rules can inherit from it; this leads to a neat way to implement new rules without the need to recompile existing code, as I will show in a later blog.
Concrete Classes ZCL_RULE_1…ZCL_RULE_n
These are the classes that implement a particular rule.
The Rule Builder and Rule Builder Factory
The Rule Builder
The Rule Builder does just that – builds the rules to be used.
The purpose of this interface is to put the rules together for use in a specific code location.It contains two methods: BUILD_RULES() and GET_RULES().
BUILD_RULES() creates an instance of each rule that is to be executed, and invokes the method ZIF_RULE~PREPARE() on each rule.
Here’s an implementation of BUILD_RULES():
METHOD zif_orders_rule_builder~build_rules. DATA: r_orders_rule TYPE REF TO zcl_orders_rule. * Check that sales order DOES NOT exist for specified purchase order. CREATE OBJECT r_orders_rule TYPE zcl_orders_rule_1. r_orders_rule->prepare( EXPORTING im_message = im_orders ). APPEND r_orders_rule TO order_check_rules. * Check that there are not multiple sales order for specified purchase order - fatal error. CREATE OBJECT r_orders_rule TYPE zcl_orders_rule_2. r_orders_rule->prepare( EXPORTING im_message = im_orders ). APPEND r_orders_rule TO order_check_rules. ENDMETHOD.
Note here that:
- Adding a rule is just a case of creating an instance of a specific rule type, calling PREPARE() on it and adding it to a table of reference to rules.
- Each rule knows how to prepare itself, so other code does not need to do that for the rule.
GET_RULES() returns the list of rules, ready to execute. This is very straightforward:
METHOD zif_cssm_orders_rule_builder~get_order_rules. ex_order_check_rules = order_check_rules. ENDMETHOD.
The Rule Builder Factory
Responsible for deciding which rule builder to build, based on arbitrarily chosen input parameters; it could be anything needed to discriminate which Rule Builder to fabricate. It only has one method – MAKE_ZCL_RULE_BUILDER(), returning the appropriate Builder.
Here’s an example of the implementing code that uses an EDI partner as a parameter:
METHOD make_zcl_rule_builder. CASE im_partner. WHEN 'KRY'. CREATE OBJECT ret_zcl_cssm_rule_builder TYPE zcl_rule_blder_kry. WHEN 'PODEMO'. CREATE OBJECT ret_zcl_cssm_rule_builder TYPE zcl_rule_blder_podemo. WHEN OTHERS. CREATE OBJECT ret_zcl_cssm_rule_builder TYPE zcl_rule_blder_gen. ENDCASE. ENDMETHOD.
Note the following:
- Any specific partner can have their own specific rule builder implementation, meaning that if a particular customer has specific rules to check, they can be instantiated for that customer only.
- Customers having their own rules builder dramatically reduces code complexity; imagine having one universal rule builder that had to check what rule to implement based on customer and possibly many other parameters.
- Note the WHEN OTHERS statement. This is important; it means that for any customer that does not have any specific rule checks, a generic rule builder is instantiated that contains rules common for all unspecified customers.
The Rules Evaluator
This is the final part of the puzzle; the Rules Evaluator:
The Rules Evaluator is a stand-alone utility class that has one job only, satisfied by the EVALUATE() method – to parse the rules passed into it and return EX_STATE and EX_ABORT. It can be used for any collection of rules in a table of references to rules.
This is the current code for the evaluator:
METHOD zif_evaluator~evaluate. DATA: r_rule TYPE REF TO zif_rule. DATA: lv_state TYPE z_state, lv_state_overall TYPE z_state, lv_abort_overall TYPE boolean. CLEAR ex_state. lv_state_overall = c_pass. lv_abort_overall = '-'. ex_state = c_pass. LOOP AT im_rules INTO r_rule. REFRESH lt_status. r_rule->execute( IMPORTING ex_state = lv_state ex_abort = ex_abort ). * Only pass states can go to warning state IF lv_state_overall = zif_evaluator=>c_pass AND lv_state = zif_evaluator=>c_warn. lv_state_overall = zif_evaluator=>c_warn. ex_state = lv_state_overall. ENDIF. * Any state can go to error state IF lv_state = zif_evaluator=>c_fail. lv_state_overall = zif_evaluator=>c_fail. ex_state = lv_state_overall. ENDIF. * Any one rule raises abort flag, needs to be preserved IF ex_abort = 'X'. lv_abort_overall = 'X'. ENDIF. ENDLOOP. ENDMETHOD.
- Most of the logic deals with aggregation of the results of the collective evaluation of the individual rules.
- A warning state can be issued only if the current overall state of evaluation if the preceding evaluated rules is a pass state.
- Any state can go to an error state, but it is irreversible; no subsequent rule evaluation can change that state.
- An abort condition is preserved to pass back with the method parameters.
Putting it all Together
Having explained the components in detail, it will probably add clarity to see it in action.
Below, I have some code that is responsible for processing an inbound IDOC message. Part of that functionality is to create a sales order as part of a multi-step process of:
- Create Sales Order
- Create Delivery with reference to Sales Order
- Allocate batches to Delivery
- Post Goods Issue
The Sales Order is to be created provided that:
- One has not already been created (due to successful processing of this stage of IDOC handling in a previous handling of the IDOC)
- The customer is not in credit block
- Several other checks
So how does it get executed? At run time, the code that handles the IDOC builds the rules and then evaluates them as follows. First, the Rule Builder Factory is invoked, in order to build the correct set of rules for a particular customer:
* Set the rule builder CALL METHOD me->set_rule_builder( EXPORTING im_partner = im_partner ).
Which invokes the factory to create the correct rule builder for the particular partner:
method ZIF_ORDERS~SET_RULE_BUILDER. zcl_orders_rule_bld_fact=>make_zcl_rule_builder( EXPORTING im_partner = im_partner RECEIVING ret_zcl_rule_builder = rule_builder ). endmethod.
Then the rules are prepared and returned:
** Set up the sales order creation check rules CALL METHOD me->build_sales_order_rules( EXPORTING im_orders = me IMPORTING ex_order_check_rules = lt_order_check_rules ).
Which is implemented thus:
METHOD zif_orders~build_sales_order_rules. DATA: lt_order_check_rules TYPE z_orders_rules_t. rule_builder->build_order_rules( EXPORTING im_orders = im_orders ). rule_builder->get_order_rules( IMPORTING ex_order_check_rules = lt_order_check_rules ). ex_order_check_rules = lt_order_check_rules. ENDMETHOD.
Finally the rules are evaluated:
** Evaluate sales order creation check rules CALL METHOD me->evaluate_sales_order_rules( EXPORTING im_order_check_rules = lt_order_check_rules IMPORTING ex_state = lv_state ).
Here is the code in the method:
METHOD zif_orders~evaluate_sales_order_rules. DATA: lv_abort TYPE boolean. CALL METHOD zcl_evaluator=>zif_evaluator~evaluate EXPORTING im_rules = im_order_check_rules IMPORTING ex_state = ex_state ex_abort = lv_abort. IF lv_abort = 'X'. CALL METHOD me->abort. ENDIF. ENDMETHOD.
Overall, in the original code responsible for creating the sales order, disruption of the original code has been reduced to a few lines of code and comments, with a minimal addition to cyclomatic complexity:
* Set up the sales order creation check rules CALL METHOD me->build_sales_order_rules( EXPORTING im_orders = me IMPORTING ex_order_check_rules = lt_order_check_rules ). * Evaluate sales order creation check rules CALL METHOD me->evaluate_sales_order_rules( EXPORTING im_order_check_rules = lt_order_check_rules IMPORTING ex_state = lv_state ex_abort = abort_state ). * Terminate if abort state. CHECK abort_state NE c_true. * Create sales order iff creation check rules do not fail. IF lv_state NE zif_cssm_evaluator=>c_fail. CALL METHOD me->create_sales_order. ENDIF.
Note from the above:
- No complex logic in the program directly
- All decision making logic has been farmed out to the rules
- Further changes to the rules will not impact the complexity of the original code above
- The Rules Pattern allows a clear way of separating check logic into independent, isolated units.
- Code complexity is reduced; the original investment in the implementation of the pattern is offset over the lifetime of the code by the significant reduction in complexity.
- Comprehensive regression tests are easier to implement, since the scope of any changes are limited to a particular rule.
- Code maintainability is enhanced by virtue of each rule being isolated from other rules.