Monday, September 24, 2012

Rules Engine Patterns - Part 4: Result Compilation

Instead of directly reacting to rule conditions, or directly mutating the model, in this pattern you will record and collect outcomes of rule conditions.  When the rule session is complete, the application will collect the results and do something with them (e.g.: save them to a database or forward them to different services).

Benefits:
  • Keep a history of the rule outcomes without needing to mutate the model objects inserted into the session.
Disadvantages:
  • A lot more scaffolding to employ.  You will need to create a model around the "plausible outcomes" for your rules, as well as, the scaffolding to collect those outcomes and react to the results.
Example:  Rich's Parcel and Post Service needs to route packages based on a number of rules governed by a package's weight and distance from the company's collection center.  If the package is less than 200 lbs., and within 500 miles of the collection center, the package will be delivered locally (dropped in a mail box!).  If the package is less than 200 lbs., but more than 500 miles away, it will be delivered via Air Mail.  Finally, if the package is more than 200 lbs., it will be delivered by train.

The implementation of the routing system will be performed using decorators, which will allow us to easily wrap the original parcel with new functionality (although, we will really only use it to distinguish the class type).

Parcel.java

A simple interface for Parcels in our package handling system.
package com.berico.rc;

public interface Parcel {

 public abstract double getWeight();

 public abstract int getDestinationZipCode();

}

BaseParcel.java

A simple implementation of the Parcel interface.  This class will be used initially for all packages prior to some routing determination being performed by the rules engine.
package com.berico.rc;

public class BaseParcel implements Parcel {

 private double weight = -1;
 
 private int destinationZipCode = -1;

 public BaseParcel(double weight, int destinationZipCode) {
  this.weight = weight;
  this.destinationZipCode = destinationZipCode;
 }

 @Override
 public double getWeight() {
  return weight;
 }

 @Override
 public int getDestinationZipCode() {
  return destinationZipCode;
 }
}

LocalDeliveryParcel.java

A decorator for Parcel, this class represents a package delivered via post office or some other local carrier.
package com.berico.rc;

public class LocalDeliveryParcel implements Parcel {

 private Parcel originalParcel = null;

 public LocalDeliveryParcel(Parcel originalParcel) {

  this.originalParcel = originalParcel;
 }

 public Parcel getOriginalParcel() {
  return originalParcel;
 }

 @Override
 public double getWeight() {
  
  return originalParcel.getWeight();
 }

 @Override
 public int getDestinationZipCode() {
  
  return originalParcel.getDestinationZipCode();
 }
 
 public void routeToPostOffice(){
  
  System.out.println(
   "Dropping package off in a mail box.");
 }
}


TrainParcel.java

A decorator for Parcel, this class represents a package delivered via freight train.
package com.berico.rc;

public class TrainParcel implements Parcel {
 
 private Parcel originalParcel = null;

 public TrainParcel(Parcel originalParcel) {

  this.originalParcel = originalParcel;
 }

 public Parcel getOriginalParcel() {
  return originalParcel;
 }

 @Override
 public double getWeight() {
  
  return originalParcel.getWeight();
 }

 @Override
 public int getDestinationZipCode() {
  
  return originalParcel.getDestinationZipCode();
 }
 
 public void routeToFreightCar(){
  
  System.out.println(
   "Giving package to hobo, destination: Akron, OH.");
 }
}


AirMailParcel.java

A decorator for Parcel, this class represents a package delivered via cargo plane.

package com.berico.rc;

public class AirMailParcel implements Parcel {

 private Parcel originalParcel = null;

 public AirMailParcel(Parcel originalParcel) {

  this.originalParcel = originalParcel;
 }

 public Parcel getOriginalParcel() {
  return originalParcel;
 }

 @Override
 public double getWeight() {
  
  return originalParcel.getWeight();
 }

 @Override
 public int getDestinationZipCode() {
  
  return originalParcel.getDestinationZipCode();
 }
 
 public void routeToPlane(){
  
  System.out.println(
   "Attaching package to underside of biplane.");
 }
}



ResultCompilation.drl

