Подтвердить что ты не робот

Каков эффективный шаблон/стиль дизайна для разработки механизма правил в Java?

Я внедряю правило-движок в Java. Мой движок правил предопределяет список независимых правил и наборов правил. Правило здесь - это просто кусочек логики. И набор правил объединяет эти простые правила в упорядоченный набор.

Я порядочный разработчик Java, но не Гуру. Мой коллега предложил мне два проекта для этой цели. Я не удовлетворен обоими проектами, поэтому этот вопрос.

Пример правила в моем проекте: Предположим, что входы - это местоположения в США, например, Санта-Барбара, Калифорния, США или Огайо, США, которые обычно находятся в определенном формате с полями города, штата и страны. Тогда у меня могут быть следующие правила:

ПРАВИЛО 1: Город не null
ПРАВИЛО 2: Состояние не null
ПРАВИЛО 3: Страна равна США или США
ПРАВИЛО 4: Длина состояния равна 2

Пример RuleSet в моем проекте:

RULESET: Допустимое местоположение Этот набор правил является упорядоченным набором указанных выше правил.

Два шаблона дизайна, которые я выполнил, следующие:

Дизайн 1: Использование Enum с анонимными внутренними классами

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

Дизайн 2: Использование абстрактного класса

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

Лучший подход к дизайну/шаблон? Как вы можете видеть, дизайн 2 избавляется от интерфейса и перечисления. Вместо этого он использует абстрактный класс. Мне все еще интересно, есть ли более эффективный дизайн/подход к реализации того же самого.

Активация с использованием блоков инициализатора:

Теперь в случае обоих конструкций выше. Скажем, если мне нужно создать экземпляр внешнего класса, чтобы использовать его в моей логике приложения, то я вынужден использовать блоки инициализаторов, которые я не совсем понимаю, является ли это хорошей практикой. Пример ниже приведен ниже:

Дизайн 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;
        }

    });
...

Дизайн 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, пожалуйста, приложите немного света! Также, пожалуйста, укажите, обнаружите ли какие-либо недостатки в этих двух конструкциях. Мне нужно знать плюсы и минусы, связанные с каждым из проектов, чтобы помочь мне принять правильное решение. Я рассматриваю лямбда, предикаты и несколько других шаблонов, как это было предложено некоторыми пользователями в комментариях.

4b9b3361

Ответ 1

Это интересный вопрос со многими возможными ответами. В какой-то мере решение будет зависеть от личных предпочтений. Я часто сталкиваюсь с подобными проблемами и имею следующие рекомендации. Обратите внимание, что они работают для меня, но могут не соответствовать вашим потребностям.

  • Используйте enum. В долгосрочной перспективе я считаю, что у них есть много преимуществ перед членами private static с точки зрения проверки их ошибок и полезных контейнеров (EnumSet и т.д.), Которые могут эффективно использовать их.

  • Используйте интерфейсы над абстрактными классами. Перед Java 8 были полезные причины использовать абстрактные классы. С членами default теперь нет веских причин (просто мое мнение - я уверен, что другие не согласятся). Перечисление может реализовать интерфейс.

  • В Java 8 логика, связанная с каждым "правилом", может быть встроена в выражение лямбда, что делает код инициализации для ваших перечислений более ясным.

  • Держите лямбды очень короткими - всего одну или две команды максимум (и предпочтительно одно выражение без блока). Это означает разделение любой сложной логики на отдельные методы.

  • Используйте отдельные перечисления для классификации ваших правил. Нет веских оснований вкладывать их в одно, и, разделив их, вы можете сделать конструкторы простыми, имея точно лямбда-выражения, относящиеся к их домену. Смотрите мой пример ниже, чтобы понять, что я имею в виду.

  • Если у вас есть иерархии правил, используйте шаблон составного дизайна. Он гибкий и надежный.

Поэтому, ставя эти рекомендации вместе, я бы предложил что-то вроде:

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

Они могут использоваться как:

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

И так далее.

Ответ 2

Там уже есть много двигателей с открытым исходным кодом Java - посмотрите http://java-source.net/open-source/rule-engines и http://drools.org/

Вы можете начать с использования/изучения источника для одного из них (с учетом того, где он не соответствует вашим требованиям) и оттуда.

Ответ 3

Интерфейс со статическими полями:

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. 

Некоторые могут предпочесть методы над функциональными объектами

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

Или вы можете иметь как метод, так и поле

public interface NlpRule 
{
    Object apply(Object object);

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

Примерами всех правил являются String->boolean, не знаю, почему NlpRule имеет значение Object->Object. Если правила действительно могут принимать/возвращать разные типы, вы должны, вероятно, создать NlpRule<T,R>.


CustomParser может быть сохранен в классе-закрытом помощнике

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

--

public interface NlpRule
...
    NlpRule STATE_ABBREVIATED = object -> 
    {
         ...
         location = NlpRuleHelper.parser.parse(location);

Ответ 4

Еще один возможный ответ заключается в использовании парсера DSL для правильного вашего правила, на языке программирования функций есть функция, называемая комбинатор парсера, которая может построить более сильный синтаксический анализатор (набор правил) из другого базового анализатора (правила). Хорошей точкой этого является гибкость, минусы - каждый раз, когда вы хотите изменить свой набор правил, вы должны перекодировать.

Ответ 5

Я думаю, вы добавили хотя бы один слой ненужного кода. Помните, что enum может также реализовывать интерфейсы и даже иметь абстрактные методы.

/**
 * 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"));
    }
}