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.


答案 1

This is an interesting question with many possible answers. To some extent the solution is going to depend on personal preference. I have often come across similar problems and have the following recommendations. Note that these work for me but might not suit your needs.

  1. Use . In the long term I feel they have a lot of advantages over members in terms of their error checking and the useful containers (EnumSet etc.) that can use them efficiently.enumprivate static

  2. Use interfaces over abstract classes. Before Java 8 there were useful reasons to use abstract classes. With members there are now no good reasons (just my opinion - I'm sure others will disagree). An enum can implement an interface.default

  3. In Java 8 the logic associated with each 'rule' can be embedded in a lambda expression which makes the initialization code for your enums clearer.

  4. Keep lambdas very short - just one or two commands at the most (and preferably one expression without a block). This means splitting any complex logic into a separate methods.

  5. Use separate enums to classify your rules. There's no good reason to put them all into one and by splitting them out you can make the constructors simple by having exactly the lambda expressions relevant to their domain. See my example below to see what I mean.

  6. If you have hierarchies of rules, use the composite design pattern. It's flexible and robust.

So putting those recommendations together I would suggest something like:

interface LocationRule{
    boolean isValid(Location location);
}

enum ValidValueRule implements LocationRule {
    STATE_NOT_NULL(location -> location.getState() != null),
    CITY_NOT_NULL(location -> location.getCity() != null);

    private final Predicate<Location> locationPredicate;
    ValidValueRule(Predicate<Location> locationPredicate) {
        this.locationPredicate = locationPredicate;
    }

    public boolean isValid(Location location) {
        return locationPredicate.test(location);
    }
}

enum StateSizeRule implements LocationRule {
    IS_BIG_STATE(size -> size > 1000000),
    IS_SMALL_STATE(size -> size < 1000);

    private final Predicate<Integer> sizePredicate;
    StateSize(Predicate<Integer> sizePredicate) {
        this.sizePredicate = sizePredicate;
    }
    public boolean isValid(Location location) {
        return sizePredicate.test(location.getState().getSize());
    }
}

class AllPassRule implements LocationRule {
    private final List<LocationRule > rules = new ArrayList<>();
    public void addRule(LocationRule rule) {
        rules.add(rule);
    }
    public boolean isValid(Location location) {
        return rules.stream().allMatch(rule -> rule.isValid(location));
    }
}

class AnyPassRule implements LocationRule {
    private final List<LocationRule > rules = new ArrayList<>();
    public void addRule(LocationRule rule) {
        rules.add(rule);
    }
    public boolean isValid(Location location) {
        return rules.stream().anyMatch(rule -> rule.isValid(location));
    }
}

class NegateRule implements LocationRule {
    private final Rule rule;
    public NegateRule(Rule rule) {
        this.rule = rule;
    }
    public boolean isValid(Location location) {
        return !rule.isValid(location);
    }
}

So, for example, to implement a rule that locations must either be in a city or in a state that isn't small:

AnyPassRule cityOrNonSmallState = new AnyPassRule();
cityOrNonSmallState.addRule(ValidValueRule.CITY_NOT_NULL);
cityOrNonSmallState.addRule(new NegateRule(StateSize.IS_SMALL_STATE));
return cityOrNonSmallState.isValid(location);

答案 2

There's lots of (open source) Java rule engines out there already - check out http://java-source.net/open-source/rule-engines & http://drools.org/

You could start with using/examining the source for one of those (taking note of where it doesn't meet your requirements) and go from there.