Quel est le modèle/style de conception efficace pour concevoir un moteur de règles en Java?


J'implémente un moteur de règles en Java. Mon moteur de règles prédéfinit une liste de règles et d'ensembles de règles indépendants. Une règle ici est simplement un morceau de logique. Et un ensemble de règles combine ces règles simples dans un ensemble ordonné.

Je suis un développeur java décent mais pas un gourou. Mon collègue m'a suggéré deux modèles à cet effet. Je ne suis pas satisfait des deux conceptions, d'où cette question.

Exemple de règle dans mon projet: Disons que les entrées sont des emplacements aux États-Unis pour par exemple, Santa Barbara, CA, USA ou OH, US qui est généralement dans un format bien défini avec les champs ville, État et pays. Ensuite, je peux avoir quelques règles comme suit:

RÈGLE 1: Ville non nulle
RÈGLE 2: État non nul
RÈGLE 3: Le pays est égal aux États-Unis ou aux États-Unis
RÈGLE 4: La longueur de l'état est égale à 2

Exemple d'un jeu de règles dans mon projet:

JEU DE RÈGLES: Emplacement valide Cet ensemble de règles est un ensemble ordonné de ce qui précède des règles définies.

Les deux modèles de conception que j'ai mis en œuvre sont les suivants:

Conception 1: Utilisation d'Enum avec des classes internes anonymes

La Règle.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);
    }
}

Jeu de règles.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;
    }
}

Jeux de Règles.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;
    }
}

Principal.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();
        }
    }
}

Conception 2: Utilisation du résumé Classe

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;
        }

    };

}

Jeu de règles.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;
    }
}

Jeux de Règles.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;
    }
}

Principal.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();
        }
    }
}

Meilleure Approche/Modèle de Conception ? Comme vous pouvez le voir, design 2 se débarrasse de l'interface et de l'énumération. Il utilise plutôt une classe abstraite. Je me demande toujours s'il existe un meilleur modèle/approche de conception pour implémenter la même chose.

Instanciation à l'aide de l'initialiseur blocs:

Maintenant dans le cas des deux dessins ci-dessus. Disons que si j'ai besoin d'instancier une classe externe pour l'utiliser dans ma logique d'application, je suis obligé d'utiliser des blocs d'initialisation dont je ne suis pas totalement conscient si c'est une bonne pratique. Voir l'exemple d'un tel scénario ci-dessous:

Conception 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;
        }

    });
...

Conception 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;
        }

    };
...

Les experts Java jettent un peu de lumière! Veuillez également identifier si vous trouvez des défauts dans les deux conceptions ci-dessus. J'ai besoin de connaître les avantages et les inconvénients associés à chacune des dessins pour m'aider à prendre la bonne décision. Je regarde les lambdas, les prédicats et plusieurs autres modèles comme suggéré par certains utilisateurs dans les commentaires.

Author: Shankar, 2015-05-25

5 answers

C'est une question intéressante avec de nombreuses réponses possibles. Dans une certaine mesure, la solution va dépendre de la préférence personnelle. J'ai souvent rencontré des problèmes similaires et ont les recommandations suivantes. Notez que ceux-ci fonctionnent pour moi mais pourraient ne pas répondre à vos besoins.

  1. Utiliser enum. À long terme, je pense qu'ils ont beaucoup d'avantages par rapport aux membres private static en termes de vérification des erreurs et de conteneurs utiles (EnumSet, etc.) qui peuvent les utiliser efficacement.

  2. Utilisez des interfaces sur des classes abstraites. Avant Java 8, il y avait des raisons utiles d'utiliser des classes abstraites. Avec les membres default, il n'y a plus de bonnes raisons (juste mon opinion - je suis sûr que les autres seront en désaccord). Une énumération peut implémenter une interface.

  3. En Java 8, la logique associée à chaque "règle" peut être intégrée dans une expression lambda qui rend le code d'initialisation de vos énumérations plus clair.

  4. Gardez les lambdas très courts - un seul ou deux commandes au maximum (et de préférence une expression sans bloc). Cela signifie diviser toute logique complexe en méthodes distinctes.

  5. Utilisez des énumérations distinctes pour classer vos règles. Il n'y a aucune bonne raison de les mettre tous en un seul et en les divisant, vous pouvez simplifier les constructeurs en ayant exactement les expressions lambda pertinentes pour leur domaine. Voir mon exemple ci-dessous pour voir ce que je veux dire.

  6. Si vous avez des hiérarchies de règles, utilisez le composite modèle de conception. Il est flexible et robuste.

Donc, en rassemblant ces recommandations, je suggérerais quelque chose comme:

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);
    }
}

