Skip to content

Geneology Rules – Decoupling the Criteria

August 29, 2013

This blog is an excerpt from the new Drools Training Course that I am currently writing for Skills421 (www.skills421.com)

::

For anyone interested in following the parts before this check out

The Project So Far

We have now got a working set of rules that will identify potential Father Son relationships in a Genealogy Tree.

The rules compare two people and make a number of checks including

  • are the surnames the same
  • are the surnames both “Walker”
  • is the Father 18 years older than the son
  • is the Father married
  • was the Father married 9 months before the son was born

Whilst this is fine and is generating matches – we may wish to dynamically change some of these conditions, particularly if we have  a GUI front end.  Certainly, we do not want to have to re-write the rules if we want to look at Smiths and not Walkers.

New Facts

For this reason, let’s insert the last name, age difference and married period as facts into to Working Memory so that the rules can consider those facts in their determinations instead of the hard-coded values we have so far provided.

We could create a single Configuration class that has  member variables lastname, agedifference and marriedperiod, but what if we wanted to add a new check later on?  In that case we would need to modify the Configuration class and re-deploy the compiled code.

Drools does provide for Declaring objects to be used later on but I am not a big fan of using this to solve this problem.

Instead let’s create a SimpleFact object that can be inserted into working memory.  This will have a key of type String and a value of type Object.

Let’s also create a new Period object that can store values for days, months, and years.

Finally let’s insert some SimpleFacts into Working Memory as follows:

  • lastName, String – e.g. “Walker”
  • ageDifference, Period – e.g. 0 days, 0 months, 18 years
  • marriedPeriod, Period – e.g. 0 days, 9 months, 0 years

So here we go…

SimpleFact and Period

Let’s start with Period

package com.skills421.training.geneology.model;

public class Period
{
	private int days = 0;
	private int months = 0;
	private int years = 0;

	public Period()
	{

	}

	public Period(int days, int months, int years)
	{
		this.days = days;
		this.months = months;
		this.years = years;
	}

	public int getDays()
	{
		return days;
	}

	public void setDays(int days)
	{
		this.days = days;
	}

	public int getMonths()
	{
		return months;
	}

	public void setMonths(int months)
	{
		this.months = months;
	}

	public int getYears()
	{
		return years;
	}

	public void setYears(int years)
	{
		this.years = years;
	}

	@Override
	public int hashCode()
	{
		final int prime = 31;
		int result = 1;
		result = prime * result + days;
		result = prime * result + months;
		result = prime * result + years;
		return result;
	}

	@Override
	public boolean equals(Object obj)
	{
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Period other = (Period) obj;
		if (days != other.days)
			return false;
		if (months != other.months)
			return false;
		if (years != other.years)
			return false;
		return true;
	}

}

Period.java

Here you can see that we have three simple members: days, months, and years.

We use Eclipse to generate getters and setters for this class, and finally we use Eclipse to generate hashCode() and equals().

Our Period class is now ready to use.

Now let’s move on to SimpleFact.

package com.skills421.training.geneology.model;

import java.util.Date;

public class SimpleFact
{
	private String key;
	private Object value;

	public SimpleFact()
	{

	}

	public SimpleFact(String key, Object value)
	{
		this.key = key;
		this.value = value;
	}

	public void setKey(String key)
	{
		this.key = key;
	}

	public String getKey()
	{
		return this.key;
	}

	public void setValue(Object value)
	{
		this.value = value;
	}

	public Object getValue()
	{
		return this.value;
	}

	public Number getNumberValue()
	{
		if(value instanceof Number)
		{
			return (Number) value;
		}

		return null;
	}

	public Date getDateValue()
	{
		if(value instanceof Date)
		{
			return (Date) value;
		}

		return null;
	}

	public String getStringValue()
	{
		if(value instanceof String)
		{
			return (String) value;
		}

		return null;
	}

	public Period getPeriodValue()
	{
		if(value instanceof Period)
		{
			return (Period) value;
		}

		return null;
	}

