for (BusRoute route: busRoutes){ if (route.isMatching(report)){ for(MessageFilter filter: messageFilters){ if (filter instanceof DelayOnBusRouteMessageFilter){ DelayOnBusRouteMessageFilter delayFilter = (DelayOnBusRouteMessageFilter)filter; if (delayFilter.getBusRoute().equals(route)){ if (route.isApproachingRoutePoint(report, delayFilter)){ if (delayFilter.getDelayApproachingThreshold() < threshold || threshold == 0){ threshold = delayFilter.getDelayApproachingThreshold(); deltaThreshold = delayFilter.getDeltaThreshold(); .... else.... ... else ...What's wrong with this code? Being overly complicated it is difficult to test and maintain and without doubt it will probably contain multiple errors, i.e. it is very difficult to verify that this piece of code indeed implements the specified requirements of what this part of the application is supposed to do. Trying to refactor it to bring down the cyclomatic complexity (McCabe index - counts separate execution paths) didn't help much. I was looking for other ways to improve code quality when I remembered using a rule engine to implement business rules for a proof of concept some years back. Rule engines are good when you can write declarative rules and want to externalize business rules or have a requirement that the rules should be dynamically modifiable at runtime. Working with a rule engine requires a different way of thinking compared to OO or even functional programming. Some understanding of the rule engine basics is necessary before we go on.
Rule engine basics
Most rule engines are based on the Rete algorithm, which is a matching algorithm in modern forward-chaining inferencing rules engines. Without going into details, in contrast to naïve implementations which loop over each rule and check for matching conditions, the Rete algorithm builds a network of nodes, where each node corresponds to a pattern occurring in the condition part of a rule. A rule is defined with a condition (when) part and a consequence (then) part:
rule "my very simple first rule" when Person(age > 30 && < 40 || hair == "black") then retract(p)In the condition part you access field constraints of the facts present in rule engine's working memory, f.i. to match all persons between 30 and 40 year of age or with black hair. Then in the consequence part you put actions to execute when this rule's condition evaluates to true and this rule is executed, f.i. the selected person is retracted from the rule engine's working memory. The normal sequence of interactions with a rule engine is as follows:
- insert all facts
- fire all rules
- retrieve the results
Setting up the environment
I set out to work to rewrite the code as a set of rules to be executed by the rule engine. Since I'm working on a Java project, I selected Drools Expert, open source software from Jboss/Redhat for this task. For a .Net project you could use Microsoft BRE. The remainder of this part will focus on setting up the Eclipse IDE to write (and debug) the rules and supporting code to use the Drools rule engine for a Java application. First we install the Jboss Tools plugin in Eclipse. This plugin gives us several rule editors, a Drools perspective and the execution/debug facilities for Drools. Now we can create a new Drools project or convert an existing project to a Drools project. If you choose for a new project, you can have a sample Hello World drools files generated, which provide great starting point for playing around with the rule engine. The following files are being generated:
- Sample.drl
- DroolsTest.java
rule "Hello World" when m : Message( status == Message.HELLO, myMessage : message ) then System.out.println( myMessage ); m.setMessage( "Goodbye cruel world" ); m.setStatus( Message.GOODBYE ); update( m ); end rule "GoodBye" when Message( status == Message.GOODBYE, myMessage : message ) then System.out.println( myMessage ); endThe first rule matches any Message with status equal to HELLO. In the consequence the message property is printed, the status of the message is changed to GOODBYE and also the message is changed. Two things to notice here. The 'm :', which binds a new local variable called 'm' to the matched Message instance. And also the myMessage variable is bound to the message property of the Message instance. The auto created variables can be references in subsequent 'when conditions' or in the consequence part of the rule like in this example. The second thing to notice is the update(m) statement, which notifies the rule engine that the Message instance was modified. This means that the engine will clear the agenda and reevaluate all rules, which sounds like a big thing, but can be accomplished very efficiently by the engine because of the Rete algorithm.
public class DroolsTest { public static final void main(String[] args) { try { // load up the knowledge base KnowledgeBase kbase = readKnowledgeBase(); StatefulKnowledgeSession ksession = kbase.newStatefulKnowledgeSession(); KnowledgeRuntimeLogger logger = KnowledgeRuntimeLoggerFactory.newFileLogger(ksession, "test"); // go ! Message message = new Message(); message.setText("Hello World"); message.setStatus(Message.HELLO); ksession.insert(message); ksession.fireAllRules(); logger.close(); } catch (Throwable t) { t.printStackTrace(); } } private static KnowledgeBase readKnowledgeBase() throws Exception { KnowledgeBuilder kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder(); kbuilder.add(ResourceFactory.newClassPathResource("Sample.drl"), ResourceType.DRL); KnowledgeBuilderErrors errors = kbuilder.getErrors(); if (errors.size() > 0) { for (KnowledgeBuilderError error : errors) { System.err.println(error); } throw new IllegalArgumentException("Could not parse knowledge."); } KnowledgeBase kbase = KnowledgeBaseFactory.newKnowledgeBase(); kbase.addKnowledgePackages(kbuilder.getKnowledgePackages()); return kbase; } public static class Message { public static final int HELLO = 0; public static final int GOODBYE = 1; private String text; private int status; getters and setters ... } }The following picture shows the audit logging after execution of the program. The audit log is a tremendous help in understanding how rules are executed.
The Message object is inserted, which results in the creation of an activation (rule is placed on the agenda). This line of logging is a result of the line ksession.insert(message) in main(). Next the program calls fireAllRules(). This will execute the first activation on the agenda, i.e. Rule Hello World. The activiation is executed (i.e. the consequence part of the rule is executed) which creates an activiation of the Goodbye rule. Then the Goodbye rule activation is executed which completes the rule engine, because there are no activiations on the agenda.
Conclusion
Now we have some understanding of how a rule engine works. I´ve managed to apply this technology to resolve our complexity problem in the messaging application. The resulting rule files are small and concise and easily maintainable because there is a clear relation to the functional requirements.
Dependency injection (Spring, CDI) gave us the ability to create applications with loosly coupled components. A rule engine can help us improving the internal logic of components on specific spots where complex if.. then constructions cause maintenance to be a helluvajob.