[edit: 12 May, 2009]
Whoopee! At least one person has read this posting:
Thanks Bob for great article on drools/grails. It is very useful.
regards
zys
I've wanted to get onto this for a while now, so here goes.
The "State of the Art" application is generally a real dog's breakfast. Sure, we all create our nice MVC-based applications using the Spring framework, or JBoss SEAM or even good-old Struts and we feel pretty good about it but let's
face it: the typical MVC application is actually a pretty nasty thing that is not MVC at all.
Here's one aetiology…I'm sure you will recognise it.
Andy the Architect comes in and specs out a nicely structured work of art. On paper, it is clean and tidy with clearly separated layers and distint responsibilities. Then a few cross-cutting concerns muddy the waters a bit but no problem, things are still lookin' good. Then Joe the Developer
comes along and starts building. Except in the interests of performance it becomes vital that some processing is moved into stored procedures. Well, OK, there is still a
logical mapping in the architecture. Joe shuffles off the project and Freda arrives. Freda wasn't privy to those early days when the architecture was decided upon and when the
rationalisation (sense 2) surrounding the decision to use stored procedures was fresh in everyone's mind and so decides to implement a nice, clean separate module for her
work. Time progresses, the business changes and expands and suddently Bill is brought in to help out Freda. Of course both Freda and Bill can't work on the same module so it is split. Bill is an old war-hardened JSP developer but is not
really au-fait with the system's backend and so another, JSP-oriented and (subtly?) different approach is brought to bear.
And so it goes.
This effect is seen most keenly with respect to the mechanisms of data access/searching/filtering: Joe is happy to spend a day working how to use n-level nested selects with carefully decided-upon projections, etc. to ensure that what
he pulls from the database is precisely and only what he needs whereas Bill-more cognizant of approaching deadlines, perhaps-just grabs everything and iterates across the result set, rejecting 90% but getting what he needs. The code is
quick and easy to write, however ;-)
The effect is also a major issue when one considers the business rules that underly most, if not all, applications. It is depressingly common to see the aetiology considered earlier resulting in rules split across an application's
database, webservice, controller and view (it is also depressingly common to see these layers implemented variously in different technologies, but that is a rant for another time).
Recognising the drawbacks of traditional MVC some have adopted the idea of MVP (Model View
Presenter) as a way of saying "make sure that your view layer is only about the UI." The idea of anaemic domain as anti-pattern still rules the roost and is frequently
used to justify the messiness that I am trying to highlight here.
In the J2EE world, it used to be said that "stored procedures should migrate to Session Beans" and there was a lot of truth in this. After all, to quote
Michael Juntao Yuan, author of Seam Framework: Experience the Evolution of Java EE, Second Edition:
In almost all enterprise applications, the database is the primary bottleneck, and the least scalable tier of the runtime environment. People from a PHP/Ruby environment will try to tell you that so-called "shared nothing"
architectures scale well. While that may be literally true, I don't know of many interesting multi-user applications which can be implemented with no sharing of resources between different nodes of the cluster. What these silly people
are really thinking of is a "share nothing except for the database" architecture. Of course, sharing the database is the primary problem with scaling a multi-user application-so the claim that this architecture is highly scalable is
absurd, and tells you a lot about the kind of applications that these folks spend most of their time working on.
Almost anything we can possibly do to share the database less often is worth doing.
Sadly however, trying to persuade someone fixated on the 'fact' that "stored procedures are faster" is usually a lost cause; one can talk about clean design and easier maintenance, fewer
technology and cultural impedance mismatches, the benefits
of integrated security and monitoring, the ease of clustering and so on but one's arguments always seem to fall short, as measured by the yardstick of a mental stopwatch. Such is life…
What if one could centralise all the messy rules that are typically spread throughout a system into one clearly-separated subsystem. This is what JBoss' Drools (now JBoss Rules) aims
to enable and-ranting aside-is the actual focus of this post.
Welcome to Bob's Pokie machine!
Before we start, please note that I do not like these damn things at all. They are a scourge on the social groups least able to resist them, IMHO. For once I agree with Russell Crowe!
A pokie is essentially simple but has a number of rules regarding when payment is to be made to the punter and when the house should take the money and run.
Bob's Pokie is written using Grails. It is very,very simple!
The design goes something like: "A Pokie has a spindle with 3 reels. After each spin, the value of each reel is obtained and the overall results evaluated to determine whether the spin has produced a win or a losing result." From this,
we get a single simple GSP and a number of simple objects: Spindle, Reel, EvaluatorResult, EvaluatorService and PokieController.
It is probably worth taking a quick tour around the source, starting with the GSP:
<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<head><title>Bob' Pokie</title></head>
<body>
<h1>Bob's Pokie</h1>
<p>Guaranteed to eat all your money, "but don't you worry about that"</p>
<g:form controller = "pokie" action = "play">
<g:select name = "leftReel" from = "${1..5}" value = "${session.spindle?.spindle.leftReel.value}" multiple="multiple" size="5" />
<g:select name = "middleReel" from = "${1..5}" value = "${session.spindle?.spindle.middleReel.value}" multiple="multiple" size="5" />
<g:select name = "rightReel" from = "${1..5}" value = "${session.spindle?.spindle.rightReel.value}" multiple="multiple" size="5" />
<br />
<g:submitButton name="spin" value="Spin" />
</g:form>
<g:if test="${session.evaluatorResult == pokie.EvaluatorResult.INACTIVE}">Give it a spin...</g:if>
<g:elseif test="${session.evaluatorResult == pokie.EvaluatorResult.LOSE}">You Lose :-(</g:elseif>
<g:elseif test="${session.evaluatorResult == pokie.EvaluatorResult.SMALLWIN}">You Win. Just.</g:elseif>
<g:elseif test="${session.evaluatorResult == pokie.EvaluatorResult.BIGWIN}">You Win...BIGTIME!</g:elseif>
<g:else>Oh Dear! Something (${session.evaluatorResult}) went wrong here<br /><g:link action="index">Start Over</g:link>
<script type="text/javascript">
document.getElementById('spin').disabled = true
</script></g:else>
</body>
</html>
The associated controller is, in true Grails style, very simple:
class PokieController {
def evaluatorService
def index = {session.spindle = new pokie.Spindle(); session.spindle.reset(); redirect(action: show) }
def show = { session.evaluatorResult = evaluatorService.evaluate(session.spindle) }
def play = {session.spindle.activate(); redirect(action: show) }
}
A Spindle is pretty much simply a collection of Reels:
package pokie;
public class Spindle {
private final spindle = [leftReel: new Reel(), middleReel: new Reel(), rightReel: new Reel()]
private initialised = false;
def reset = { initialised = false; spindle.each { k, v -> v.reset() } }
def activate = { initialised = true; spindle.each { k, v -> v.activate() } }
def getSpindle = { Collections.unmodifiableList(spindle) }
}
A Reel is:
package pokie;
public class Reel {
final rand = new Random()
int value
private rint = { rand.nextInt(5) + 1 }
def reset = { value = 3 }
def activate = { value = rint() }
}
For completeness, here is EvaluatorResult, a standard enum:
package pokie
public enum EvaluatorResult {
INACTIVE, LOSE, BIGWIN, SMALLWIN, OTHER
}
The first attempt at making EvaluatorService will ignore Drools and do everything "by hand." Here goes:
import pokie.EvaluatorResult
public class EvaluatorService {
static transactional = false
// a SMALLWIN is: all 3 reels show the same value
// A BIGWIN is: all 3 reels have the value 3
// everything else is a LOSE
static evaluate = {spindle ->
if (!spindle.initialised)
return EvaluatorResult.INACTIVE;
def firstVal = spindle.leftReel.value
if (spindle.find {k, v -> v.value != firstVal} != null)
return EvaluatorResult.LOSE
return firstVal == 3 ? EvaluatorResult.BIGWIN : EvaluatorResult.SMALLWIN
}
}
Nothing special here; the code is small, straightforward and easy to understand, right?
Yes and no.
Looking through the code, I see a couple of 'if' statements and a bit of searching through a collection. In fact there is absolutely nothing to tell me that here is the key set of business rules. Unfortunately enough,
it is the comment that gives the only real clue. Truly this is not good and we need to try harder.
Consider too, that it may be neccessary to get a Product Owner or domain specialist to review the rules (something that may actually be required for a Pokie in Queensland). My guess is that not too many domain specialists are going to understand
even simple code like that in EvaluatorService.
Drools helps by providing a general-purpose rules engine, based on some pretty clever ideas, which allows for rules to be specified in ways other
than pure code. Drools allows for rules to be written in XML, the "near english" DRL or even Spreadsheet-based Decision Tables. There is also an extensive toolset
supporting Drools with editors, debuggers, etc..
Take a look a the DRL corresponding to the evaluation that we have just used:
package pokie;
rule "Other"
salience -1000
dialect "mvel"
lock-on-active
activation-group "pokie"
when
e : Evaluation(initialised == true)
then
modify ( e ) { result = EvaluatorResult.OTHER };
end
rule "Pokie is Initialising"
salience -1
dialect "mvel"
lock-on-active true
activation-group "pokie"
when
e : Evaluation(initialised == false)
then
modify ( e ) { result = EvaluatorResult.INACTIVE };
end
rule "Big Win"
salience 2
dialect "mvel"
lock-on-active
activation-group "pokie"
when
e : Evaluation(initialised == true,
(leftReelValue == middleReelValue) && (leftReelValue == rightReelValue) && (leftReelValue == 3))
then
#System.out.println( "Big Win" );
modify ( e ) { result = EvaluatorResult.BIGWIN };
end
rule "Small Win"
salience 1
dialect "mvel"
lock-on-active
activation-group "pokie"
when
e : Evaluation(initialised == true,
(leftReelValue == middleReelValue) && (leftReelValue == rightReelValue))
then
modify ( e ) { result = EvaluatorResult.SMALLWIN };
end
rule "Losing"
salience 0
dialect "mvel"
lock-on-active
activation-group "pokie"
when
e : Evaluation(initialised == true)
then
modify ( e ) { result = EvaluatorResult.LOSE };
end
Much more verbose, of course but also probably clearer, especially once one becomes familiar with the ideas of salience (basically a rule's weight or priority), activation-group (only one rule in an activation-group is
allowed to become active at any time) and lock-on-active (don't reevaluate the ruleset after a modify has executed).
With the exception of the way that data is packaged up into an Evaluation instance for the ruleset to use, the code that handles rule evaluation is pretty much boilerplate:
import org.drools.KnowledgeBase
import org.drools.KnowledgeBaseFactory
import org.drools.builder.KnowledgeBuilder
import org.drools.builder.KnowledgeBuilderFactory
import org.drools.builder.ResourceType
import org.drools.definition.KnowledgePackage
import org.drools.io.ResourceFactory
import org.drools.runtime.StatefulKnowledgeSession
import pokie.Evaluation
public class EvaluatorService {
static final RULES = 'pokie.drl'
static transactional = false
def evaluate = {spindle ->
def kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder();
// this will parse and compile in one step
kbuilder.add(ResourceFactory.newClassPathResource(RULES,
EvaluatorService.class), ResourceType.DRL);
// Check the builder for errors
if (kbuilder.hasErrors()) {
def errs = kbuilder.getErrors().toString()
def msg = "Unable to compile '${RULES}': ${errs}"
log.error (msg);
throw new RuntimeException(msg);
}
// get the compiled packages (which are serializable)
def pkgs = kbuilder.getKnowledgePackages();
// add the packages to a knowledgebase (deploy the knowledge packages).
def kbase = KnowledgeBaseFactory.newKnowledgeBase();
kbase.addKnowledgePackages(pkgs);
def ksession = kbase.newStatefulKnowledgeSession();
def e = new Evaluation(leftReelValue: spindle.spindle.leftReel.value,
middleReelValue: spindle.spindle.middleReel.value, rightReelValue: spindle.spindle.rightReel.value, initialised: spindle.initialised)
ksession.insert(e);
ksession.fireAllRules();
ksession.dispose();
e.getResult()
}
}
That's it! Plop a few jars on the classpath and we're up and running. Very clean.
So what does this buy us? Those obsessed with SLOC or 'efficiency' would be quick to say "nothing." IMHO, this may well be short-sighted and probably quite wrong.
For one thing, none of the rest of the application outside of the EvaluatorService is 'infected' by the need for rule handling at all. This is A Good Thing.
Another benefit is that this gets us to a situation where adding rules is easy. So far the rules defining the winning combinations are few and simple: all 3 reels must have the same value, with a 'bonus' if that value is 3:
In a fit of unexpected generosity, let's make it easier to win by adding a few new winning patterns:
With Drools, very little extra work is needed. Only a slight mostly cosmetic modification of the GSP and the addition of a couple of new values in the EvaluatorResult enum are required to support the addition of a few more
rules to the ruleset, viz:
...
rule "MEDIUM Win 0 > 1 > 2; ASC"
salience 1
dialect "mvel"
lock-on-active
activation-group "pokie"
when
e : Evaluation(initialised == true,
(leftReelValue == (middleReelValue + 1)) && (middleReelValue == (rightReelValue + 1)))
then
modify ( e ) { result = EvaluatorResult.MEDIUMWIN_ASC };
end
rule "MEDIUM Win 0 < 1 < 2; DSC"
salience 1
dialect "mvel"
lock-on-active
activation-group "pokie"
when
e : Evaluation(initialised == true,
(leftReelValue == (middleReelValue - 1)) && (middleReelValue == (rightReelValue - 1)))
then
modify ( e ) { result = EvaluatorResult.MEDIUMWIN_DSC };
end
...
These changes have added almost nothing to the complexity of the application but have added some significant new features.
If you take the time to consider what the 'old' hand-coded evaluator will be starting to look like (a few more 'if's to get one's head around, with a lot nastier cyclomatic complexity), you will start to get an appreciation of the value of the Drools approach. If you push this example a little further and consider that a real pokie
would have rules for dealing with everything from dealing with maintenance needs and hardware faults through determining an response to a network outage to dealing with customer loyalty cards and you will see that even a simple system can
get complex very quickly; the original simple evaluate() method has ballooned out into a module…
Now, I am the first to admit that this is a very simple example and not at all a good use-case for a rules engine like Drools: it is far too simple and the problem doesn't really have a multitude of cross-cutting rules and business
illogic that would make Drools a better fit. It has made a nice playpen for understanding and experimenting with Drools however, and I hope that the
fundamentals have come through: how Drools has kept the application clean and tidy and centralised all the messiness of rule processing into one place. A major benefit of this has been to make the rules themselves explicit, easily
changeable and amenable to review by an appropriate tame domain expert.
Of course, if you really feel the need, you can be really clever ;-)
One of the more interesting uses for Drools is that it is used to provide the underlying security infrastructure for Seam 2.0.
The Grails/IntelliJ project for this posting is available (requires Grails 1.1).