	@SuppressWarnings("unchecked")
	public <T> T getValueOfType(Class<T> type)
	{
		if(value.getClass().equals(type))
		{
			return (T) value;
		}

		return null;
	}
}

SimpleFact.java

SimpleFact has two members: key of type String and value of type object.

To get values from SimpleFact I have created 5 methods as follows:

  • getValue which returns an Object
  • getNumberValue which returns a Number or null
  • getDateValue which returns a Date or null
  • getStringValue which returns a String or null
  • getPeriodValue which returns a Period or null
  • getValueOfType which returns a value of specified type, or null

Whilst we’re at it, let’s add a few more constructors to our Person class to make life a little easier:

package com.skills421.training.geneology.model;

import java.io.Serializable;
import java.text.ParseException;

import com.skills421.training.extend.util.ExtendedDate;

public class Person implements Serializable
{
	/**
	 *
	 */
	private static final long serialVersionUID = 8214659086096344769L;

	private ExtendedDate birthdate;
	private ExtendedDate christeningdate;
	private ExtendedDate marriagedate;
	private ExtendedDate deathdate;

	private String firstname;
	private String lastname;
	private String fatherFirstname;
	private String motherFirstname;

	private String birthLocation;

	public Person()
	{

	}

	public Person(String firstname, String lastname)
	{
		this.firstname = firstname;
		this.lastname = lastname;
	}

	public Person(String firstname, String lastname, String born, String christened, String married, String died)
	throws ParseException
	{
		this(firstname,lastname);

		this.birthdate = born!=null ? new ExtendedDate(born) : null;
		this.christeningdate = christened!=null ? new ExtendedDate(christened) : null;
		this.marriagedate = married!=null ? new ExtendedDate(married) : null;
		this.deathdate = died!=null ? new ExtendedDate(died) : null;
	}

	public ExtendedDate getBirthdate()
	{
		return birthdate;
	}

	public void setBirthdate(ExtendedDate birthdate)
	{
		this.birthdate = birthdate;
	}

	public ExtendedDate getChristeningdate()
	{
		return christeningdate;
	}

	public void setChristeningdate(ExtendedDate christeningdate)
	{
		this.christeningdate = christeningdate;
	}

	public ExtendedDate getMarriagedate()
	{
		return marriagedate;
	}

	public void setMarriagedate(ExtendedDate marriagedate)
	{
		this.marriagedate = marriagedate;
	}

	public ExtendedDate getDeathdate()
	{
		return deathdate;
	}

	public void setDeathdate(ExtendedDate deathdate)
	{
		this.deathdate = deathdate;
	}

	public String getFirstname()
	{
		return firstname;
	}

	public void setFirstname(String firstname)
	{
		this.firstname = firstname;
	}

	public String getLastname()
	{
		return lastname;
	}

	public void setLastname(String lastname)
	{
		this.lastname = lastname;
	}

	public String getFatherFirstname()
	{
		return fatherFirstname;
	}

	public void setFatherFirstname(String fatherFirstname)
	{
		this.fatherFirstname = fatherFirstname;
	}

	public String getMotherFirstname()
	{
		return motherFirstname;
	}

	public void setMotherFirstname(String motherFirstname)
	{
		this.motherFirstname = motherFirstname;
	}

	public String getBirthLocation()
	{
		return birthLocation;
	}

	public void setBirthLocation(String birthLocation)
	{
		this.birthLocation = birthLocation;
	}
}

Person.java

These changes provide the following constructors:

  • Person()
  • Person(firstName, lastName)
  • Person(firstname, lastname, born, christened, married, died)
    all the dates are passed as Strings and converted within the constructor

Changing the Rules

Now we have finished modifying our fact objects, let’s move on to changing the rules.

We want our rules to handle 3 SimpleFacts:

  • lastName, String – e.g. “Walker”
  • ageDifference, Period – e.g. 0 days, 0 months, 18 years
  • marriedPeriod, Period – e.g. 0 days, 9 months, 0 years

