The Rules Pattern in Business Central - AL Language

The Rules Pattern is a Design Pattern to simplify the complex business logic which is based on multiple if-else statements. Using this Design Pattern each if-else branch in the business logic is segregated into a separate rules and also the processing logic is separated from the rules.


Class Diagram

Demo Scenario

To understand the Rules Design Pattern better, the following scenarios are being used:

  • If the Customer's "Customer Price Group" is 'GOLD' and the "Business Posting Group" is 'DOM' then 15% discount will be given, else if the "Business Posting Group" is other than 'DOM' 25% discount will be given.

  • If the Customer's "Customer Price Group" is 'SILVER' and the "Business Posting Group" is 'DOM' then 10% discount will be given, else if the "Business Posting Group" is other than 'DOM' 15% discount will be given.


Rule Interface (DiscountRule.Interface.al)

This is the implementation of Rule Interface for Discount Calculation. The method "Process" is going to return the Discount Percent based on conditions defined in the "CanProcess" method. If the "CanProcess" method returns false, it means there is no discount applicable for the given customer.

interface DiscountRule
{
    procedure CanProcess(CustomerNo: Code[20]): Boolean;
    procedure Process(CustomerNo: Code[20]): Decimal;
}


Evaluator (DiscountEvaluator.Codeunit.al)

The following logic calculates maximum possible discount. This evaluates the incoming Rule and updates the discount, if the previous discount is less than the current discount.

codeunit 50106 DiscountEvaluator
{
    procedure Evaluate(DiscountRule: Interface DiscountRule; CustomerNo: Code[20]; var Discount: Decimal)
    var
        NewDiscount: Decimal;
    begin
        if not DiscountRule.CanProcess(CustomerNo) then
            exit;

        NewDiscount := DiscountRule.Process(CustomerNo);
        if NewDiscount > Discount then
            Discount := NewDiscount;
    end;
}

Rules

End number of Discount Rules can be implemented, in this example two discount rules are being used. First Rule is for Gold Customers, and the second one for the Silver Customers.


Rule One (GoldCustomerDiscountRule.Codeunit.al)

codeunit 50110 GoldCustomerDiscountRule implements DiscountRule
{
    procedure CanProcess(CustomerNo: Code[20]): Boolean;
    var
        Customer: Record Customer;
    begin
        Customer.Get(CustomerNo);
        if Customer."Customer Price Group" = 'GOLD' then
            exit(true);
    end;

    procedure Process(CustomerNo: Code[20]): Decimal;
    var
        Customer: Record Customer;
    begin
        Customer.Get(CustomerNo);
        case Customer."Gen. Bus. Posting Group" of
            'DOM':
                exit(0.15);
            else
                exit(0.25);
        end;
    end;
}

Rule Two (SilverCustomerDiscountRule.Codeunit.al)

codeunit 50111 SilverCustomerDiscountRule implements DiscountRule
{
    procedure CanProcess(CustomerNo: Code[20]): Boolean;
    var
        Customer: Record Customer;
    begin
        Customer.Get(CustomerNo);
        if Customer."Customer Price Group" = 'SILVER' then
            exit(true);
    end;

    procedure Process(CustomerNo: Code[20]): Decimal;
    var
        Customer: Record Customer;
    begin
        Customer.Get(CustomerNo);
        case Customer."Gen. Bus. Posting Group" of
            'DOM':
                exit(0.1);
            else
                exit(0.15);
        end;
    end;
}


Rule Engine / Processor (DiscountCalculator.Codeunit.al)

This is the main codeunit to calculate the Discount. This follows Open Closed Principle which means it is closed for modification and open for extension.


To modify the discount logic, there is no need to change anything in this codeunit. To modify any condition or the logic in the Discount Rule, the appropriate Discount Rule codeunit has to be modified. To add a new Discount Rule, a new codeunit can be created that implements the Rule Interface and it has to be attached to the Processor (in OnExecute subscriber).

codeunit 50105 DiscountCalculator
{
    procedure Execute(CustomerNo: Code[20]): Decimal
    var
        DiscountEvaluator: Codeunit DiscountEvaluator;
        Discount: Decimal;
    begin
        OnExecute(DiscountEvaluator, CustomerNo, Discount);
        exit(Discount);
    end;

    [BusinessEvent(false)]
    local procedure OnExecute(DiscountEvaluator: Codeunit DiscountEvaluator; CustomerNo: Code[20]; var Discount: Decimal)
    begin
    end;
}

Rules Collection (DiscountRuleSubscriber.Codeunit.al)

This codeunit will attach the available Discount Rules to the Processor (DiscountCalculator).

codeunit 50112 DiscountRuleSubscriber
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::DiscountCalculator, 'OnExecute', '', false, false)]
    local procedure OnExecute(DiscountEvaluator: Codeunit DiscountEvaluator; CustomerNo: Code[20]; var Discount: Decimal)
    var
        GoldCustomerDiscountRule: Codeunit GoldCustomerDiscountRule;
        SilverCustomerDiscountRule: Codeunit SilverCustomerDiscountRule;
    begin
        DiscountEvaluator.Evaluate(GoldCustomerDiscountRule, CustomerNo, Discount);
        DiscountEvaluator.Evaluate(SilverCustomerDiscountRule, CustomerNo, Discount);
    end;
}

Conclusion

Design Patterns represents the best practices used by the experienced developers to solve general problems. In my opinion, the objective and the solution are more important than the implementation itself. Design patterns can be implemented in different ways depending on the Language features and the capability of the developer. And the most important thing is design patterns should not be implemented forcefully when it is not necessary.


The Rules Pattern solves complexity of the business problem by dividing it into multiple classes / codeunits so that the code becomes readable and extendable for future enhancements.

Happy Coding!!!


Source Code can be downloaded from GitHub


Credits to Steve Smith who is the originator of this design pattern.


#MSDyn365 #MSDyn365BC #BusinessCentral #DynamicsNAV #DesignPatterns