Decision service business rules in JBoss Rules
Decision service business rules in JBoss Rules
This article describes the kinds of business rules that you might implement in a decision service, with a simple example; this is essentially a functional design.
This article is part 2 of a series:
-
link: https://blog.lunatech.com/posts/2009-12-14-decision-service-architecture-jboss-rules[Decision service architecture with JBoss Rules] - what a decision service is and gave a high-level technical overview how you can use the JBoss Rules Execution Server to build one
-
Decision service business rules in JBoss Rules - this article
-
link:/2010/01/04/how-build-decision-service-using-jboss-rules-execution-server[How to build a decision service using JBoss Rules Execution Server] - how to get this working, with example rules - a RESTful decision service with no Java code required.
Functional requirements
For this example we are going to write business rules for desktop PC configuration, to determine which components can be selected when building a custom PC. The decision service will implement the following business rules:
-
The user must select a motherboard type, a processor type.
-
The user must select memory modules with specific sizes.
-
An empty selection will result in an error message.
-
The selected motherboard type and processor type must be present in a pre-defined list.
-
The selected memory module size must be present in a pre-defined list.
-
The selected processor's socket type must match the motherboard's processor socket type.
-
The selected number of memory modules must not exceed the motherboard's number of memory sockets.
-
Memory modules must be selected in pairs of matching sizes.
-
If the selection violates one of these rules, a message must be generated.
-
The decision service will provide lists of available components: motherboards, processors and memory modules.
-
The lists of available components will only include components that are compatible with any existing selections.
Decision service functionality
The requirements above mean that the decision service will need to provide the following functionality.
-
Define lists of available components.
-
Generate lists of the remaining available components.
-
Generate result messages.
-
Validate that each selection is not empty.
-
Validate that each selection is in the list of available components.
-
Filter available components based on previous selections.
-
Perform additional ad-hoc validations.
Data model
For this example, we shall use the following data model. This includes the input data for the user selections, the reference data for the items in the lists of available components, and additional output data.
Selection has properties for the selected motherboard type and processor type. In addition, there is a separate MemorySelection for each memory module selected.
The reference data types Motherboard, Processor and MemoryDimmeach represent a different version of each component, with properties for their different characteristics. The Motherboard and Processortype properties are the identifiers specified in the Selection, while each type of MemoryDimm is identified by size, matching the size in the MemorySelection.
The output data consists of text messages. The reference data will also be included in the output data, to represent the lists of available components.
Business rules in the Drools Rule Language
Lists of available components
Now it is time to look at the rules code, starting with the lists of available components.
First, I am going to assume that the reference data objects have already been inserted into the rules session's working memory, to provide the facts that the rules will use to reason about the user's selection. In this example, we can use additional rules to insert the facts from within the rules session:
rule "Insert motherboards"
when
not Message(text == "Motherboards inserted")
then
Motherboard integrated = new Motherboard();
integrated.setType("integrated");
integrated.setSocketType("none");
integrated.setMemorySockets(0);
insert(integrated);
Motherboard standard = new Motherboard();
standard.setType("standard");
standard.setSocketType("pga");
standard.setMemorySockets(2);
insert(standard);
insertLogMessage(drools, "Motherboards inserted");
end
In practice, however, you would be more likely to do this using the rule session's Java API:
final List<Command> commands = new ArrayList<Command>();
final List<Motherboard> motherboards = getMotherboards();
commands.add(CommandFactory.newInsertElements(motherboards);
final StatelessKnowledgeSession session = knowledgeBase.newStatelessKnowledgeSession();
session.execute(CommandFactory.newBatchExecution(commands));
This means that the following rule would be activated - for example, there are motherboards facts:
rule "Motherboard reference data loaded"
when
$motherboard : Motherboard()
then
System.out.println("Found motherboard: " + $motherboard);
end
To make the lists of these components available as output data, we define queries:
query "motherboards"
value : Motherboard()
end
Result messages
Another piece of functionality we need is to generate result messages. For this, we define a new JavaBean type inline in the rules file that has properties for the message text, and a message type that we can use to identify which kinds of messages to include in the output:
declare Message
type : String
text : String
end
We can now use this new type in rules. For example, the following rule inserts a new message "Found first motherboard" when there is aMotherboard fact in working memory. This only happens once, because the left-hand side also checks that the message itself is not yet in working memory.
rule "First motherboard reference data loaded"
when
Motherboard()
not Message(text == "Found first motherboard")
then
Message message = new Message();
message.setType("DEBUG");
message.setText("Found first motherboard");
insert(message);
end
Since the Message type only has a default constructor, it is somewhat verbose to insert the message; it is more convenient to define a function in the rules file:
import org.drools.spi.KnowledgeHelper
function void insertDebugMessage(KnowledgeHelper drools, String text) {
Message message = new Message();
message.setType("DEBUG");
message.setText(text);
drools.insert(message);
}
To make a certain type of messages available in the output, we just define another query:
query "messages"
value : Message(type == "RESULT")
end
Validating user selections
The user selections are String properties in the Selection type. The first validation is simply to check that the selection is not empty:
rule "No motherboard selected"
when
Selection(motherboardType == null)
then
insertMessage(drools, "No motherboard selected");
end
In general, a good way to name a rule is to summarise the condition that its left-hand side represents - the same kind of self-documentation as good method names in Java. However, in the previous validation rule this means that the message duplicates the rule name, which is bad. We can easily avoid the duplication by adding another utility function that gets the rule name from the drools helper object:
function void insertRuleNameMessage(KnowledgeHelper drools) {
insertMessage(drools, drools.getRule().getName());
}
Next, using the new insertRuleNameMessage function, the selection'smotherboardType should match the type property value of an available motherboard:
rule "Selected motherboard type does not exist"
when
Selection($type : motherboardType != null)
not Motherboard(type == $type)
then
insertRuleNameMessage(drools);
end
Filtering available components
So far the validation rules have not been very interesting, in the sense that they would be just as easy to implement in Java. However, things get more interesting if we start changing which facts are in working memory.
In PC configuration, selecting one component may affect what you may choose for another component. In our example, selecting a particular processor rules out motherboards with an incompatible processor socket.
rule "Filter motherboards for selected processor socket type"
when
Selection($processor : processorType != null)
Processor(type == $processor, $socket : socketType)
$motherboard : Motherboard(socketType != $socket)
then
retract($motherboard);
end
This rule has three left-hand side conditions. First, the selection must specify a processor type, which is bound to the $processor variable. Second, there must be an available processor that has the selected processor type; its socket type is also bound to a variable. Finally, there is a motherboard that has a different socket type, which is also bound to a variable. This rule matches against each such motherboard, and the right-hand side removes the matched motherboard from working memory, filtering the list of available motherboards.
The interesting thing about this rule is that as well as filtering the list of motherboards that are returned by the motherboards query defined above, this affects which motherboards are available for theSelected motherboard type does not exist rule. The selected motherboard type might initially have been in the list of available motherboards before being filtered out, resulting in the message "Selected motherboard type does not exist".
A crucially important thing to consider when implementing these kinds of rules is that you do not have to care about what order these things happen in - you do not have to think about making sure the filtering happens first. This is because when the filtering rule modifies working memory by retracting the motherboard, the rules engine automatically re-evaluates the validation rule's not Motherboard(type == $type)condition, which may now be true.
In a more realistic example, there would be many more complex dependencies between components, such as powerful graphics cards requiring a second or larger power supply, which in turn means needing a larger physical case.
Ad-hoc validations
Beyond the kinds of basic validations described above, which apply to all kinds of selections, a real-world problem will always have additional validations that do not fit into any kind of pattern. This is where you get the most benefit from using a rules engine, because each special case can just be an additional rule that uses the same working memory data as other rules.
For example, a special rule for memory modules is that they must be selected in matched pairs of the same capacity. In other words, there must be an even number of each size selected. In our model, each individual memory module is a separate MemorySelection fact, so we count them using the built-in collect function:
import java.util.ArrayList
rule "Memory must be selected in matching pairs"
when
MemorySelection($selectedDimmSize : dimmSize)
ArrayList($quantitySelected : size) from collect( MemorySelection(dimmSize == $selectedDimmSize) )
eval($quantitySelected % 2 != 0)
then
insertRuleNameMessage(drools);
insertMessage(drools, $quantitySelected + " x " + $selectedDimmSize + "GB DIMMs selected");
end
Again, there are three left-hand side conditions. The first condition matches against a selected memory module, and binds its size to a variable. The second condition uses the collect function to collect all MemorySelection facts that have that size into ajava.util.ArrayList, and binds the number of facts in the list (the quantity of selected memory modules) to a variable. The third condition then evaluates a Java expression that is true when the quantity is an odd humber.
The rule inserts the rule name as a validation message, as usual, as well as an additional message that indicates which size was not selected in matched pairs.
One problem with this version of this rule is that it generates duplicate messages. Suppose that the selection includes threeMemorySelection facts with size 8GB. The rule's second condition will get the value 3 and the third condition will be true because three is odd. However, the first condition will cause the rule to be activated three times, once for each of the three MemorySelection facts, which means that the right-hand side will execute three times. One way to solve this would be to add a condition that the message "3 x 8GB DIMMs selected" is not in working memory. Alternatively, in practice, theMemorySelection facts might be ordered in some way so that you can add a condition that only matches on the 'first' one.
Next steps
Once you have written some business rules for your decision service, the next step is obviously to run them and test them. The simplest way to do this is to configure the JBoss Rules http://downloads.jboss.com/drools/docs/5.0.1.26597.FINAL/drools-guvnor/html/ch01.html#d0e1095[Execution Server] to load the rules file, so that you can execute the rules using its web services interface.