Starting with Ancestry1.drl we change the rule as follows:

package com.skills421.training.geneology

import com.skills421.training.geneology.model.Person;
import com.skills421.training.geneology.model.SimpleFact;

dialect "mvel"

rule "Person Found"
    when
    	SimpleFact(key=="lastName", $lastName : stringValue);
        $person : Person(lastname==$lastName);
    then
        System.out.println( "Found Person: "+$person.firstname+" "+$person.lastname+" b:"+$person.birthdate);
end

Ancestry1.drl

Looking at the rule, we can see that the Matcher in the Rule Engine first looks for a SimpleFact with a key of ‘lastName’.  If it finds this it then obtains the value from the SimpleFact as a String.

Next, the Matcher looks for  a Person with a lastname that matches the value in the SimpleFact.

Where a match is found, the rule prints the details of the matched person.

::

Moving on to our next rule, let’s make the changes to Ancestry1.1

package com.skills421.training.geneology

import com.skills421.training.geneology.model.Person;
import com.skills421.training.geneology.model.SimpleFact;

dialect "mvel"

rule "Order People by Date of Birth"
    when
    	SimpleFact(key=="lastName", $lastName : stringValue);
        $person : Person(lastname==$lastName,  $dob : birthdate);
        not Person(lastname==$lastName, birthdate < $dob)
    then
        System.out.println( "Found Person: "+$person.firstname+" "+$person.lastname+" b:"+$person.birthdate);
        retract($person);
end

Ancestry1.1.drl

This rule is the same as Ancestry1.drl with one additional line in the Criteria to ensure that there is not a Person in Working Memory with a last name that matches the person we have already found, and who was born before this person.

Remember that for this rule to re-evaluate until all the Facts have been processed, we much retract the first born fact that we identify after we have printed it.

::

Moving on to our next rule, Ancestry2 starts looking for a possible Father – Son match by comparing the dates of birth.

package com.skills421.training.geneology

import com.skills421.training.geneology.model.Person;
import com.skills421.training.extend.util.ExtendedDate;
import com.skills421.training.geneology.model.SimpleFact;

dialect "mvel"

rule "Could be Father"
	when
    	SimpleFact(key=="lastName", $name : stringValue);
    	SimpleFact(key=="ageDifference", $agedif : periodValue);

        $father : Person(lastname==$name, $fborn : birthdate);
        $son : Person(lastname==$name, $sborn : birthdate > ((ExtendedDate) $fborn).add($agedif.days,$agedif.months,$agedif.years))
    then
        System.out.println( $father.firstname+" (b: "+$fborn+") could be father of "+$son.firstname+" (b: "+$sborn+")");
end

Ancestry2.drl

In this example we load 2 SimpleFacts, the lastName and the ageDifference between the father and the son.

The rest of the Criteria section remains the same with the one change that we use the ageDifference Period values when adding them to the Father’s birth date.

::

Finally, let’s change Ancestry3 which also stipulates that the father must have married before the son was born.

java training, it training, objective-c training, ios training, drools training, spring training, java, spring, spring mvc, spring webflow, core spring, java enterprise, ejb, jsf, java server faces, primefaces, jpa, java persistence architecture, drools, rule engines

This is a great training company with excellent trainers. Highly recommended. IT Training Courses throughout the UK.

com.skills421.training.geneology.rules

com.skills421.training.geneology.rules

package com.skills421.training.geneology

import com.skills421.training.geneology.model.Person;
import com.skills421.training.extend.util.ExtendedDate;
import com.skills421.training.geneology.model.SimpleFact;
import com.skills421.training.geneology.model.Period;

dialect "mvel"

