What is the an effective design pattern/style for designing a rule engine in Java?
I am implementing a rule-engine in Java. My rule-engine predefines a list of independent rules and rule sets. A rule here is simply a piece of logic. And a rule set combines these simple rules into an ordered set.
I am a decent java developer but not a Guru. My colleague suggested me two designs for this purpose. I am not satisfied with both the designs, hence this question.
Example of a Rule in my project: Say the inputs are locations in USA for e.g., Santa Barbara, CA, USA or OH, US which is usually in some well defined format with the city, state and country fields. Then I can have some rules as follows:
RULE 1: City not null
RULE 2: State not null
RULE 3: Country equals US or USA
RULE 4: State length equals 2
Example of a RuleSet in my project:
RULESET: Valid location This ruleset is an ordered set of the above defined rules.
The two design templates I have implemented are as follows:
Design 1: Using Enum with Anonymous Inner classes
Rule.java
public interface Rule {
public Object apply(Object object);
}
NlpRule.java
public enum NlpRule {
CITY_NOT_NULL(new Rule() {
@Override
public Object apply(Object object) {
String location = (String) object;
String city = location.split(",")[0];
if (city != null) {
return true;
}
return false;
}
}),
STATE_NOT_NULL(new Rule() {
@Override
public Object apply(Object object) {
String location = (String) object;
String state = location.split(",")[1];
if (state != null) {
return true;
}
return false;
}
}),
COUNTRY_US(new Rule() {
@Override
public Object apply(Object object) {
String location = (String) object;
String country = location.split(",")[2];
if (country.equals("US") || country.equals("USA")) {
return true;
}
return false;
}
}),
STATE_ABBREVIATED(new Rule() {
@Override
public Object apply(Object object) {
String location = (String) object;
String state = location.split(",")[1];
if (state.length() == 2) {
return true;
}
return false;
}
});
private Rule rule;
NlpRule(Rule rule) {
this.rule = rule;
}
public Object apply(Object object) {
return rule.apply(object);
}
}
RuleSet.java
public class RuleSet {
private List<NlpRule> rules;
public RuleSet() {
rules = new ArrayList<NlpRule>();
}
public RuleSet(List<NlpRule> rules) {
this.rules = rules;
}
public void add(NlpRule rule) {
rules.add(rule);
}
public boolean apply(Object object) throws Exception {
boolean state = false;
for (NlpRule rule : rules) {
state = (boolean) rule.apply(object);
}
return state;
}
}
RuleSets.java
public class RuleSets {
private RuleSets() {
}
public static RuleSet isValidLocation() {
RuleSet ruleSet = new RuleSet();
ruleSet.add(NlpRule.CITY_NOT_NULL);
ruleSet.add(NlpRule.STATE_NOT_NULL);
ruleSet.add(NlpRule.COUNTRY_US);
ruleSet.add(NlpRule.STATE_ABBREVIATED);
return ruleSet;
}
}
Main.java
public class Main {
public static void main(String... args) {
String location = "Santa Barbara,CA,USA";
RuleSet ruleSet = RuleSets.isValidLocation();
try {
boolean isValid = (boolean) ruleSet.apply(location);
System.out.println(isValid);
} catch (Exception e) {
e.getMessage();
}
}
}
Design 2: Using Abstract Class
NlpRule.java
public abstract class NlpRule {
public abstract Object apply(Object object);
public final static NlpRule CITY_NOT_NULL = new NlpRule() {
public Object apply(Object object) {
String location = (String) object;
String city = location.split(",")[0];
if (city != null) {
return true;
}
return false;
}
};
public final static NlpRule STATE_NOT_NULL = new NlpRule() {
public Object apply(Object object) {
String location = (String) object;
String city = location.split(",")[0];
if (city != null) {
return true;
}
return false;
}
};
public final static NlpRule COUNTRY_US = new NlpRule() {
public Object apply(Object object) {
String location = (String) object;
String country = location.split(",")[2];
if (country.equals("US") || country.equals("USA")) {
return true;
}
return false;
}
};
public final static NlpRule STATE_ABBREVIATED = new NlpRule() {
public Object apply(Object object) {
String location = (String) object;
String state = location.split(",")[1];
if (state.length() == 2) {
return true;
}
return false;
}
};
}
RuleSet.java
public class RuleSet {
private List<NlpRule> rules;
public RuleSet() {
rules = new ArrayList<NlpRule>();
}
public RuleSet(List<NlpRule> rules) {
this.rules = rules;
}
public void add(NlpRule rule) {
rules.add(rule);
}
public boolean apply(Object object) throws Exception {
boolean state = false;
for (NlpRule rule : rules) {
state = (boolean) rule.apply(object);
}
return state;
}
}
RuleSets.java
import com.hgdata.design.one.NlpRule;
import com.hgdata.design.one.RuleSet;
public class RuleSets {
private RuleSets() {
}
public static RuleSet isValidLocation() {
RuleSet ruleSet = new RuleSet();
ruleSet.add(NlpRule.CITY_NOT_NULL);
ruleSet.add(NlpRule.STATE_NOT_NULL);
ruleSet.add(NlpRule.COUNTRY_US);
ruleSet.add(NlpRule.STATE_ABBREVIATED);
return ruleSet;
}
}
Main.java
public class Main {
public static void main(String... args) {
String location = "Santa Barbara,CA,USA";
RuleSet ruleSet = RuleSets.isValidLocation();
try {
boolean isValid = (boolean) ruleSet.apply(location);
System.out.println(isValid);
} catch (Exception e) {
e.getMessage();
}
}
}
Better Design Approach/Pattern ? As you can see, design 2 gets rid of the interface and enum. It instead uses an abstract class. I am still wondering if there is a better design pattern/approach to implement the same.
Instantiation using initializer blocks:
Now in case of both designs above. Say, if I need to instantiate an external class to use it inside my apply logic, then I am forced to use initializer blocks which I am not totally aware whether is a good practice. See example for such a scenario below:
Design 1:
...
STATE_ABBREVIATED(new Rule() {
private CustomParser parser;
{
parser = new CustomParser();
}
@Override
public Object apply(Object object) {
String location = (String) object;
location = parser.parse(location);
String state = location.split(",")[1];
if (state.length() == 2) {
return true;
}
return false;
}
});
...
Design 2:
...
public final static NlpRule STATE_ABBREVIATED = new NlpRule() {
private CustomParser parser;
{
parser = new CustomParser();
}
public Object apply(Object object) {
String location = (String) object;
location = parser.parse(location);
String state = location.split(",")[1];
if (state.length() == 2) {
return true;
}
return false;
}
};
...
Java experts please cast some light! Also please pinpoint if you find any flaws in the above two designs. I need to know the pros and cons associated with each of the designs to help me make the right decision. I am looking into lambdas, predicates and several other patterns as suggested by some users in the comments.