The rules that govern the package routing system.  You will notice the use of a "function" defined within the ruleset to calculate the distance between the call center and a zip code.  This is, of course, a pretend calculation.

package com.berico.rc

// If only it were this simple.
function double distance(int zipcode){
 return zipcode * 0.01;
}

rule "Local Delivery routing"

  when
    parcel : BaseParcel( 
     weight < 200.0, distance(destinationZipCode) < 500) 
  then
    insert( new LocalDeliveryParcel(parcel) );
end

rule "Train routing"

  when
    parcel : BaseParcel( weight >= 200.0) 
  then
    insert( new TrainParcel(parcel) );
end

rule "Air Mail routing"

  when
    parcel : BaseParcel( 
     weight < 200.0, distance(destinationZipCode) > 500) 
  then
    insert( new AirMailParcel(parcel) );
end

ResultCompilationApp.java

A decorator for Parcel, this class represents a package delivered via post office or some other local carrier.

package com.berico.rc;

import java.util.Collection;

import org.drools.runtime.ObjectFilter;

import com.berico.BaseApp;

public class ResultCompilationApp extends BaseApp {

 @Override
 protected String getRuleFile() {
  
  return "ResultCompilation.drl";
 }

 
 public ResultCompilationApp() {
  super();
  
  // Create a bunch of parcels that need
  // to be routed.
  Parcel[] parcels = new Parcel[]{
   new BaseParcel(100, 90210),
   new BaseParcel(200, 90210),
   new BaseParcel(100, 20110),
   new BaseParcel(500, 87234),
   new BaseParcel(1000, 51234)
  };
  
  // Iterate over the parcels...
  for(Parcel parcel : parcels){
   
   // Inserting each parcel into the
   // rule session.
   getSession().insert(parcel);
  }
  
  // Apply all rules against the parcels
  getSession().fireAllRules();
  
  System.out.println("LOCAL DELIVERY PARCELS........");
  
  // Print the parcels that require Local Delivery
  printParcelInfo(LocalDeliveryParcel.class);
  
  System.out.println("TRAIN PARCELS........");
  
  // Print the parcels that require Train delivery
  printParcelInfo(TrainParcel.class);
  
  System.out.println("AIR MAIL PARCELS........");
  
  // Print the parcels that require Air Mail
  printParcelInfo(AirMailParcel.class);
  
  // Kill the session
  getSession().dispose();
 }
 
 /**
  * Simple predicate to get all objects that match the
  * supplied object type.
  */
 public class ByClassTypeFilter implements ObjectFilter {

  private Class<?> targetClass = null;
  
  public ByClassTypeFilter(Class<?> targetClass){
   this.targetClass = targetClass;
  }
  
  @Override
  public boolean accept(Object object) {
   return object.getClass().equals(targetClass);
  }
 }
 
 /**
  * Get the objects of the particular parcel type from
  * the "Working Memory" of Drools and print them weight
  * and destination zip code to the console.
  * @param parcelClass Type of Parcel to retrieve
  */
 protected void printParcelInfo(Class<? extends Parcel> parcelClass){
  
  // Pull the objects from the rule session,
  // by using a custom predicate that looks
  // for a specific Parcel implementation type.
  Collection<Object> oParcels 
   = getSession().getObjects(
    new ByClassTypeFilter(parcelClass));
 
  // Iterate over the matching objects...
  for(Object oParcel : oParcels){
   
   // Cast the object to the interface.
   Parcel parcel = (Parcel)oParcel;
   
   // Print the parcel information
   System.out.println(
    String.format("Weight: %s, Zip Code: %s", 
     parcel.getWeight(), 
     parcel.getDestinationZipCode()));
  }
 }
 
 public static void main(String[] args){
  
  new ResultCompilationApp();
 }
}

On the console, you should see the following message:
LOCAL DELIVERY PARCELS........
Weight: 100.0, Zip Code: 20110
TRAIN PARCELS........
Weight: 500.0, Zip Code: 87234
Weight: 1000.0, Zip Code: 51234
Weight: 200.0, Zip Code: 90210
AIR MAIL PARCELS........
Weight: 100.0, Zip Code: 90210

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.