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>
…
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 …
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 }
…
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: