Monday 10 July 2017

Using the Rules Pattern to Improve Code Maintainability

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

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.

The 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.

SAP ABAP Tutorials and Materials, SAP ABAP Certifications

Let’s have a look at the salient features of the diagram above. We have:

Interface ZIF_RULE


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

SAP ABAP Tutorials and Materials, SAP ABAP Certifications

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:

SAP ABAP Tutorials and Materials, SAP ABAP Certifications

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.

Salient features:
  • 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

No comments:

Post a Comment