Ceux-ci peuvent être utilisés comme:

AnyPassRule anyPass = new AnyPassRule();
anyPass.addRule(ValidValueRule.STATE_NOT_NULL);
anyPass.addRule(new NegateRule(StateSize.IS_SMALL_STATE));
anyPass.isValid(location);

Et ainsi de suite.

 6
Author: sprinter, 2015-05-25 04:48:18

Il y a déjà beaucoup de moteurs de règles Java (open source) - consultez http://java-source.net/open-source/rule-engines & http://drools.org/

Vous pouvez commencer par utiliser/examiner la source pour l'un de ceux-ci (en prenant note de l'endroit où il ne répond pas à vos besoins) et aller à partir de là.

 4
Author: eddiewould, 2015-05-25 03:41:43

Une autre réponse possible est d'utiliser un analyseur DSL pour valider votre règle, dans le langage de programmation de fonction, il existe une chose appelée parser combinator qui pourrait construire un analyseur plus grand(ensemble de règles) à partir de différents analyseurs de base(règle). Le bon point de cette façon est la flexibilité, les inconvénients sont que chaque fois que vous voulez changer votre jeu de règles, vous devez re-coder.

 3
Author: Jade Tang, 2015-05-25 05:53:19

Interface avec les champs statiques:

public interface NlpRule 
{
    Object apply(Object object);

    NlpRule CITY_NOT_NULL = object ->
    {
        String location = (String) object;
        String city = location.split(",")[0];
        return ...true/false;
    };

    // etc. 

Certains peuvent préférer les méthodes aux objets fonctionnels

public interface NlpRule 
{
    Object apply(Object object);

    static boolean cityNotNull(Object object) // java8: static method in interface
    {
        String location = (String) object;
        String city = location.split(",")[0];
        return ...true/false;
    };

    // etc. 

}

// use method reference as functional object

NlpRule rule = NlpRule::cityNotNull;

ruleset.add( NlpRule::cityNotNull );

Ou vous pourriez avoir à la fois la méthode et le champ

public interface NlpRule 
{
    Object apply(Object object);

    NlpRule CITY_NOT_NULL = NlpRule::cityNotNull;
    static boolean cityNotNull(Object object)
    {
        ...
    };

Les règles d'exemple sont toutes String->boolean, je ne sais pas pourquoi NlpRule est Object->Object. Si les règles peuvent en effet accepter / renvoyer différents types, vous devriez probablement generify NlpRule<T,R>.


Le CustomParser peut être stocké dans une classe d'assistance privée package

class NlpRuleHelper
{
    static final CustomParser parser = new CustomParser();
}

--

public interface NlpRule
...
    NlpRule STATE_ABBREVIATED = object -> 
    {
         ...
         location = NlpRuleHelper.parser.parse(location);
 3
Author: ZhongYu, 2015-05-25 05:57:09

Je pense que vous avez ajouté au moins une couche de code inutile. Rappelez-vous que enum peut également implémenter des interfaces et peut même avoir des méthodes abstraites.

/**
 * I don't like `Object` so I will adjust.
 */
public interface Rule {

    public boolean pass(String s);
}

/**
 * All pass country codes.
 */
public enum ValidCountry {

    US, USA;
    public static Set<String> all = new HashSet<>();

    static {
        for (ValidCountry c : ValidCountry.values()) {
            all.add(c.name());
        }
    }
}

public enum NlpRule implements Rule {

    CITY_NOT_NULL {

                @Override
                public boolean pass(String location) {
                    return location.split(",")[0] != null;
                }

            },
    STATE_NOT_NULL {

                @Override
                public boolean pass(String location) {
                    return location.split(",")[1] != null;
                }

            },
    COUNTRY_US {
                @Override
                public boolean pass(String location) {
                    return ValidCountry.all.contains(location.split(",")[2]);
                }

            },
    STATE_ABBREVIATED {
                @Override
                public boolean pass(String location) {
                    return location.split(",")[1].length() == 2;
                }

            };
    /**
     * You can even make Sets of them.
     */
    static Set<NlpRule> isValidLocation = EnumSet.of(CITY_NOT_NULL, STATE_NOT_NULL, COUNTRY_US, STATE_ABBREVIATED);
}

public void test() {
    String test = "Santa Barbara,CA,USA";
    for (Rule r : NlpRule.isValidLocation) {
        boolean pass = r.pass(test);
        System.out.println(r + "(\"" + test + "\") - " + (pass ? "passes" : "FAILS"));
    }
}
 1
Author: OldCurmudgeon, 2015-06-05 13:43:57