RuleBook Grows Up: Keeping Feature-Rich Software Simple
Let's take a look at how RuleBook, a lightweight rules engine that leverages Java 8's lambda expressions, has grown in the past six months.
Join the DZone community and get the full member experience.
Join For FreeOver 6 months ago, I wrote the post RuleBook: A Simple Rules Engine That Leverages Java 8. It was created to solve a recurring problem: abstracting business logic from application logic. It was a simple business rules abstraction that used Chain of Responsibility behind a (very) simple Domain Specific Language (DSL).
RuleBook has become an exercise in making software feature rich while still staying true to its core tenet of simplicity first.
As I explained in that previous post, the existing rules engines I found just weren't meeting my needs. They were either too complex or too cumbersome to use. So, RuleBook was born as a very lightweight rules engine that got the heck out of my way and just let me do what I wanted to do: quickly and easily build business rules that were abstracted from application logic.
From the very start, the core tenet of RuleBook was simplicity. That meant rules could be defined in a commonly used programming language (Java) instead of some half-hearted XML or spreadsheet language. But above all else, it had to be easy to create rules and build RuleBooks.
Unfortunately, simplicity often conflicts with the need to add more features. Below, some of the major features added to RuleBook over the past six months are discussed, along with their approach and how simplicity was maintained as a core tenet. Be forewarned, some of the approaches described are a little unconventional. But if DZone is nothing else, it's a place where programmers can share their (sometimes unique and unconventional) approaches to solving problems. And if you would like to see the specific implementation, RuleBook is open source and the code is available on GitHub.
Feature: POJO Rules
RuleBook started out with a very simple DSL in Given-When-Then format that leveraged Java 8's lambda feature with simple method chaining. Basically, it did the job, it was indeed simple to use, and it was flexible enough for regular reuse. But what about for complex rules? This was one area where RuleBook arguably fell short with the first implementation of its DSL.
POJO rules were first introduced as a way to easily create rules that could handle complex behavior in a very familiar Spring-like structure that used Java annotations. Keeping with the Given-When-Then format, instance attributes with a @Given("{factName}") annotation injected the fact with the corresponding fact name. A method annotated with @When served as the condition for the rule. And one or more methods annotated with @Then served as the rules action that was invoked if the condition was true. POJO rules were denoted by a @Rule attribute on the class and the order in which they were executed could be specified by using the 'order' paramter on the @Rule attribute. Later enhancements would also include the ability to inject collections of facts based on the generic type specified. For more information on RuleBook POJO rules, see the README on GitHub.
Rules can do anything any other Java class can do.
With the addition of POJO rules, there were two ways to define rules: the traditional way of using chained methods with lambda expressions or adding rules as annotated POJOs to a package and having a RuleBookRunner scan the package to create, well, a RuleBook. On one hand, the power and simplicity of POJO rules really enhanced RuleBook's offering — rules could now easily do anything any other Java class could do with just a few annotations. But on the other hand, there were two different ways that rules could be defined, created and used... on the surface, anyway.
The simplification of POJO rules came in the form of its implementation.
The simplification of POJO rules came in the form of its implementation within RuleBook. Even with the addition of POJO rules, all rules used the same interfaces and practically, they all used the same rules engine, too. That's because POJO rules were implemented using the Adapter pattern, which just adapted annotated POJOs into the Rule interface. And the seemingly different RuleBookRunner that scanned a package, built the RuleBook and ran the POJO rules? It was just a Decorator that implemented the RuleBook interface and decorated an existing rules engine — the same rules engine that was already used before POJO rules became a reality. So, even though there were two seemingly divergent paths for creating rules and RuleBooks, they were really the same.
@Rule(order = 4)
public class FirstTimeHomeBuyerRule {
@Given
List < ApplicantBean > applicants; //inject facts of type ApplicantBean
@Result
private double rate; //inject the result
@When
public boolean when() {
//if any of the applicants are first time home buyers, invoke the rule's action
return
applicants.stream().anyMatch(applicant - > applicant.isFirstTimeHomeBuyer());
}
@Then
public void then() {
//first time home buyers get a 20% discount
//since 'rate' is the result, it's propigated across rules in the RuleBook
rate *= 0.80;
}
}
Figure 1: POJO rule example
Feature: Fluid Interface
After POJO rules were brought to RuleBook, it became abundantly clear that the initial simple DSL was lacking. For one thing, it wasn't fluid. Methods could be chained in any order. Sure, the methods had descriptive and intuitive names. But should you really be able to specify 'when' after 'then?' Probably not. Then there was the fact that if you wanted to define a RuleBook, you actually had to extend an existing RuleBook implementation. Why not be able to create an entire RuleBook from soup to nuts in a single statement that provided logical intellisense (via a modern IDE)?
Complex rules can literally be built in a single statement.
The basic approach for RuleBook's fluid interface was described generically in Building an Intuitive DSL in Java. With the enhancement of a fluid interface for the DSL, complex rules could literally be built in a single statement and methods could only be chained in a meaningful way. Not only did it bring a much-needed feature to RuleBook, but it also further committed RuleBook to its promise of being a simple and intuitive rules engine.
RuleBook < Double > loanRateRuleBook = RuleBookBuilder.create()
.withResultType(Double.class)
.withDefaultResult(4.5)
.addRule(rule - > rule.withFactType(ApplicantBean.class)
.when(facts - > facts.getOne().getCreditScore() < 600)
.then((facts, result) - > result.setValue(result.getValue() * 4))
.stop())
.addRule(rule - > rule.withFactType(ApplicantBean.class)
.when(facts - > facts.getOne().getCreditScore() < 700)
.then((facts, result) - > result.setValue(result.getValue() + 1)))
.addRule(rule - > rule.withFactType(ApplicantBean.class)
.when(facts - >
facts.getOne().getCreditScore() >= 700 &&
facts.getOne().getCashOnHand() >= 25000)
.then((facts, result) - > result.setValue(result.getValue() - 0.25)))
.addRule(rule - > rule.withFactType(ApplicantBean.class)
.when(facts - > facts.getOne().isFirstTimeHomeBuyer())
.then((facts, result) - > result.setValue(result.getValue() * 0.80)))
.build();
Figure 2: RuleBook built with the fluid interface example (Adapted from RuleBook: A [Slightly] More Complex Scenario)
Feature: Thread Safety
Thread safety was the feature that could have crippled the simplicity of RuleBook. Goodness knows other self-professed 'simple' rules engines have traded simplicity for [thread] safety. And to be honest, there is almost always a tradeoff. Simple to use might mean more challenging to implement. And conversely, one might choose to simplify the implementation only to shift the burden to the developers who might ultimately use their work.
Simple to use might mean more challenging to implement.
In the case of RuleBook, there were a few thread safety gotchas. First, rules can have a result and rule results chain across rules within a RuleBook. Second, POJO rules have state; it's part of what makes them easy to use. I was therefore faced with a choice: remove results as a feature and change the way POJO rules work, thereby simplifying the implementation of thread safety at the cost of a simple and intuitive interface or... absorb some of that complexity into the implementation and preserve the interface. Needless to say, it was a no-brainer. The interface would be preserved.
Preserving Results
For those unfamiliar with RuleBook, a result is basically the return value of a RuleBook after it's run. A RuleBook, of course, is a group of rules that are run together. And to go further down the terminology train, facts are the objects (data) supplied to a RuleBook that rules can use to determine whether or not their action(s) should be invoked. Rules can also add facts that are then propagated to other rules. And they can change existing facts or perform actions using facts.
Given that, facts are passed to a RuleBook when it's run. So, the state of facts is not stored in a RuleBook (or a rule). And as a consequence, they do not factor into a RuleBook's thread safety. A result, however, is a different animal. A result is maintained within a RuleBook and chained across rules. So, its state belongs to the RuleBook. This creates a problem for thread safety.
The solution was basically to maintain a result per thread.
The solution to preserving results was basically to have RuleBooks maintain a result per thread, with appropriate locking of supporting critical sections of code. This led to an interesting side effect with thread pools, which actually turned out to be a relatively minor issue. When a thread is in a thread pool, it can be reused. However, it won't be reused say, in the middle of a RuleBook run. So, in the worst case, a result for a given RuleBook might be carried over from one run to the next. The solution was just to reset the result to its initial default value between subsequent runs.
Making POJO Rules Thread Safe
As I alluded to above, an easy solution to POJO rule thread safety would be to just not allow POJO rules to maintain state. I could have changed the way RuleBook was used by doing something like passing everything into methods in POJO rules instead of allowing facts and results to be injected as instance attributes. But in my opinion, that makes it harder to use RuleBook, not easier. Sure, it would be easier for me to implement. But it breaks the core tenet of RuleBook: simplicity.
If I couldn't prevent state from being stored, I could ensure that it wasn't used across threads.
Instead of removing state from POJO rules, I thought about how the existing POJO rules could be made thread safe. The sad answer was that they couldn't. But the interesting thing was that each POJO rule was only needed for a single run. So, if I couldn't prevent state from being stored in POJO rules, I could ensure that POJO rules weren't used across threads by discarding them after each run.
The consequence of discarding POJO rules after each run was that subsequent RuleBook runs would have to rebuild all of the POJO rules that comprised the RuleBook. That wasn't a resource intensive task, but it was still a little extra overhead, just the same. But it was a small price to pay to maintain the existing simplicity of POJO rules.
Next Up
There are still some major features that are on deck for RuleBook: enhanced support for Groovy, better support for externalizing rules (perhaps without JRebel, SpringLoaded or DCEVM), more packaged rules engines and built-in rules versioning. The challenge then continues to be keeping the software simple to use and light weight while at the same time providing an increasingly robust feature set. Inevitably, some enhancements will become other repos/projects. Other features will be streamlined within the project. But one thing is certain, the most important feature will remain its ease of use.
Summary
Any software that is enhanced over time will ultimately face trade-offs between simplicity, performance and feature richness. Which path to take should be largely based on the goals of the software. Some software favors a large feature set above all else. Some software elevates performance as its primary benefit. For RuleBook, the goal is now and has always been simplicity. Sure, performance and feature richness are also important, but not equally so. And in some cases, reasonable sacrifices were made for the benefit of maintaining simplicity. Certainly, the approaches described for implementing the above features reflected that. If nothing else, perhaps they also helped you think a little more about the trade-offs different approaches might have in your own software development experiences.
Opinions expressed by DZone contributors are their own.
Comments