April 12, 2007, 8:05 a.m.
posted by oxy
Item 26: Prefer rules engines for complex state evaluation and executionFrequently, business or domain logic reaches degrees of complexity that are difficult, if not outright impossible, to express in imperative languages like Java. Purists will no doubt scoff at the idea that there's anything that the Almighty Object can't do, but the basic fact is, business rules don't always fit neatly into object-oriented categories, and we end up creating Blobs [Malveau, 73] with "validate" or "process" methods that encompass these rules—in essence, falling back on procedural habits for lack of something better. Consider for a moment the computer on your desk and all the possible variations of hardware combinations that the manufacturer offers for that one model—memory, hard drive, monitor, not to mention all the peripherals—and the inevitable incompatibilities encompassed therein. ("If they order the DVD, then we need to make sure they didn't get the KorSplatt 5900 video card, because it won't work with this DVD model, unless of course they want the combo CD-RW/DVD instead. Oh, and the KorSplatt 5900 won't run in a machine that has less than 512MB of RAM, unless it's SuperReallyFastRAM....") Factor in all the possible promotionals that the company will want to run on top of the basic incompatibility restrictions, plus the fact that all this stuff changes on a monthly (if not weekly!) basis, and suddenly the idea of trying to create a "Computer Configurator" for an online PC manufacturer makes the most hardened IT veteran consider retirement. The problem here is that this kind of complex evaluation is hard to do in an imperative language like Java, where the language focuses on step after step for the CPU to carry out. In essence, we're telling the machine how to do its job, and that means we have to be very explicit about the conditions that need to be evaluated and the order in which they need to be considered. It leads to complex and hard-to-maintain code like this:
if (currentPC.drives().contains("DVD"))
{
if (currentPC.videoCard().equals("KorSplatt 5900") &&
!(currentPC.drives().get("DVD").equals("CD-RW/DVD")))
{
warn("DVD incompatible with KorSplatt 5900");
}
}
else if (currentPC.videoCard().equals("KorSplatt 5900") &&
currentPC.memory() < 512)
{
// . . .
}
We can talk about trying to abstract out higher-order incompatibilities and refactor them into more generic base-class-oriented code, but the truth remains that in many cases, businesses create product-targeted rules that make no sense to engineers but help drive business up 5% for the quarter. This kind of processing is a specific form of programming, called rules-based programming, and fortunately there are rules engines, software that can examine a set of data, look at the list of rules declared within the engine, and infer what rules need to fire in response to the current state of the data. More importantly, the rules engine can then reapply the rules as necessary as the data changes, until the data reaches a stable state that triggers no further rules. Rules-oriented implementations have been around for years and are becoming more popular in consumer use. The ubiquitous sendmail program for handling e-mail uses a cryptic set of rules to decide how to forward mail based on the contents of each message. Spam filters use rules to evaluate whether an incoming mail message is from your mother wishing you a happy birthday or an anonymous e-mail address trying to sell you love potions. By the way, lest you think this is a spurious application, one of the first and most famous rules-based applications was Digital Equipment Corporation's XCON system, which did exactly this: it helped DEC sales consultants configure DEC mainframe computer orders, to ensure the order had all the necessary components and accessories.[1] By 1989, XCON in cluded over 17,000 rules and knew about 31,000 hardware requirements, saving DEC an estimated $40 million annually due to increased accuracy, reduced testing costs, and higher customer satisfaction. And on the off chance you're not in the business of selling computers, rules-based systems are useful for a wide variety of other domain-oriented tasks, including "customer recommendations" based on previous purchasing history, something that marketing departments everywhere are starting to ask for in public-facing purchasing systems. In fact, with a little bit of analysis, it's hard to find an IT system in place that doesn't have some kind of rules-based processing in it.
Rules engines typically serve two purposes: (1) to capture business rules in a first-class fashion, and (2) to allow those rules to be modified with out requiring recoding of the Java code itself. The first is typically the biggest gain, since trying to track down business rules within a pile of if/then/else statements is not only difficult but also error-prone. If your users are sophisticated enough, however, teaching them the "rules language" the rules engine understands has the added benefit of giving them the ability to modify a substantial part of the business logic of the application, effectively taking the programmer out of the loop on business logic changes that would otherwise require a complete development cycle (development, testing, QA, release, deployment, and so on). Several different implementations are available: one, an open-source implementation called drools, is available from http://www.codehaus.org; BEA includes one as part of its WebLogic platform, and ILOG sells a third-party engine for integration with other servers. JSR 94 defines a standard API (in the package javax.rules) for interfacing with rules engines; its reference implementation, called the Java Expert System Shell (JESS), is freely available for noncommercial use. Explaining the complete JESS language and API is far beyond the scope of what we can cover here; for more details, consult JESS in Action [Friedman-Hill]. Using a JSR-94-compliant rules engine follows the same basic principles as any other J2EE resource, in that you start by obtaining an instance of a RuleServiceProvider through a JNDI lookup. Initializing the rules engine requires creating a RuleExecutionSet, a loaded set of rules ready to be executed. Register the RuleExecutionSet with a RuleAdministrator instance, and you're ready to go:
RuleServiceProvider provider = // . . .
// Either use a JNDI lookup, or else use
// RuleServiceProviderManager, similar in concept
// to the JDBC DriverManager
RuleAdministrator admin = provider.getRuleAdministrator();
RuleExecutionSet ruleSet = null;
// Load the rules from a local file using
// LocalRuleExecutionSetProvider
//
FileReader reader = new FileReader("rules.xml");
try
{
HashMap props = new HashMap();
props.put("name", "Configuration Rules");
props.put("description",
"Rulebase for company PC configuration");
LocalRuleExecutionSetProvider lresp =
admin.getLocalRuleExecutionSetProvider(props);
ruleSet = lresp.createRuleExecutionSet(reader, props);
}
finally
{
reader.close();
}
admin.registerRuleExecutionSet("rules", ruleSet, props);
The rules.xml file, as its name implies, contains the rules in an XML-oriented format. The JSR-94 Specification makes no mandate as to the rules language itself; the XML format is entirely specific to the reference implementation, based on JESS. Other rules engines may also offer an XML-like format or use something else entirely. Once the engine is initialized, using it at runtime is a fairly straight forward exercise. JSR-94 defines two kinds of RuleSession types, stateful and stateless, roughly corresponding to the difference between stateful and stateless session beans in EJB: a stateful RuleSession has its working memory preserved across calls, whereas a stateless one doesn't. This means that objects that are added to working memory (for the rules to evaluate against) are lost in the stateless RuleSession after the rules are evaluated:
//Create the RuleSession instance
//
RuleRuntime runtime = rep.getRuleRuntime();
StatefulRuleSession srs =
(StatefulRuleSession)runtime.createRuleSession("rules",
props,
RuleRuntime.STATEFUL_SESSION_TYPE);
// Populate the RuleSession's working memory with the data
// to evaluate against
//
srs.addObject(new Integer(12));
srs.addObject("Hello, world");
srs.addObject(new CustomDataObject());
// Execute the rules
//
srs.executeRules();
// Examine the entire contents of the working
// memory—objects may have been modified by
// executing rules
//
List results = srs.getObjects();
// Release the RuleSession
//
srs.release();
Depending on the contents of the rules themselves, the results List will contain modified objects that were created/modified by the body of the rules in the rules.xml file. Rules engines, when used appropriately, can be a powerful way to express business logic in such a way that doesn't require piles of if/then/else statements; it is, however, yet another language to learn, and in the situations where your business analysts or end users either can't or won't learn the language, that task then falls on your shoulders. Much like SQL and XSLT, most rule languages are declarative in nature, meaning you don't bother specifying how so much as when and what—that is, you specify some kind of predicate condition that triggers the rule and the action to take when the rule is triggered. For example, in a completely fabricated XML-like rule language, a rule for the PC Configurator might look like the code shown here, where the condition element is some form of XPath-like query syntax and the action element is some kind of script language.
<rule>
<condition>(drives[@type="DVD" and @type!="CD-RW/DVD"] > 1) and
(video[@mfr="KorSplatt"] and
video[@version="5900"])
</condition>
<action>
put("KorSplatt 5900 is incompatible with regular DVD")
</action>
</rule>
While it won't necessarily replace objects as the principal general-purpose language we use to build enterprise systems, a rules engine can certainly make your life a lot easier when working with complicated conditional logic. Although JSR-94 isn't a formal part of the J2EE Specification collection, it was designed primarily for J2EE applications, and many of the existing rules vendors were part of the Expert Group. At the very least, it's something to keep your eyes on. |
- Comment