rule "Could be Married Father"
when
    SimpleFact(key=="lastName", $name : stringValue);
    SimpleFact(key=="ageDifference", $ageDif : periodValue != null);
    SimpleFact(key=="marriedPeriod", $marriedPeriod : periodValue != null);

        $father : Person(lastname=="Walker", $fborn : birthdate, $married : marriagedate != null);
        $son : Person(lastname=="Walker",
          $sborn : birthdate > ((ExtendedDate) $fborn).add($ageDif.days,$ageDif.months,$ageDif.years),
                      birthdate > ((ExtendedDate) $married).add(0,$marriedPeriod.months,$marriedPeriod.years) );
    then
        System.out.println("MARRIED "+$father.firstname+" (b: "+$fborn+", m:"+$married+") could be father of "+$son.firstname+" ("+$sborn+")");
end

Ancestry3.drl

In this rule we load the additional SimpleFact called marriedPeriod and use this to ensure that our father was married before the son was born.

Modifying the Test Case

Now, with our Facts and our Rules modified all that remains is for us to modify the tests and re-run them.

package com.skills421.training.geneology.rules;

import org.junit.BeforeClass;
import org.junit.Test;

import com.skills421.training.geneology.model.Period;
import com.skills421.training.geneology.model.Person;
import com.skills421.training.geneology.model.SimpleFact;

public class TestDroolsRunner
{
	private static RuleRunner ruleRunner;

	private static Person p1;
	private static Person p2;
	private static Person p3;
	private static Person p4;
	private static Person p5;
	private static Person p6;
	private static Person p7;

	@BeforeClass
	public static void setupFacts()
	{
		try
		{
			ruleRunner = new RuleRunner();

			p1 = new Person("John", "Walker", "18/02/1803", null, null, null);
			p2 = new Person("John", "Walker", "14/11/1825", null, null, null);
			p3 = new Person("John", "Walker", "28/12/1810", null, null, null);
			p4 = new Person("John", "Walker", "07/01/1800", null, "14/02/1824", null);

			p5 = new Person("Betty", "Smith", "21/06/1804", null, null, null);
			p6 = new Person("John", "Smith", "19/04/1806", null, null, null);
			p7 = new Person("Jean", "Smith", "14/02/1801", null, "14/02/1824", null);
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
	}

	@Test
	public void testFactsInserted()
	{
		System.out.println("*** testFactsInserted ***");
		ruleRunner.buildKnowledgeBaseWithRuleFiles("Ancestry1.drl");

		System.out.println("*** Looking for Walker ***");
		SimpleFact lastname = new SimpleFact("lastName","Walker");

		ruleRunner.runWithFacts(p1,p2,p3,p4,p5,p6,p7,lastname);
		ruleRunner.dispose();

		System.out.println("*** Looking for Smith ***");
		lastname.setValue("Smith");

		ruleRunner.runWithFacts(p1,p2,p3,p4,p5,p6,p7,lastname);

		ruleRunner.dispose();
	}

	@Test
	public void testFactsInsertedPrintInOrder()
	{
		System.out.println("*** testFactsInsertedPrintInOrder ***");
		ruleRunner.buildKnowledgeBaseWithRuleFiles("Ancestry1.1.drl");

		System.out.println("*** Looking for Walker ***");
		SimpleFact lastname = new SimpleFact("lastName","Walker");

		ruleRunner.runWithFacts(p1,p2,p3,p4,p5,p6,p7,lastname);
		ruleRunner.dispose();

		System.out.println("*** Looking for Smith ***");
		lastname.setValue("Smith");

		ruleRunner.runWithFacts(p1,p2,p3,p4,p5,p6,p7,lastname);

		ruleRunner.dispose();
	}

	@Test
	public void testFatherOlderThanSon()
	{
		System.out.println("*** testFatherOlderThanSon ***");
		ruleRunner.buildKnowledgeBaseWithRuleFiles("Ancestry2.drl");

		System.out.println("*** Looking for Walker 18 years diff ***");
		SimpleFact lastname = new SimpleFact("lastName","Walker");
		Period ageDifference = new Period(0,0,18);
		SimpleFact ageDifferenceSF = new SimpleFact("ageDifference",ageDifference);

		ruleRunner.runWithFacts(p1,p2,p3,p4,p5,p6,p7,lastname,ageDifferenceSF);
		ruleRunner.dispose();

		//

		System.out.println("*** Looking for Walker 25 years diff ***");
		ageDifference.setYears(25);

		ruleRunner.runWithFacts(p1,p2,p3,p4,p5,p6,p7,lastname,ageDifferenceSF);
		ruleRunner.dispose();
	}

	@Test
	public void testFatherMarriedAndOlderThanSon()
	{
		System.out.println("*** testFatherMarriedAndOlderThanSon ***");
		ruleRunner.buildKnowledgeBaseWithRuleFiles("Ancestry3.drl");

		System.out.println("*** Looking for Walker 18 years diff, married 9 months ***");
		SimpleFact lastname = new SimpleFact("lastName","Walker");
		Period ageDifference = new Period(0,0,18);
		SimpleFact ageDifferenceSF = new SimpleFact("ageDifference",ageDifference);
		Period marriedPeriod = new Period(0,9,0);
		SimpleFact marriedPeriodSF = new SimpleFact("marriedPeriod",marriedPeriod);

		ruleRunner.runWithFacts(p1,p2,p3,p4,p5,p6,p7,lastname,ageDifferenceSF,marriedPeriodSF);
		ruleRunner.dispose();

		System.out.println("*** Looking for Walker 18 years diff, married 1 year ***");
		marriedPeriod.setMonths(0);
		marriedPeriod.setYears(1);

		ruleRunner.runWithFacts(p1,p2,p3,p4,p5,p6,p7,lastname,ageDifferenceSF,marriedPeriodSF);
		ruleRunner.dispose();

		System.out.println("*** Looking for Walker 18 years diff, married 2 years ***");
		marriedPeriod.setMonths(0);
		marriedPeriod.setYears(2);

		ruleRunner.runWithFacts(p1,p2,p3,p4,p5,p6,p7,lastname,ageDifferenceSF,marriedPeriodSF);
		ruleRunner.dispose();

	}

}

TestRulesRunner.java

A few things to note here about the modified TestRulesRunner:

