Implement business logic with the Drools rules engine
Use a declarative programming approach to write your program's business logic
http://www-128.ibm.com/developerworks/java/library/j-drools/
Ricardo Olivieri (
roliv@us.ibm.com), Software Engineer, IBM
Using a rules engine can lower an application's maintenance and extensibility costs by reducing the complexity of components that implement complex business logic. This article shows you how to use the Drools rules engine to make a Java™ application more adaptive to changes. Drools has the added benefit of a syntax that lets you embed Java code directly in a rules file.
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 an application's business logic a daunting task, even for experienced developers. And any change, however simple, incurs recompilation and redeployment costs.
A rules engine tries to 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 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 (see Resources) to evaluate the rules you write. Drools lets you express your business logic rules in a declarative way. You can write rules in a Java/XML syntax, which is very useful for getting started with Drools because you can embed Java code directly in a rules file. You can also use a Groovy/XML syntax or a Python/XML syntax to write rules in Drools. Drools has other advantages:
- A very active community
- Easy to use
- Fast execution speed
- Gaining popularity among Java developers
- JSR 94-compliant (JSR 94 is the Java Rule Engine API) (see Resources)
- Free
To follow along, you should be familiar with developing Java code using the Eclipse IDE. You should be familiar with the JUnit testing framework and know how to use it within Eclipse. You should also have a good understanding of XML.
The problem to solve
This article shows how to use Drools as part of the business logic layer in a sample Java application. 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 above 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.
Importing the sample program
A sample program that uses the Drools rules engine is provided with this article in a ZIP archive (see Downloads). The program uses a Drools rules file to express in a declarative way the business rules defined in the preceding section. I suggest you download the ZIP archive before continuing. It contains an Eclipse (v3.1) Java project to import into your Eclipse workspace. Select the option to import Existing Projects into Workspace (see Figure 1):
Figure 1. Importing the sample program into your Eclipse workspace
Then select the archive file you downloaded and import it into your workspace. You'll find a new Java project named DroolsDemo
in your workspace, as shown in Figure 2:
Figure 2. Sample program imported into your workspace
If you have the Build automatically option enabled, then the code should be compiled and ready to use by now. If you don't have this option enabled, then build the DroolsDemo
project now.
Examining the code
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 machine 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 a string
property) - Holds the type value for a machine.
functions
(represented as a list
) - Holds the functions for a machine.
testsDueTime
(represented as a timestamp
variable) - Holds the assigned tests due date value.
tests
(a Collection
object) - 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 the type
and functions
properties of a Machine
instance, 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.
The RulesEngine class
One of the more important classes (if not the most important) in the demo
package is the RulesEngine
class. 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 static Logger logger = Logger.getLogger(RulesEngine.class);
private RuleBase rules;
private String rulesFile;
private boolean debug = false;
public RulesEngine(String rulesFile) throws RulesEngineException {
super();
this.rulesFile = rulesFile;
try {
rules = RuleBaseLoader.loadFromInputStream(this.getClass()
.getResourceAsStream("/rules/" + rulesFile));
} catch (Exception e) {
throw new RulesEngineException("Could not load 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 the RuleBaseLoader
class's static loadFromInputStream()
method to load the rules contained in the rules file into memory. (Note: this code assumes the rules file is located in a folder called rules in the program's classpath.) The loadFromInputStream()
method returns an instance of the Drools RuleBase
class, which is assigned to the RulesEngine
class's rules
property. You can think of an instance of the RulesBase
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 RulesEngine class
public List executeRules(WorkingEnvironmentCallback callback)
throws RulesEngineException {
try {
WorkingMemory workingMemory = rules.newWorkingMemory();
if (debug) {
workingMemory.addEventListener(
new DebugWorkingMemoryEventListener());
}
callback.initEnvironment(workingMemory);
workingMemory.fireAllRules();
return workingMemory.getObjects();
} catch (FactException fe) {
logFactException(fe);
throw new RulesEngineException(
"Exception occurred while attempting to execute "
+ "rules file: " + rulesFile, fe);
}
}
|
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 consequence of that rule is 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 the consequence of a rule occurs when the WorkingMemory
object's fireAllRules()
method is invoked. You might be wondering (and I hope you are) how the knowledge is asserted 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 (see 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 assert the knowledge into the WorkingMemory
instance. I'll show how this is done soon.
The TestsRulesEngine class
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("testRules.xml");
this.testDAO = testDAO;
}
public void assignTests(final Machine machine) {
rulesEngine.executeRules(new WorkingEnvironmentCallback() {
public void initEnvironment(WorkingMemory workingMemory)
throws FactException {
workingMemory.assertObject(machine);
Iterator functions = machine.getFunctions().iterator();
while (functions.hasNext()) {
workingMemory.assertObject(functions.next());
}
workingMemory.setApplicationData("testDAO", testDAO);
};
});
}
}
|
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 "testRules.xml
" string as the parameter for its constructor. The testRules.xml 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 asserted into the WorkingMemory
instance. The WorkingMemory
class's assertObject()
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 and the functions of that machine. Objects that are asserted 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 setApplicationData()
method. In the sample program, the setApplicationData()
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.
The Drools rules file
As I previously mentioned, the testRules.xml file contains the rules that the rules engine should follow to assign tests and a tests due date to a machine. It uses a Java/XML syntax to express the rules it contains.
A Drools rules file has a root element named rule-set
that is composed of one or more rule
elements. Each rule
element is composed of one or more parameter
elements, one or more condition
elements, and a consequence
element. The rule-set
element can also have one or more import
elements, one or more application-data
elements, and a functions
element.
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 testRules.xml file, shown in Listing 8:
Listing 8. First section of the testRules.xml file
<rule-set name="Tests assignment rules" xmlns="http://drools.org/rules"
xmlns:java="http://drools.org/semantics/java"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<java:import>demo.Machine</java:import>
<java:import>demo.Test</java:import>
<java:import>demo.TestDAO</java:import>
<java:import>java.util.Calendar</java:import>
<java:import>java.sql.Timestamp</java:import>
<java:import>java.lang.String</java:import>
<application-data identifier="testDAO">TestDAO</application-data>
...
|
In Listing 8, you can see the root element rule-set
has a name
attribute that identifies this set of rules. The import
elements let the rules execution engine know where to find the class definitions of the objects you'll be using in your rules. The application-data
element 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. This element has an identifier
attribute that should match the identifier
value that was used when the WorkingMemory
class's setApplicationData()
method was invoked (see Listing 7).
The functions
element can contain the definition of one or more Java functions (see Listing 9). If you see code that's repeated in your consequence
elements (which I discuss soon), then you should probably extract that code and write it as a Java function inside a functions
element. However, be careful when using this element because you should avoid writing complex Java code in the Drools rules file. The Java functions defined within this element 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 class that contains the complex code to the rules engine as application data.
Listing 9. Java functions defined in the testRules.xml file
<java:functions>
public static void setTestsDueTime(Machine machine, int numberOfDays) {
setTestsDueTime(machine, Calendar.DATE, numberOfDays);
}
public static void setTestsDueTime(Machine machine, int field, int amount) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(machine.getCreationTs());
calendar.add(field, amount);
machine.setTestsDueTime(new Timestamp(calendar.getTimeInMillis()));
}
</java:functions>
...
|
Listing 10 shows the first rule found in the testRules.xml file:
Listing 10. First rule defined in the testRules.xml file
<rule name="Tests for type1 machine" salience="100">
<parameter identifier="machine">
<java:class>
Machine
</java:class>
</parameter>
<java:condition>machine.getType().equals("Type1")</java:condition>
<java:consequence>
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);
drools.assertObject(test1);
drools.assertObject(test2);
drools.assertObject(test5);
</java:consequence>
</rule>
|
As shown in Listing 10, the rule
element has a name
attribute that uniquely identifies a rule
within a rule-set
. You can see that the rule shown in Listing 10 takes only one parameter: a Machine
object. If you go back to Listing 7, you'll see that a Machine
object was asserted into the WorkingMemory
object. That same object is the one that is passed as a parameter to this rule. The condition
element evaluates the Machine
instance (which is part of the knowledge) to determine whether the consequence of the rule should be executed. If the condition evaluates to true
, the consequence is then fired or executed. A consequence
element has one or more Java language statements. 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.
The only statements that might look somewhat strange are the last three Java statements that are part of the consequence
element. Recall from the business rules in The problem to solve 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 last three statements within the consequence
element do. These statements use a variable named drools
(which is available in any consequence block) 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 blocks of the rules within a rule set. The consequence block of the rule with the highest salience value is executed first; the consequence block of the rule with the second highest salience value is executed second, and so on. This is very 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 testRules.xml file implement the remaining business rules that pertain to the assignment of tests to machines (see Listing 11). These rules are very 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 the testRules.xml file that pertain to assignment of tests
<rule name="Tests for type2, DNS server machine" salience="100">
<parameter identifier="machine">
<java:class>
Machine
</java:class>
</parameter>
<parameter identifier="function">
<java:class>
String
</java:class>
</parameter>
<java:condition>machine.getType().equals("Type2")</java:condition>
<java:condition>function.equals("DNS Server")</java:condition>
<java:consequence>
Test test5 = testDAO.findByKey(Test.TEST5);
Test test4 = testDAO.findByKey(Test.TEST4);
machine.getTests().add(test5);
machine.getTests().add(test4);
drools.assertObject(test4);
drools.assertObject(test5);
</java:consequence>
</rule>
<rule name="Tests for type2, DDNS server machine" salience="100">
<parameter identifier="machine">
<java:class>
Machine
</java:class>
</parameter>
<parameter identifier="function">
<java:class>
String
</java:class>
</parameter>
<java:condition>machine.getType().equals("Type2")</java:condition>
<java:condition>function.equals("DDNS Server")</java:condition>
<java:consequence>
Test test2 = testDAO.findByKey(Test.TEST2);
Test test3 = testDAO.findByKey(Test.TEST3);
machine.getTests().add(test2);
machine.getTests().add(test3);
drools.assertObject(test2);
drools.assertObject(test3);
</java:consequence>
</rule>
<rule name="Tests for type2, Gateway machine" salience="100">
<parameter identifier="machine">
<java:class>
Machine
</java:class>
</parameter>
<parameter identifier="function">
<java:class>
String
</java:class>
</parameter>
<java:condition>machine.getType().equals("Type2")</java:condition>
<java:condition>function.equals("Gateway")</java:condition>
<java:consequence>
Test test3 = testDAO.findByKey(Test.TEST3);
Test test4 = testDAO.findByKey(Test.TEST4);
machine.getTests().add(test3);
machine.getTests().add(test4);
drools.assertObject(test3);
drools.assertObject(test4);
</java:consequence>
</rule>
<rule name="Tests for type2, Router machine" salience="100">
<parameter identifier="machine">
<java:class>
Machine
</java:class>
</parameter>
<parameter identifier="function">
<java:class>
String
</java:class>
</parameter>
<java:condition>machine.getType().equals("Type2")</java:condition>
<java:condition>function.equals("Router")</java:condition>
<java:consequence>
Test test3 = testDAO.findByKey(Test.TEST3);
Test test1 = testDAO.findByKey(Test.TEST1);
machine.getTests().add(test3);
machine.getTests().add(test1);
drools.assertObject(test1);
drools.assertObject(test3);
</java:consequence>
</rule>
...
|
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 the testRules.xml file that pertain to assignment of the tests due date
<rule name="Due date for Test 5" salience="50">
<parameter identifier="machine">
<java:class>
Machine
</java:class>
</parameter>
<parameter identifier="test">
<java:class>
Test
</java:class>
</parameter>
<java:condition>test.getId().equals(Test.TEST5)</java:condition>
<java:consequence>
setTestsDueTime(machine, 14);
</java:consequence>
</rule>
<rule name="Due date for Test 4" salience="40">
<parameter identifier="machine">
<java:class>
Machine
</java:class>
</parameter>
<parameter identifier="test">
<java:class>
Test
</java:class>
</parameter>
<java:condition>test.getId().equals(Test.TEST4)</java:condition>
<java:consequence>
setTestsDueTime(machine, 12);
</java:consequence>
</rule>
<rule name="Due date for Test 3" salience="30">
<parameter identifier="machine">
<java:class>
Machine
</java:class>
</parameter>
<parameter identifier="test">
<java:class>
Test
</java:class>
</parameter>
<java:condition>test.getId().equals(Test.TEST3)</java:condition>
<java:consequence>
setTestsDueTime(machine, 10);
</java:consequence>
</rule>
<rule name="Due date for Test 2" salience="20">
<parameter identifier="machine">
<java:class>
Machine
</java:class>
</parameter>
<parameter identifier="test">
<java:class>
Test
</java:class>
</parameter>
<java:condition>test.getId().equals(Test.TEST2)</java:condition>
<java:consequence>
setTestsDueTime(machine, 7);
</java:consequence>
</rule>
<rule name="Due date for Test 1" salience="10">
<parameter identifier="machine">
<java:class>
Machine
</java:class>
</parameter>
<parameter identifier="test">
<java:class>
Test
</java:class>
</parameter>
<java:condition>test.getId().equals(Test.TEST1)</java:condition>
<java:consequence>
setTestsDueTime(machine, 3);
</java:consequence>
</rule>
|
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 three 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, the condition
elements of the rules that assign a value to the testsDueTime
property are not (and cannot be) evaluated by the Drools rules execution engine until there is an instance of the Test
class that 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, 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 asserted into the working memory during the execution of the consequence block of the rules that pertain to the assignment of tests (see Listing 10 and Listing 11).
Third, note that the consequence block 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 in rules file's functions
element. This method is where the actual assignment of a value to the testsDueTime
property occurs.
Testing the code
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 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 testRules.xml 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 made in the testTestsRulesEngine() method 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 asserting 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 block. If an object that is part of the current knowledge is modified in a consequence block and if the property that was modified is used in a condition
element to determine whether a rule should be fired, you should invoke the drools
instance's modifyObject()
method in your consequence block. When you invoke the modifyObject()
method, you let the Drools rules execution engine know that an object has been updated and that any condition (of any rule) that uses one or more properties of this object should be evaluated again to determine if the result of the condition 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 block) can be evaluated again, which might cause the rule to be fired again and could lead to an infinite recursion. If you don't want this to occur, you should include the rule
element'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 modifyObject()
of the drools
variable to let the rules execution engine know about this update, which should trigger a reevaluation of the condition
elements of the rules that reference objectA
. Hence, the condition for firing Rule 1
should be evaluated again. 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 block), 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 executing again.
Listing 14. Modifying an object in the working memory and using the rule element's no-loop attribute
...
<rule name="Rule 1" salience="100" no-loop="true">
<parameter identifier="objectA">
<java:class>
ClassA
</java:class>
</parameter>
<java:condition>objectA.getProperty2().equals(...)</java:condition>
<java:consequence>
Object value = ...
objectA.setProperty1(value);
drools.modifyObject(objectA);
</java:consequence>
</rule>
<rule name="Rule 2" salience="100">
<parameter identifier="objectA">
<java:class>
ClassA
</java:class>
</parameter>
<parameter identifier="objectB">
<java:class>
ClassB
</java:class>
</parameter>
<java:condition>objectA.getProperty1().equals(objectB)</java:condition>
...
<java:consequence>
...
</java:consequence>
</rule>
...
|
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 drools
object's retractObject()
method in a consequence block. When an object is removed from the working memory, any condition
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 name="Rule 1" salience="100" >
<parameter identifier="objectA">
<java:class>
ClassA
</java:class>
</parameter>
<parameter identifier="objectB">
<java:class>
ClassB
</java:class>
</parameter>
<java:condition>...</java:condition>
<java:condition>...</java:condition>
<java:consequence>
Object value = ...
objectA.setProperty1(value);
drools.retractObject(objectB);
</java:consequence>
</rule>
<rule name="Rule 2" salience="90">
<parameter identifier="objectB">
<java:class>
ClassB
</java:class>
</parameter>
<java:condition>objectB.getProperty().equals(...)</java:condition>
...
<java:consequence>
...
</java:consequence>
</rule>
...
|
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 block 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 anymore because it references an object (objectB
) that is no longer part of the knowledge. If there were more rules in Listing 15 referencing objectB
and those rules had not been fired yet, they would now no longer be fired.
Conclusion
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's features and capabilities, you should be able to implement the complex business logic of your application in a declarative manner. Drools makes learning and using declarative programming quite easy for Java developers because it has a Java semantic module to express rules in a Java/XML syntax.
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 (testRules.xml 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.
|
Download
Description |
Name |
Size |
Download method |
Sample Java project that uses Drools |
j-DroolsDemo.zip |
5KB |
HTTP |
Resources
Learn
Get products and technologies
- Drools: Download the latest Drools distribution.
- Eclipse: Download the latest version of the Eclipse IDE for the Java platform.
Discuss
About the author
|
|
|
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
|