0
votes

I'm struggling a bit with a concept in Drools, can anyone clarify if I'm looking at this problem the wrong way? The solution works but it seems like the wrong approach, as I'll explain at the end.

I have a Country model object containing id, continent, area; where area is the economic area for example EU or US; I have enums for CountryID, EconomicAreaId, ContinentID; I have a Request object containing origin:Country, destination:Country, srucharge:Int; I have two Drools rules (Actually expressed in xls, but I'll list the drl here):

<package and imports are correct>
rule "UK"
    when
        $c:Country($c.getId() in (CountryID.UNITED_KINGDOM))
    then
        $c.setArea(EconomicAreaID.EU);
        $c.setContinent(ContinentID.EUROPE);
        /* update($c); // I tried this too :) //
end

rule "US"
    when
        $c:Country($c.getId() in (CountryID.UNITED_STATES))
    then
        $c.setArea(EconomicAreaID.US);
        $c.setContinent(ContinentID.N_AMERICA);
end

rule "EU to EU"
    when
        $rr:ChargeRequest($rr.getOrigin.getArea() == EconomicAreaID.EU, $rr.getDestination.getArea() == EconomicAreaID.EU)
    then
        $rr.setCharge(1);
end

rule "US to US"
    when
        $rr:ChargeRequest($rr.getOrigin.getArea() == EconomicAreaID.US, $rr.getDestination.getArea() == EconomicAreaID.US)
    then
        $rr.setCharge(2);
end

rule "EU to US"
    when
        $rr:ChargeRequest($rr.getOrigin.getArea() == EconomicAreaID.EU, $rr.getDestination.getArea() == EconomicAreaID.US)
    then
        $rr.setCharge(3);
end

rule "US to EU"
    when
        $rr:ChargeRequest($rr.getOrigin.getArea() == EconomicAreaID.US, $rr.getDestination.getArea() == EconomicAreaID.EU)
    then
        $rr.setCharge(4);
end

First I want to insert two country objects, origin and destination, and have Drools automatically assign the continent and economic area to their attributes;

Then I want Drools to calculate the surcharge, simplified here as an int.

In order to do this I have to add both country objects and fire all rules to populate the country objects, THEN add the request object and fire all rules AGAIN to get the surcharge.

KieSession kSession = kieContainer.newKieSession();
ChargeRequest rr = new ChargeRequest(new Country("UNITED_STATES"), new Country("UNITED_KINGDOM"));

kSession.insert(rr.getOrigin()); // add both country objects
kSession.insert(rr.getDestination());
kSession.fireAllRules(); // LINE 6 : fire rules to calculate the economic areas and continents of the country objects

kSession.insert(rr); // add the ChargeRequest object
kSession.fireAllRules(); // fire the rules to calculate the charge

return rr.toString(); // ChargeRequest(origin=Country(id=UNITED_STATES, continent=N_AMERICA, area=US), destination=Country(id=UNITED_KINGDOM, continent=EUROPE, area=EU), charge=4)

If I leave out line 6 then the charge is not calculated as 4 because Drools performs the charge rules on the underpopulated country objects. I thought Drools had the concept of evaluating all the rules in the correct order so it would "know" to apply rules to the countries before using them as input to the charge rules.

This is only a simplified example I'm playing with so it seems it shouldn't be this complicated, so have I got the conceptual approach wrong?

1

1 Answers

2
votes

Oh, Drools knows how and when to evaluate your rules, sure it does... The only thing is that you have to let it know when something changes in your model. The setters you are invoking in the right-hand side of your rules are never notified to Drools, so it doesn't know about the changes you have made.

There is a special keyword in Drools called modify to let the engine know when something is changed in your model.

The rules that set the area and continent should look like this (depending on the Drools version you are doing, you may start experiencing infinite-loops when you use modify. I've rewritten the left-hand side of your rules to avoid them):

rule "UK"
when
    $c:Country(
      area == null,
      continent == null,
      id in (CountryID.UNITED_KINGDOM)
    )
then
    modify($c){
      setArea(EconomicAreaID.EU),
      setContinent(ContinentID.EUROPE)
    };
end

Now, the second problem is that, in the rules that are setting the charge, the Country is nested inside your ChargeRequest objects. Drools will not automatically understand that when you modify a Country it has to reevaluate the rules that are related to ChargeRequests. You have to make this explicit:

rule "EU to EU"
when
    $rr:ChargeRequest(
      $origin: origin, 
      $destination: destination
    )
    Country(
      this == $origin,
      area == EconomicAreaID.EU
    )
    Country(
      this == $destination,
      area == EconomicAreaID.EU
    )
then
    $rr.setCharge(1);
end

As you can see, Drools is intelligent, but you have to help him a bit ;)


A better approach when dealing with this kind of parameterized values according to a fixed set of conditions is to model the condition itself as a fact:

1.- Create a class to model the constraints and results:

public class RequestCharge{
  private EconomicAreaID origin;
  private EconomicAreaID destination;
  private double charge;

  //getters and setters
}

2.- Insert all the constraints into your session:

kSession.insert(new RequestCharge(EconomicAreaID.EU, EconomicAreaID.EU, 1.0);
kSession.insert(new RequestCharge(EconomicAreaID.US, EconomicAreaID.US, 2.0);
kSession.insert(new RequestCharge(EconomicAreaID.EU, EconomicAreaID.US, 3.0);
kSession.insert(new RequestCharge(EconomicAreaID.US, EconomicAreaID.EU, 4.0);

3.- Have a single rule to deal with all requests (you may want to add an extra one to deal with request that don't match any of the configured RequestCharges):

rule "Apply Charge"
when
    $rr:ChargeRequest(
      $origin: origin, 
      $destination: destination
    )
    $o: Country(  //You still need these Country patterns to react to the changes in the first rule
      this == $origin
    )
    $d: Country(
      this == $destination
    )
    RequestCharge(
      origin == $o,
      destination == $d,
      $charge: charge
    )
then
    $rr.setCharge($charge);
end

Hope it helps,