  • I’ve added 3 new facts for the Smith family.
  • The Test Cases now insert our SimpleFact objects for comparison

Running the Rules

So, now we have everything in place, let’s give the TestRulesRunner  a whirl and check the Console for output.

Here’s what you should get:

*** testFactsInserted ***

*** Looking for Walker ***
Found Person: John Walker b:18/02/1803
Found Person: John Walker b:14/11/1825
Found Person: John Walker b:28/12/1810
Found Person: John Walker b:07/01/1800

*** Looking for Smith ***
Found Person: Betty Smith b:21/06/1804
Found Person: John Smith b:19/04/1806
Found Person: Jean Smith b:14/02/1801

*** testFactsInsertedPrintInOrder ***

*** Looking for Walker ***
Found Person: John Walker b:07/01/1800
Found Person: John Walker b:18/02/1803
Found Person: John Walker b:28/12/1810
Found Person: John Walker b:14/11/1825

*** Looking for Smith ***
Found Person: Jean Smith b:14/02/1801
Found Person: Betty Smith b:21/06/1804
Found Person: John Smith b:19/04/1806

*** testFatherOlderThanSon ***

*** Looking for Walker 18 years diff ***
John (b: 07/01/1800) could be father of John (b: 14/11/1825)
John (b: 18/02/1803) could be father of John (b: 14/11/1825)

*** Looking for Walker 25 years diff ***
John (b: 07/01/1800) could be father of John (b: 14/11/1825)

*** testFatherMarriedAndOlderThanSon ***

*** Looking for Walker 18 years diff, married 9 months ***
MARRIED John (b: 07/01/1800, m:14/02/1824) could be father of John (14/11/1825)

*** Looking for Walker 18 years diff, married 1 year ***
MARRIED John (b: 07/01/1800, m:14/02/1824) could be father of John (14/11/1825)

*** Looking for Walker 18 years diff, married 2 years ***

Console Output

Advertisements

From → Tutorials

One Comment
  1. Reblogged this on Skills421.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: