Most of the complexities that requirements impose on today's software products are behavioral and functional, resulting in component implementations with complex business logic. The most common way to implement the business logic in a J2EE or J2SE application is to write Java code that realizes the requirements document's rules and logic. In most cases, this code's intricacy and complexity makes maintaining and updating the application's business logic a daunting task, even for experienced developers. And any change, however simple, incurs recompilation and redeployment costs.
A rules engine helps you resolve (or at least reduce) the issues and difficulties inherent in the development and maintenance of an application's business logic. You can think of a rules engine as a framework for implementing complex business logic. Most rules engines let you use declarative programming to express the consequences that are valid given some information or knowledge. You can concentrate on facts that are known to be true and their associated outcomes — that is, on an application's business logic.
Several rules engines are available, including commercial and open source choices. Commercial rules engines usually let you express rules in a proprietary English-like language. Others let you write rules using scripting languages such as Groovy or Python. This updated article introduces you to the Drools engine and uses a sample program to help you understand how to use Drools as part of your business logic layer in a Java application.
Drools is an open source rules engine, written in the Java language, that uses the Rete algorithm to evaluate the rules you write (see Resources). Drools lets you express your business logic rules in a declarative way. You can write rules using a non-XML native language that is quite easy to learn and understand. And you can embed Java code directly in a rules file, which makes the experience of learning Drools even more attractive. Drools also has other advantages. It is:
- Supported by an active community
- Easy to use
- Quick to execute
- Gaining popularity among Java developers
- Compliant with the Java Rule Engine API (JSR 94) (see Resources)
- Free
As of this writing, the latest version of the Drools rules engine is 4.0.4. This is a major update. Although some backward-compatibility issues exist, this version's features make Drools even more attractive than before. For instance, the new native language for expressing rules is simpler and more elegant than the XML format some older versions use. This new language requires less coding and has a human-readable form.
Another notable development is that a Drools plug-in for the Eclipse IDE (Versions 3.2 and 3.3) is now available. I highly recommend that you use this plug-in to work with Drools. It simplifies the development of projects that use Drools and will improve your productivity. For instance, the plug-in checks your rules file for syntax errors and offers code completion. It also allows you to debug your rules file, potentially reducing debugging time from hours to minutes. You can add breakpoints to your rules file, which lets you inspect the state of the objects at specific moments during rule execution. This gives you information about the knowledge — a term you'll become familiar with later in this article — that the rules engine possesses at a particular moment in time.
This article shows how to use Drools as part of the business logic layer in a sample Java application. To follow along, you should be familiar with developing and debugging Java code using the Eclipse IDE. And you should be familiar with the JUnit testing framework and know how to use it within Eclipse.
The following assumptions set the scenario for the fictitious problem the application solves:
- A company named XYZ builds two types of computer machines: Type1 and Type2. A
machine's type is defined by its architecture.
- An XYZ computer can serve multiple functions. Four functions are currently
defined: DDNS Server, DNS Server, Gateway, and Router.
- XYZ performs several tests on each machine before it is shipped
out.
- The tests performed on each machine depend on each machine's type and
functions. Currently, five tests are defined: Test1, Test2, Test3, Test4, and
Test5.
- When tests are assigned to a computer, a tests due date is also
assigned to the machine. Tests assigned to the computer should be conducted no
later than this due date. The due date's value depends on the tests that were
assigned to the machine.
- XYZ has automated much of the process for executing the tests using an
internally developed software application that can determine a machine's type
and functions. Then, based on these properties, the application determines which
tests to execute and their due date.
- Currently, the logic that assigns the tests and tests due date to a computer
is part of this application's compiled code. The component that contains this
logic is written in the Java language.
- The logic for assigning tests and due dates is changed more than once a month. The developers must go through a tedious process every time they need to implement it in the Java code.
Because the company incurs high costs whenever changes are made to the logic that assigns tests and due dates to a computer, the XYZ executives have asked their software engineers to find a flexible way to "push" changes to the business rules to the production environment with minimal effort. Here's where Drools comes into play. The engineers have decided that if they use a rules engine to express the conditions for determining which tests should be performed, they can save much time and effort. They would only need to change the content of a rules file and then replace this file in the production environment. This seems simpler and less time consuming to them than changing compiled code and going through the long process mandated by the organization whenever compiled code is to be deployed in the production environment (see the sidebar When do you use a rules engine?).
Currently, these are the business rules that must be followed when assigning tests and their due dates to a machine:
- If a computer is of Type1, then only Test1, Test2, and Test5 should be
conducted on it.
- If a computer is of Type2 and one of its functions is DNS Server, then Test4
and Test5 should be conducted.
- If a computer is of Type2 and one of its functions is DDNS Server, then Test2
and Test3 should be conducted.
- If a computer is of Type2 and one of its functions is Gateway, then Test3 and
Test4 should be conducted.
- If a computer is of Type2 and one of its functions is Router, then Test1 and
Test3 should be conducted.
- If Test1 is among the tests to be conducted on a computer, then the tests due
date is three days from the machine's creation date. This rule has priority over
all following rules for the tests due date.
- If Test2 is among the tests to be conducted on a computer, then the tests due
date is seven days from the machine's creation date. This rule has priority over
all following rules for the tests due date.
- If Test3 is among the tests to be conducted on a computer, then the tests due
date is 10 days from the machine's creation date. This rule has priority over
all following rules for the tests due date.
- If Test4 is among the tests to be conducted on a computer, then the tests due
date is 12 days from the machine's creation date. This rule has priority over
all following rules for the tests due date.
- If Test5 is among the tests to be conducted on a computer, then the tests due
date is 14 days from the machine's creation date.
The current Java code that captures the preceding business rules for assigning tests and a tests due date to a machine looks similar to code in Listing 1:
Listing 1. Using if-else statements to implement business-rules logic
Machine machine = ...
// Assign tests
Collections.sort(machine.getFunctions());
int index;
if (machine.getType().equals("Type1")) {
Test test1 = ...
Test test2 = ...
Test test5 = ...
machine.getTests().add(test1);
machine.getTests().add(test2);
machine.getTests().add(test5);
} else if (machine.getType().equals("Type2")) {
index = Collections.binarySearch(machine.getFunctions(), "Router");
if (index >= 0) {
Test test1 = ...
Test test3 = ...
machine.getTests().add(test1);
machine.getTests().add(test3);
}
index = Collections.binarySearch(machine.getFunctions(), "Gateway");
if (index >= 0) {
Test test4 = ...
Test test3 = ...
machine.getTests().add(test4);
machine.getTests().add(test3);
}
...
}
// Assign tests due date
Collections.sort(machine.getTests(), new TestComparator());
...
Test test1 = ...
index = Collections.binarySearch(machine.getTests(), test1);
if (index >= 0) {
// Set due date to 3 days after Machine was created
Timestamp creationTs = machine.getCreationTs();
machine.setTestsDueTime(...);
return;
}
index = Collections.binarySearch(machine.getTests(), test2);
if (index >= 0) {
// Set due date to 7 days after Machine was created
Timestamp creationTs = machine.getCreationTs();
machine.setTestsDueTime(...);
return;
}
...
|
The code in Listing 1 isn't overly complicated, but it's not simple either. If you were to make changes to it, you'd need to be extremely careful. A bunch of entangled if-else statements are trying to capture the business logic that has been identified for the application. If you knew little or nothing about the business rules, the code's intent would not be apparent from a quick look.
A sample program that uses the Drools rules engine is provided with this article in a ZIP archive. The program uses a Drools rules file to express in a declarative way the business rules defined in the preceding section. It contains an Eclipse 3.2 Java project that was developed using the Drools plug-in and version 4.0.4 of the Drools rules engine. Follow these steps to set up the sample program:
- Download the ZIP archive (see Download).
- Download and install the Drools Eclipse plug-in (see Resources).
- In Eclipse, select the option to import Existing Projects into
Workspace, as shown in Figure 1:
Figure 1. Importing the sample program into your Eclipse workspace
- Select the archive file you downloaded and import it into your workspace.
You'll find a new Java project named
DroolsDemoin your workspace, as shown in Figure 2:.
Figure 2. Sample program imported into your workspace
If you have the Eclipse Build automatically option enabled, then the code
should be compiled and ready to use by now. Otherwise, build the
DroolsDemo project now.
Now you'll take a look at the code in the sample program. The core set of Java
classes for this program is in the demo package. There
you'll find the Machine and
Test domain object classes. An instance of the
Machine class represents a computer to which tests and
a tests due date are assigned. Take a look at the
Machine class, shown in Listing 2:
Listing 2. Instance variables for the
Machine class
public class Machine {
private String type;
private List functions = new ArrayList();
private String serialNumber;
private Collection tests = new HashSet();
private Timestamp creationTs;
private Timestamp testsDueTime;
public Machine() {
super();
this.creationTs = new Timestamp(System.currentTimeMillis());
}
...
|
You can see in Listing 2 that among the properties for the
Machine class are:
-
type(represented as astringproperty) - Holds the type value for a machine. -
functions(represented as alist) - Holds the functions for a machine. -
testsDueTime(represented as atimestampvariable) - Holds the assigned tests due date value. -
tests(aCollectionobject) - Holds the set of assigned tests.
Note that more than one test can be assigned to a machine and that a machine can have one or more functions.
For the sake of simplicity, the creation-time value for a machine is set to the
current time when an instance of the Machine class is
created. If this were a real-world application, the creation time would be set to
the actual time when the machine is finally built and ready to be tested.
An instance of the Test class represents a test that
can be assigned to a machine. A Test instance is
uniquely described by its id and
name, as shown in Listing 3:
Listing 3. Instance variables for the
Test class
public class Test {
public static Integer TEST1 = new Integer(1);
public static Integer TEST2 = new Integer(2);
public static Integer TEST3 = new Integer(3);
public static Integer TEST4 = new Integer(4);
public static Integer TEST5 = new Integer(5);
private Integer id;
private String name;
private String description;
public Test() {
super();
}
...
|
The sample program uses the Drools rules engine to evaluate instances of the
Machine class. Based on the values of a
Machine instance's type and
functions properties, the rules engine determines which
values should be assigned to the tests and
testsDueTime properties.
In the demo package, you'll also find an
implementation of a data access object (TestDAOImpl)
for Test objects, which lets you find
Test instances by ID. This data access object is
extremely simple; it does not connect to any external resources (such as
relational databases) to obtain Test instances.
Instead, a predefined set of Test instances is
hardcoded in its definition. In a real-world scenario, you would probably have a
data access object that does connect to an external resource to retrieve
Test objects.
One of the more important classes (if not the most important) in the
demo package is RulesEngine.
An instance of this class serves as a wrapper object that encapsulates the logic
to access the Drools classes. You could easily reuse this class in your own Java
projects, because the logic it contains is not specific to the sample program.
Listing 4 shows the properties and constructor of this class:
Listing 4. Instance variables and constructor for the
RulesEngine class
public class RulesEngine {
private RuleBase rules;
private boolean debug = false;
public RulesEngine(String rulesFile) throws RulesEngineException {
super();
try {
// Read in the rules source file
Reader source = new InputStreamReader(RulesEngine.class
.getResourceAsStream("/" + rulesFile));
// Use package builder to build up a rule package
PackageBuilder builder = new PackageBuilder();
// This parses and compiles in one step
builder.addPackageFromDrl(source);
// Get the compiled package
Package pkg = builder.getPackage();
// Add the package to a rulebase (deploy the rule package).
rules = RuleBaseFactory.newRuleBase();
rules.addPackage(pkg);
} catch (Exception e) {
throw new RulesEngineException(
"Could not load/compile rules file: " + rulesFile, e);
}
}
...
|
As you can see in Listing 4, the RulesEngine class's
constructor takes as an argument a string value that represents the name of the
file that contains a set of business rules. This constructor uses an instance of
the PackageBuilder class to parse and compile the rules
contained in the source file. (Note: This code assumes the rules file is
located in a folder called rules in the program's classpath.) Once this is done,
the PackageBuilder instance is used to merge all the
compiled rules into a binary Package instance. This
instance is then used to configure an instance of the Drools
RuleBase class, which is assigned to the
RulesEngine class's rules
property. You can think of an instance of this class as an in-memory
representation of the rules contained in your rules file.
Listing 5 shows the RulesEngine class's
executeRules() method:
Listing 5.
executeRules() method of the RulesEngine class
public void executeRules(WorkingEnvironmentCallback callback) {
WorkingMemory workingMemory = rules.newStatefulSession();
if (debug) {
workingMemory
.addEventListener(new DebugWorkingMemoryEventListener());
}
callback.initEnvironment(workingMemory);
workingMemory.fireAllRules();
}
|
The executeRules() method is pretty much where all
the magic in the Java code happens. Invoking this method executes the rules that
were previously loaded in the class's constructor. An instance of the Drools
WorkingMemory class is used to assert or declare the
knowledge that the rules engine should use to determine which
consequences should be executed. (If all the conditions of a rule are met, then
the consequences of that rule are executed.) Think of knowledge as the data or
information that a rules engine should use to determine whether the rules should
be fired. For instance, a rules engine's knowledge can consist of the current
state of one or more objects and their properties.
Execution of a rule's consequences occurs when the
WorkingMemory object's
fireAllRules() method is invoked. You might be
wondering (and I hope you are) how the knowledge is inserted into the
WorkingMemory instance. If you take a closer look at
this method's signature, you'll notice that the argument that is passed in is an
instance of the WorkingEnvironmentCallback interface.
Callers of the executeRules() method need to create an
object that implements this interface. This interface requires developers to
implement only one method, as shown in Listing 6:
Listing 6.
WorkingEnvironmentCallback interface
public interface WorkingEnvironmentCallback {
void initEnvironment(WorkingMemory workingMemory) throws FactException;
}
|
So, it's up to the caller of the executeRules()
method to insert the knowledge into the WorkingMemory
instance. I'll show how this is done soon.
Listing 7 shows the TestsRulesEngine class, also found
in the demo package:
Listing 7.
TestsRulesEngine class
public class TestsRulesEngine {
private RulesEngine rulesEngine;
private TestDAO testDAO;
public TestsRulesEngine(TestDAO testDAO) throws RulesEngineException {
super();
rulesEngine = new RulesEngine("testRules1.drl");
this.testDAO = testDAO;
}
public void assignTests(final Machine machine) {
rulesEngine.executeRules(new WorkingEnvironmentCallback() {
public void initEnvironment(WorkingMemory workingMemory) {
// Set globals first before asserting/inserting any knowledge!
workingMemory.setGlobal("testDAO", testDAO);
workingMemory.insert(machine);
};
});
}
}
|
The TestsRulesEngine class has only two instance
variables. The rulesEngine property is an instance of
the RulesEngine class. The
testDAO property holds a reference to a concrete
implementation of the TestDAO interface. The
rulesEngine object is instantiated using the
"testRules1.drl" string as the parameter for its
constructor. The testRules1.drl file captures the business rules in
The problem to solve in a declarative way. The
TestsRulesEngine class's
assignTests() method invokes the
RulesEngine class's
executeRules() method. In this method, an anonymous
instance of the WorkingEnvironmentCallback interface is
created, which is then passed as a parameter to the
executeRules() method.
If you take a look at the implementation of the
assignTests() method, you can see how knowledge is
inserted into the WorkingMemory instance. The
WorkingMemory class's
insert() method is called to state the knowledge that
the rules engine should use when evaluating rules. In this case, the knowledge
consists of an instance of the Machine class. Objects
that are inserted are used to evaluate a rule's conditions.
If you need your rules engine to have references to objects that are not
to be used as knowledge when evaluating conditions, you should use the
WorkingMemory class's
setGlobal() method. In the sample program, the
setGlobal() method passes a reference to the
TestDAO instance to the rules engine. The rules engine
then uses the TestDAO instance to look up any
Test instances it might need.
The TestsRulesEngine class is the only Java code in
the sample program that contains logic pertaining specifically to the
implementation of the business rules for assigning tests and a tests due date to
machines. The logic in this class may never need to change, even if the business
rules need to be updated.
As I previously mentioned, the testRules1.drl file contains the rules that the rules engine should follow to assign tests and a tests due date to a machine. It uses the Drools native language to express the rules it contains.
A Drools rules file has one or more rule
declarations. Each rule declaration is composed of one
or more conditional elements, and one or more consequences or
actions to execute. A rules file can also have multiple (that is, zero or
more) import declarations, multiple
global declarations, and multiple
function declarations.
The best way to understand the composition of a Drools rules file is to look at a real one. Take a look at the first section of the testRules1.drl file, shown in Listing 8:
Listing 8. First section of the testRules1.drl file
package demo;
import demo.Machine;
import demo.Test;
import demo.TestDAO;
import java.util.Calendar;
import java.sql.Timestamp;
global TestDAO testDAO;
|
In Listing 8, you can see how the import declarations
let the rules execution engine know where to find the class definitions of the
objects you'll be using in your rules. The global
declaration lets the rules engine know that an object should be accessible from
within your rules but that it should not be part of the knowledge used to evaluate
the rules' conditions. You can think of global
declarations as global variables within your rules. For a
global declaration, you need to specify its type (that
is, class name) and the identifier you want to use to refer to it (that is,
variable name). This identifier name in the global
declaration should match the identifier value that was used when the
WorkingMemory class's
setGlobal() method was invoked, which in this case is
testDAO (see Listing 7).
The function keyword is used to define a Java function
(see Listing 9). If you see code that's repeated in your
consequences (which I discuss soon), then you should probably extract that code
and write it as a Java function. However, when doing this you should be careful to
avoid writing complex Java code in the Drools rules file. The Java functions
defined in a rules file should be short and easy to follow. This is not a
technical limitation of Drools. If you want to write complex Java code in your
rules file, you can. But doing so will probably make your code harder to test,
debug, and maintain. Complex Java code should be part of a Java class. If you need
the Drools rules execution engine to invoke complex Java code, then you can pass a
reference to the Java object that contains the complex code to the rules engine as
global data.
Listing 9. Java functions defined in the testRules1.drl file
function void setTestsDueTime(Machine machine, int numberOfDays) {
setDueTime(machine, Calendar.DATE, numberOfDays);
}
function void setDueTime(Machine machine, int field, int amount) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(machine.getCreationTs());
calendar.add(field, amount);
machine.setTestsDueTime(new Timestamp(calendar.getTimeInMillis()));
}
...
|
Listing 10 shows the first rule in the testRules1.drl file:
Listing 10. First rule defined in testRules1.drl
rule "Tests for type1 machine"
salience 100
when
machine : Machine( type == "Type1" )
then
Test test1 = testDAO.findByKey(Test.TEST1);
Test test2 = testDAO.findByKey(Test.TEST2);
Test test5 = testDAO.findByKey(Test.TEST5);
machine.getTests().add(test1);
machine.getTests().add(test2);
machine.getTests().add(test5);
insert( test1 );
insert( test2 );
insert( test5 );
end
|
As you can see in Listing 10, the rule declaration
has a name that uniquely identifies it. You can also
see that the when keyword defines the conditional part
of a rule, and the then keyword defines the consequence
part. The rule shown in Listing 10 has one conditional element that references a
Machine object. If you go back to
Listing 7, you'll see that a
Machine object was inserted into the
WorkingMemory object. That same object is the one used
in this rule. The conditional element evaluates the
Machine instance (which is part of the knowledge) to
determine whether the consequences of the rule should be executed. If the
conditional element evaluates to true, the consequences
are then fired or executed. You can also see in Listing 10 that a consequence is
simply a Java language statement. By taking a quick look at this rule, you can
easily recognize that it's the implementation of the following business rule:
- If a computer is of Type1, then Test1, Test2, and Test5 should be the only tests conducted on this machine.
Therefore, this rule's conditional element checks whether the value of the
type property (of the
Machine object) is Type1.
(In a conditional element, you can access an object's properties without needing
to invoke the getter methods, as long as the object follows the Java bean
pattern.) If this evaluates to true, then a reference
to the Machine instance is assigned to the
machine identifier. This reference is then used in the
consequence part of the rule to assign tests to the
Machine object.
The only statements in this rule that might look somewhat strange are the last
three consequence statements. Recall from the business rules in
"The problem to solve" section that the value that
should be assigned as a tests due date depends on the tests that are assigned to
the machine. So the tests that are assigned to a machine need to become part of
the knowledge that the rules execution engine should use when evaluating the
rules. This is exactly what the three statements do. These statements use a method
named insert to update the knowledge in the rules
engine.
Determining rule execution order
Another important aspect of a rule is the optional
salience attribute. You use it to let the rules
execution engine know the order in which it should fire the consequence statements
of your rules. The consequence statements of the rule with the highest salience
value are executed first, the consequence statements of the rule with the
second-highest salience value are executed second, and so on. This is important
when you need your rules to be fired in a predefined order, as you'll see in a
moment.
The next four rules in the testRules1.drl file implement the remaining business
rules that pertain to the assignment of tests to machines (see Listing 11). These
rules are similar to the first rule I just discussed. Note that the
salience attribute value is the same for these first
five rules; the outcome of executing these five rules will be the same regardless
of the order in which they are fired. If the outcome were affected by the order in
which your rules are fired, then you would need to specify a different salience
value for your rules.
Listing 11. Remaining rules in testRules1.drl that pertain to assignment of tests
rule "Tests for type2, DNS server machine"
salience 100
when
machine : Machine( type == "Type2", functions contains "DNS Server")
then
Test test5 = testDAO.findByKey(Test.TEST5);
Test test4 = testDAO.findByKey(Test.TEST4);
machine.getTests().add(test5);
machine.getTests().add(test4);
insert( test4 );
insert( test5 );
end
rule "Tests for type2, DDNS server machine"
salience 100
when
machine : Machine( type == "Type2", functions contains "DDNS Server")
then
Test test2 = testDAO.findByKey(Test.TEST2);
Test test3 = testDAO.findByKey(Test.TEST3);
machine.getTests().add(test2);
machine.getTests().add(test3);
insert( test2 );
insert( test3 );
end
rule "Tests for type2, Gateway machine"
salience 100
when
machine : Machine( type == "Type2", functions contains "Gateway")
then
Test test3 = testDAO.findByKey(Test.TEST3);
Test test4 = testDAO.findByKey(Test.TEST4);
machine.getTests().add(test3);
machine.getTests().add(test4);
insert( test3 );
insert( test4 );
end
rule "Tests for type2, Router machine"
salience 100
when
machine : Machine( type == "Type2", functions contains "Router")
then
Test test3 = testDAO.findByKey(Test.TEST3);
Test test1 = testDAO.findByKey(Test.TEST1);
machine.getTests().add(test3);
machine.getTests().add(test1);
insert( test1 );
insert( test3 );
end
...
|
Listing 12 shows the remaining rules in the Drools rules file. As you've probably guessed, these rules pertain to the assignment of the tests due date:
Listing 12. Rules in testRules1.drl that pertain to assignment of the tests due date
rule "Due date for Test 5"
salience 50
when
machine : Machine()
Test( id == Test.TEST5 )
then
setTestsDueTime(machine, 14);
end
rule "Due date for Test 4"
salience 40
when
machine : Machine()
Test( id == Test.TEST4 )
then
setTestsDueTime(machine, 12);
end
rule "Due date for Test 3"
salience 30
when
machine : Machine()
Test( id == Test.TEST3 )
then
setTestsDueTime(machine, 10);
end
rule "Due date for Test 2"
salience 20
when
machine : Machine()
Test( id == Test.TEST2 )
then
setTestsDueTime(machine, 7);
end
rule "Due date for Test 1"
salience 10
when
machine : Machine()
Test( id == Test.TEST1 )
then
setTestsDueTime(machine, 3);
end
|
The implementation of these rules is a little simpler than the implementation of the rules for assigning tests, but I find them a little more interesting, for four reasons.
First, note that the order in which these rules should execute does matter. The
outcome (that is, the value assigned to a Machine
instance's testsDueTime property) is affected by the
order in which these rules are fired. If you review the business rules detailed in
The problem to solve, you'll notice that the rules for
assigning a tests due date have a precedence order. For instance, if Test3, Test4,
and Test5 have been assigned to a machine, then the tests due date should be 10
days from the machine's creation date. The reason is that the tests due date rule
for Test3 has precedence over the tests due date rules for Test4 and Test5. How do
you express this in a Drools rules file? The answer is the
salience attribute. The value of the
salience attribute of the rules that set a value to the
testsDueTime property is different. The tests due date
rule for Test1 has precedence over all the other tests due date rules, so this
should be the last rule to be fired. In other words, the value assigned by this
rule is the one that should prevail in the case that Test1 is among the tests that
were assigned to a machine. So, the value of the
salience attribute for this rule is the lowest one: 10.
Second, each one of these rules has two conditional elements. The first element
simply checks for the existence of a Machine instance
in the working memory. (Note that no comparisons are performed on the
Machine object's properties.) Whenever this element
evaluates to true, it assigns a reference to the
Machine object that is then used in the consequence
part of the rule. Without this reference assignment, there would be no way to
assign a tests due date to the Machine object. The
second conditional element checks the id property of
the Test object. Only when both conditional elements
evaluate to true are the rule's consequence elements
executed.
Third, the conditional parts of these rules are not (and cannot be) evaluated by
the Drools rules execution engine until an instance of the
Test class is part of the knowledge (that is, contained
in the working memory). This seems quite logical, because if an instance of the
Test class isn't in the working memory yet, the rules
execution engine has no way to perform the comparison contained in these rules'
conditions. If you're wondering when a Test instance
becomes part of the knowledge, recall that one or more
Test instances are inserted into the working memory
during the execution of the consequences of the rules that pertain to the
assignment of tests (see Listing 10 and
Listing 11).
Fourth, note that the consequence part of these rules is quite short and simple.
The reason is that in all of them an invocation is made to the
setTestsDueTime() Java method that was defined earlier
in the rules file using the function keyword. This
method is where the actual assignment of a value to the
testsDueTime property occurs.
Now that you've gone over the code that implements the business-rules logic, it's
time to see if it works. To execute the sample program, run the
TestsRulesEngineTest JUnit test found in the
demo.test package.
In this test, five Machine objects are created, each
one with a different set of properties (serial numbers, types, and functions). The
TestsRulesEngine class's
assignTests() method is invoked, iteratively, for each
one of these five Machine objects. Once the
assignTests() method finishes its execution, assertions
are performed to verify that the business-rules logic specified in the
testRules1.drl is correct (see Listing 13). You could modify the
TestsRulesEngineTest JUnit class to add a few more
Machine instances with different properties and then
use assertions to verify that the outcome is as expected.
Listing 13. Assertions in
testTestsRulesEngine() to verify that the business logic's implementation is correct
public void testTestsRulesEngine() throws Exception {
while (machineResultSet.next()) {
Machine machine = machineResultSet.getMachine();
testsRulesEngine.assignTests(machine);
Timestamp creationTs = machine.getCreationTs();
Calendar calendar = Calendar.getInstance();
calendar.setTime(creationTs);
Timestamp testsDueTime = machine.getTestsDueTime();
if (machine.getSerialNumber().equals("1234A")) {
assertEquals(3, machine.getTests().size());
assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST1)));
assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST2)));
assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST5)));
calendar.add(Calendar.DATE, 3);
assertEquals(calendar.getTime(), testsDueTime);
} else if (machine.getSerialNumber().equals("1234B")) {
assertEquals(4, machine.getTests().size());
assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST5)));
assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST4)));
assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST3)));
assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST2)));
calendar.add(Calendar.DATE, 7);
assertEquals(calendar.getTime(), testsDueTime);
...
|
A few more comments on knowledge
It's worth mentioning that besides inserting objects into the working memory, you
can also modify objects in it or retract them from it. You can do this within a
rule's consequence part. If an object that's part of the current knowledge is
modified in a consequence statement, and if the modified property is used in a
conditional element to determine whether a rule should be fired, you should invoke
the update() method in the rule's consequence part.
When you invoke the update() method, you let the Drools
rules execution engine know that an object has been updated and that any
conditional element (of any rule) that references this object (for instance,
inspects the value of one or more of the object's properties) should be
reevaluated to determine if the condition's result is now
true or false. This means
that even the conditions of the current active rule (the rule that modifies the
object in its consequence part) can be reevaluated, which might cause the rule to
be fired again and could lead to an infinite recursion. If you don't want the
active rule to be reevaluated, you should include the
rule's optional no-loop
attribute and give it a value of true.
Listing 14 demonstrates this situation with pseudocode for the definition of two
rules. Rule 1 modifies
property1 of objectA. It
then invokes the update() method to let the rules
execution engine know about this change, which should trigger a reevaluation of
the conditional elements of the rules that reference
objectA. Hence, the condition for firing
Rule 1 should be reevaluated. And because this
condition should evaluate again to true (the value of
property2 is still the same because it was not changed
in the consequence part of the rule), Rule 1 should be
fired again, resulting in the execution of an infinite loop. To prevent this
situation, you add the no-loop attribute and give it a
value of true, which prevents the current active rule
from reexecuting.
Listing 14. Modifying an object in the working memory and using the rule element's
no-loop attribute
...
rule "Rule 1"
salience 100
no-loop true
when
objectA : ClassA (property2().equals(...))
then
Object value = ...
objectA.setProperty1(value);
update( objectA );
end
rule "Rule 2"
salience 100
when
objectB : ClassB()
objectA : ClassA ( property1().equals(objectB) )
...
then
...
end
...
|
If an object should no longer be part of the knowledge, then you should retract
that object from the working memory (see Listing 15). You do this by calling the
retract() method in the consequence part of your rule.
When an object is removed from the working memory, any conditional elements (of
any rule) that had a reference to this object cannot be evaluated now. Because the
object no longer exists as part of the knowledge, the rule has no chance of being
fired.
Listing 15. Retracting an object from the working memory
...
rule "Rule 1"
salience 100
when
objectB : ...
objectA : ...
then
Object value = ...
objectA.setProperty1(value);
retract(objectB);
end
rule "Rule 2"
salience 90
when
objectB : ClassB ( property().equals(...) )
then
...
end
...
|
Listing 15 contains pseudocode for the definition of two rules. Assume the
conditions for firing both rules evaluate to true.
Then, Rule 1 should be fired first because
Rule 1 has a higher salience value than
Rule 2. Now, note that in the consequence part of
Rule 1, objectB is retracted
from the working memory (that is, objectB is no longer
part of the knowledge). This action alters the "execution agenda" of the rules
engine because Rule 2 won't be fired now. The reason is
that the condition for firing Rule 2 that was once true
is not true anymore because it references an object
(objectB) that is no longer part of the knowledge. If
Listing 15 contained more rules referencing objectB
that have not been fired yet, they would now no longer be fired.
As a concrete example of how to modify current knowledge in the working memory,
I'll rewrite the rules source file I discussed earlier. The business rules are
still the same as outlined in "The problem to solve"
section. However, I'll use a different implementation for the rules to achieve the
same results. With this approach, a Machine instance is
the only knowledge available to the working memory at all times. In other words,
the conditional elements of the rules will perform comparisons only on properties
of a Machine object. This is different from the earlier
approach, which also made comparisons of properties of
Test objects (see Listing 12).
This new implementation of the rules is captured in the sample application's
testRules2.drl file. Listing 16 shows the rules in testRules2.drl that pertain to
tests assignment:
Listing 16. Rules in testRules2.drl that pertain to assignment of tests
rule "Tests for type1 machine"
lock-on-active true
salience 100
when
machine : Machine( type == "Type1" )
then
Test test1 = testDAO.findByKey(Test.TEST1);
Test test2 = testDAO.findByKey(Test.TEST2);
Test test5 = testDAO.findByKey(Test.TEST5);
machine.getTests().add(test1);
machine.getTests().add(test2);
machine.getTests().add(test5);
update( machine );
end
rule "Tests for type2, DNS server machine"
lock-on-active true
salience 100
when
machine : Machine( type == "Type2", functions contains "DNS Server")
then
Test test5 = testDAO.findByKey(Test.TEST5);
Test test4 = testDAO.findByKey(Test.TEST4);
machine.getTests().add(test5);
machine.getTests().add(test4);
update( machine );
end
rule "Tests for type2, DDNS server machine"
lock-on-active true
salience 100
when
machine : Machine( type == "Type2", functions contains "DDNS Server")
then
Test test2 = testDAO.findByKey(Test.TEST2);
Test test3 = testDAO.findByKey(Test.TEST3);
machine.getTests().add(test2);
machine.getTests().add(test3);
update( machine );
end
rule "Tests for type2, Gateway machine"
lock-on-active true
salience 100
when
machine : Machine( type == "Type2", functions contains "Gateway")
then
Test test3 = testDAO.findByKey(Test.TEST3);
Test test4 = testDAO.findByKey(Test.TEST4);
machine.getTests().add(test3);
machine.getTests().add(test4);
update( machine );
end
rule "Tests for type2, Router machine"
lock-on-active true
salience 100
when
machine : Machine( type == "Type2", functions contains "Router")
then
Test test3 = testDAO.findByKey(Test.TEST3);
Test test1 = testDAO.findByKey(Test.TEST1);
machine.getTests().add(test3);
machine.getTests().add(test1);
update( machine );
end
...
|
If you compare the definition of the first rule in Listing 16 to the definition
shown in Listing 10, you can see that instead of
inserting the Test instances that were assigned to the
Machine object into the working memory, the consequence
part of the rule invokes the update() method to let the
rules engine know that the Machine object has been
modified. (Test instances were added/assigned to it.)
If you look at the remaining rules in Listing 16, you should see that this is the
approach also taken whenever tests are assigned to a
Machine object: one or more
Test instances are assigned to a
Machine instance and, consequently, the working
knowledge is modified, and the rules engine is notified about it.
Note also the active-lock attribute used for all rules
shown in Listing 16. The value of this attribute is set to
true; if it weren't, you'd end up with an infinite loop
when executing these rules. Setting it to true ensures
that when a rule updates the knowledge in the working memory, you don't end up
with rules being reevaluated and reexecuted, which could then cause an infinite
recursion. You can think of the active-lock attribute
as a stronger version of the no-loop attribute. The
no-loop attribute ensures that the rule modifying the
knowledge is not invoked again as a consequence of its own update, whereas the
active-lock attribute ensures that any rules (with this
attribute set to true) in your file are not reexecuted
as a consequence of modifying the knowledge.
Listing 17 shows how the remaining rules were changed:
Listing 17. Rules in testRules2.drl that pertain to assignment of the tests due date
rule "Due date for Test 5"
salience 50
when
machine : Machine(tests contains (testDAO.findByKey(Test.TEST5)))
then
setTestsDueTime(machine, 14);
end
rule "Due date for Test 4"
salience 40
when
machine : Machine(tests contains (testDAO.findByKey(Test.TEST4)))
then
setTestsDueTime(machine, 12);
end
rule "Due date for Test 3"
salience 30
when
machine : Machine(tests contains (testDAO.findByKey(Test.TEST3)))
then
setTestsDueTime(machine, 10);
end
rule "Due date for Test 2"
salience 20
when
machine : Machine(tests contains (testDAO.findByKey(Test.TEST2)))
then
setTestsDueTime(machine, 7);
end
rule "Due date for Test 1"
salience 10
when
machine : Machine(tests contains (testDAO.findByKey(Test.TEST1)))
then
setTestsDueTime(machine, 3);
end
|
These rules' conditional elements now inspect a
Machine object's tests
collection to determine if it contains a specific Test
instance. Hence, as I mentioned earlier, with this approach the rules engine is
dealing with only one object in the working memory (a
Machine instance), as opposed to multiple objects
(Machine and Test
instances).
To test the testRules2.drl file, just edit the
TestsRulesEngine class provided in the sample
application (see Listing 7): change the
"testRules1.drl" string to
"testRules2.drl" and then run the
TestsRulesEngineTest JUnit test. All tests should
succeed just as they did with testRules1.drl as the rules source.
As I mentioned before, the Drools plug-in for Eclipse allows you to place breakpoints in your rules file. Be aware that these breakpoints are enabled only when you debug your program as a "Drools Application." Otherwise, the debugger ignores them.
For example, suppose you want to debug the
TestsRulesEngineTest JUnit test class as a "Drools
Application." Open the general Debug dialog in Eclipse. In this dialog window, you
should see a "Drools Application" category. Under this category, create a new
launch configuration. In the Main tab for this new configuration, you should see a
Project field and a Main class field. For the Project field, select the
Drools4Demo project. For the Main class field, enter
junit.textui.TestRunner (see Figure 3).
Figure 3. Drools application launch configuration for
TestsRulesEngineTest class (Main
tab)
Now select the Arguments tab and enter
-t demo.test.TestsRulesEngineTest as a program argument
(see Figure 4). Once you are done entering the argument, save your new launch
configuration by clicking on the Apply button on the bottom right corner of
the dialog. You can then click the Debug button to start debugging the
TestsRulesEngineTest JUnit class as a "Drools
Application." If you added breakpoints to testRules1.drl or testRules2.drl, the
debugger should stop at them when using this launch configuration.
Figure 4. Drools Application launch configuration for
TestsRulesEngineTest class
(Arguments tab)
Using a rules engine can significantly reduce the complexity of components that implement the business-rules logic in your Java applications. An application that uses a rules engine to express rules using a declarative approach has a higher chance of being more maintainable and extensible than one that doesn't. As you've seen, Drools is a powerful and flexible rules engine implementation. Using Drools' features and capabilities, you should be able to implement the complex business logic of your application in a declarative manner. And as you've seen, Drools makes learning and using declarative programming quite easy for Java developers.
The Drools classes that this article showed you are Drools-specific. If you were to use another rules engine implementation with the sample program, the code would need a few changes. Because Drools is JSR 94-compliant, you could use the Java Rule Engine API (as specified in JSR 94) to interface with Drools-specific classes. (The Java Rule Engine API is for rules engines what JDBC is for databases.) If you use this API, then you can change your rules engine implementation to a different one without needing to change the Java code, as long as this other implementation is also JSR 94-compliant. JSR 94 does not address the structure of the rules file that contains your business rules (testRules1.drl in this article's sample application). The file's structure would still depend on the rules engine implementation you choose. As an exercise, you can modify the sample application so that it uses the Java Rule Engine API instead of referencing the Drools-specific classes in the Java code.
| Description | Name | Size | Download method |
|---|---|---|---|
| Sample Java project that uses Drools | j-Drools4Demo.zip | 45KB | HTTP |
Information about download methods
Learn
-
Drools Web site: Get more information
about the Drools rules engine project.
-
"The Logic of the Bottom Line: An Introduction to The Drools Project"
(N. Alex Rupp, TheServerSide.com, May 2004): A good introduction to the Drools
rules engine.
- "Getting Started With the Java Rule Engine API (JSR 94): Toward Rule-Based Applications"
(Qusay H. Mahmoud, Sun Developer Network, July 2005): An introduction to the Java
Rule Engine API.
-
JSR 94: Java Rule Engine API:
The official JSR 94 specification.
-
Drools documentation:
The Drools documentation library.
-
"The Rete Matching Algorithm" (Bruce
Schneier, Dr. Dobb's Portal, December 1992): A description of the pattern-matching
algorithm underlying Drools.
-
Technology bookstore:
Browse for books on these and other technical topics.
-
The Java technology zone:
Hundreds of articles about every aspect of Java programming.
Get products and technologies
-
Drools: Download the
latest Drools distribution and Drools plug-in for Eclipse.
-
Eclipse: Download
the latest version of the Eclipse IDE for the Java platform.
Discuss
-
developerWorks blogs: Get
involved in the developerWorks community.

Ricardo Olivieri is a software engineer in IBM Global Services. His areas of expertise include design and development of enterprise Java applications for WebSphere Application Server, administration and configuration of WebSphere Application Server, and distributed software architectures. During the last few years, Ricardo has become interested in learning about open source projects such as Drools, Spring, WebWork, Hibernate, and JasperReports. He is a certified Java developer and a certified WebSphere Application Server administrator. He has a B.S. in computer engineering from the University of Puerto Rico Mayaguez Campus.
Comments (Undergoing maintenance)





