Howardism Musings from my Awakening Dementia
My collected thoughts flamed by hubris
Home PageSend Comment

Rules Engine with Groovy

The MVC pattern, while ubiquitous, is just not enough. The purpose of MVC is to separate concerns and responsibilities, that is, allow graphic designers with UI skills to worry about the View and DBAs with database skills to concern themselves with the model, etc. It also makes it easy to apply specific technologies to those

<prices>
    <half>
        <size dimensions="7.5x11">
            <num people="1">$70.00</num1>
            <num people="2">$90.00</num1>
        </size>
        <size dimensions="11x15">
            <num people="1">$90.00</num1>
            <num people="2">$125.00</num1>
            <num people="3">$160.00</num1>
            &hellip;

We could then, once we have collected all of the aspects, figure out the price by using an XPath expression:

"/prices/" + proportion + "/size[@dimensions='" + size + "']/num['" + numpeople + "']/text()"

Of course, you may find editing XML, instantiating the XML parsing libraries and working with XPath expressions particularly icky and you may want to use some other technology (like Groovy's NodeBuilder or at least create an Object class hierarchy and instantiate it with Spring), but the concept of separating out the "business rules" that are highly volatile and most likely to change will save you lots of work down the road.

But what if the data that changes is a bit more complicated, and it isn't so much the data that changes as much as the processes associated with it. Sure you could use the many "Rules Engines" that are available, but these guys are extremely heavy weight and complicated… more complicated than the data I typically encounter.

But I got to thinking about a middle road. Where we could have some rules without the rules engine, and I started to play…

Let's begin this example with a "Person" model, implemented with the following class:

class Person {
   String name
   int age
}

Yeah, we'll keep it simple for this example. Now, we want to figure out the admission price for a person, and this is based on the person's age. We will encode the logic in such a way that this Groovy file could actually be sent and edited by the domain experts, that is, the business guys. Don't worry, we can still review their code before it gets checked in:

def engine = new RulesEngine()

engine.rules( [
   baby:   { person -> person.age  <  3 },
   child:  { person -> person.age >=  3 && person.age < 10 },
   student:{ person -> person.age >= 10 && person.age < 21 },
   adult:  { person -> person.age >= 21 && person.age < 65 },
   senior: { person -> person.age >= 65 },

   // We could have written the rules using the default pointer, like:
   //   child:  { it.age >=  3 && it.age < 10 }
   // But I don't like using gender neutral pronouns in this case.

   admission: [
      isBaby:    { 0.00 },
      isChild:   { 3.00 },
      isStudent: { 7.00 },
      isSenior:  { 7.00 },
      isAdult:   { 9.00 }
   ]
] );

The first thing you'll notice is that this code would be understandable by anyone who understands the business.

The second thing you'll notice is that our "Rules Engine" accepts maps of a rule associated with a Closure. Except for the admission, which isn't a rule, but really a query evaluation. This will accept some query references to other rules and a closure to evaluate and return if the rule references evaluate to true.

I really like Groovy's map definition, as it works quite well here, however, if I were to really persue it, I would like to have the commas optional and maybe even the colons. However, this would mean a grammar and all sorts of nonsense that would make me think about using a real rules engine.

But this query evaluation business would allow me to have code like:

def bob = new Person ( [ name:'Bob Barker', age:83 ] )
def price = engine.evaluate ( 'admission', bob )

And the price variable would then contain 7.00. This particular example would be easy to code if (and only if) we don't have recursive rules… you know, rules that refer to rules.

We just need to have a method, rules, which split a map into a collection of rules and queries, and then another method, evaluate, to actually walk around and call the closures:

class RulesEngine {

    def rules = [:]
    def evals = [:]

    // Separate the map into the rules and the query evaluations:

    public rules ( Map rules ) {
      rules.each { rule, defn ->
        // If the rule is map, then it is stored in the evals 
        if ( defn instanceof Map )
            evals.put(rule, defn)
        else
            this.rules.put(rule, defn)
      }
    }

    // Given a 'phrase', search the query evaluation map for a
    // matching rule &hellip;

    Object evaluate ( String phrase, Object object ) {
      def results = null
      evals[phrase].each { arule, evalcode ->
        def callrule = unTitleCase(arule - 'is')
        def code = rules[callrule]
        if ( code && code.call(object) )
          results = evalcode.call(object)
      }
      return results
    }

    // Given: CamelCased this returns camelCased
    String unTitleCase ( String str ) {
      str[0].toLowerCase() + str[1..str.size()-1 ]
    }
}

This really whets the appetite for something more. For instance, we could have our RulesEngine class be an actual Builder, and we'd then trade in our commas and colons for parenthesis. But the biggest advantage would be that our rules could then ordered, as in:

engine.rules() {
   baby()    { person -> person.age < 3 }
   child()   { person -> person.age < 10 }
   student() { person -> person.age < 21 }
   senior()  { person -> person.age > 64 }
   adult()   { true }
   &hellip;

Now before you get too excited, this is just a prototype of some ideas and is not, and should not be a full-blown rules engine… and is certainly not JSR-94 compliant. But this idea looks quite intriquing.

I suppose that if you are going the route of including a scripting language like Groovy, you might as well grab one of the many fine Prolog engines that execute in the JVM… and that is an possibility. I'm just looking to start a discussion right now.

Tell others about this article:
Click here to submit this page to Stumble It