Friday, April 29, 2011

Drools as a replacement for the 'Composite Strategy' design pattern.

When facing the need to implement complex business rules over the classical object-oriented approach we always fall down to one of the following solutions:

Spaghetti code:
 if (bill.getAmmount() > 100) {
//apply the best calculation for the store
if (customer.getAge() > 65) {
//apply the senior citizen discount
bill.setAmmount(bill.getAmmount() - 10);
if (customer.isPremium()) {
//apply 10% discount
}
} else {
if (customer.isPremium()) {
//apply 10% discount
}
}
}
Can you guess the business rules out of that code? What about adding a new rule?

The Composite Strategy design pattern:
 DiscountStrategy ds = buildStrategy(customer, bill);
ds.applyDiscounts();
In this last case we would need to create five classes and one interface, one per business rule, one for the composite strategy (to decide in which order is best to apply the strategies) and one strategy factory (to decide which strategies apply). Thats a lot of classes!! And also it would be really hard for any person to understand what were the business rules in the first place.

The bottom line is, we have three business rules:
  • Only bills with value higher than $100 can have discounts.
  • Senior citizens have $10 discount on their purchase.
  • Premium customers have 10% discount on their purchase.
For the cases where two discount policies can be applied we have two situations: what is best for the store and what is best for the customer, and all of this highly affects the readability and maintainability of the code, and also the addition of one simple new rule can make things worse!

The drools approach:
Drools helps us defining complex business logic introducing a new way to face problems: Logical programming. We can define all of our business rules in an elegant fashion:
 package com.juancavallotti
global Bill bill;
import function com.juancavallotti.DiscountLogic.applyDiscount;
rule "Discounts are only for bill over 100 dollars"
salience 20
when
$customer : Customer( )
$bill : Bill( ammount <= 100 )
then
System.out.println("Low value bill");
retract($customer);
retract($bill);
end
rule "Senior Citizen Discount"
salience 10
no-loop
when
Customer( age > 65 )
then
System.out.println("Senior Citizen Discount");
bill.setAmmount(bill.getAmmount() - 10);
end
rule "Premium Customer Discount"
salience 0
no-loop
when
Customer( premium == true )
then
System.out.println("Premium Customer Discount");
bill.setAmmount(applyDiscount(bill.getAmmount(), 10));
end
In the file (called discount.drl) we define our business rules in a very simple format. Now if the business requirements change, we only need to add more rules to the file in a declarative way and the rules engine takes care of how those rules will be applied on our objects.

Also we gave priority to our rules with the "salience" rule property, the greater the salience the more priority the rule has.

I've added the "no-loop" rule property so the rules don't get applied more than once.

Finally the discount service would look as simple as this:
 public class DiscountLogic {
private StatefulKnowledgeSession kbSession;
public DiscountLogic(StatefulKnowledgeSession kbSession) {
this.kbSession = kbSession;
}
public Bill applyDiscountPolicy(Customer customer, Bill bill) {
kbSession.insert(customer);
kbSession.setGlobal("bill", bill);
kbSession.insert(bill);
kbSession.fireAllRules();
return bill;
}
public static double applyDiscount(double ammount, double discountPercent) {
return ammount * (1 - discountPercent / 100f);
}
}
In order to inject the StatefulKnowledgeSession instance we need to add some boilerplate code that will bootstrap our knowledge base and compile the rules and so. You may want to download the sample code and play with it.

To learn more about drools, please refer to the drools documentation. (We are currently having fun with the drools-expert module).

Have Fun!