Статьи

Действительно простой, но мощный механизм правил

У меня есть требование использовать механизм правил. Я хочу что-то легкое и довольно простое, но мощное. Хотя есть продукты, которые очень хороши, я не хочу чего-то с большими затратами на обучение. И я хотел написать свой собственный!

Вот несколько моих основных требований:

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

Чтобы прояснить эти требования, представьте следующие примеры:

1) В некоторых системах форума администратор должен иметь возможность настраивать отправку писем.

Здесь я бы написал несколько правил, например «когда флаг конфигурации с именем sendUserEmail установлен в значение true, отправлять электронное письмо пользователю» и «когда флаг конфигурации с именем sendAdministratorEmail имеет значение true и пользователь опубликовал менее 5 сообщений, отправьте электронное письмо администратору «.

2) Тарифная система должна быть настраиваемой, чтобы клиенты могли предложить лучший тариф.

Для этого я мог бы написать такие правила: «когда человек моложе 26 лет, применяется молодежный тариф», «когда человек старше 59 лет, применяется старший тариф» и «когда человек не является молодым Если вы не являетесь старшим, то им должен быть предоставлен тариф по умолчанию, если у них нет учетной записи более 24 месяцев, и в этом случае им должен быть предложен тариф лояльности ».

3) Билет на поезд можно считать продуктом. В зависимости от запроса на поездку подходят различные продукты.

Правило здесь может быть что-то вроде: «если расстояние перемещения составляет более 100 км, и желателен первый класс, то продукт А должен быть продан».

Наконец, более сложный пример, включающий некоторую итерацию ввода, а не просто оценку свойства:

4) Программное обеспечение расписания должно определять, когда ученики могут покинуть школу.

Правило для этого может быть следующим: «Если в классе есть ученик младше 10 лет, весь класс уходит рано. В противном случае они уходят в обычное время».

Итак, учитывая эти требования, я пошел искать язык выражения. Я начал с языка унифицированных выражений, указанного в JSP 2.1. Используя банку с яшмой, используемую в банках Tomcat и Apache Commons EL, я быстро что-то запустил. Затем я обнаружил библиотеку MVEL на Codehaus.org , которая, кстати, используется в Drools (ведущем механизме Java-правил?), И она работала еще лучше. Насколько я могу судить, он предлагает гораздо больше функциональности.

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

1) Двигатель настроен с некоторыми правилами.

2) Правило имеет следующие атрибуты:

    — пространство имен: механизм может содержать много правил, но только некоторые могут относиться к определенному вызову, и это пространство имен может использоваться для фильтрации

    — name: уникальное имя в пространстве имен

    — выражение: выражение MVEL для правила

    — result: строка, которую движок может использовать, если выражение этого правила имеет значение true

    — приоритет: целое число. Чем больше значение, тем выше приоритет.

    — описание: полезное описание, чтобы помочь управлению правилами.

3) Движок получает объект ввода и оценивает все правила (необязательно в пространстве имен) и либо:

    а) возвращает все правила, которые оцениваются как истинные,

    b) возвращает результат (строку) из правила с наивысшим приоритетом из всех правил, имеющих значение true,

    c) выполнить действие (определенное в приложении), которое связано с результатом правила с наивысшим приоритетом, из всех правил, имеющих значение true.

4) «Действие» — это экземпляры классов, которые может предоставить программист приложения. Действие получает имя. Когда механизму предлагается выполнить действие, основанное на правилах, выполняется имя действия, соответствующее результату «выигрышного» правила.

5) Правило может быть построено из «подправил». Подправило всегда используется только как строительный блок, на котором можно основывать более сложные правила. При оценке правил движок никогда не выберет подчиненное правило, которое будет лучшим (с наивысшим приоритетом) «выигрышным» правилом, т. Е. Оценивающим как истинное. Subrules облегчают построение сложных правил, как я скоро покажу.

Итак, время для некоторого кода!

Сначала давайте посмотрим на код для системы тарифов:

Rule r1 = new Rule("YouthTarif", "input.person.age < 26", "YT2011", 3, "ch.maxant.someapp.tarifs", null);
Rule r2 = new Rule("SeniorTarif", "input.person.age > 59", "ST2011", 3, "ch.maxant.someapp.tarifs", null);
Rule r3 = new Rule("DefaultTarif", "!#YouthTarif && !#SeniorTarif", "DT2011", 3, "ch.maxant.someapp.tarifs", null);
Rule r4 = new Rule("LoyaltyTarif", "#DefaultTarif && input.account.ageInMonths > 24", "LT2011", 4, "ch.maxant.someapp.tarifs", null);
List<Rule> rules = Arrays.asList(r1, r2, r3, r4);

Engine engine = new Engine(rules, true);

TarifRequest request = new TarifRequest();
request.setPerson(new Person("p"));
request.setAccount(new Account());

request.getPerson().setAge(24);
request.getAccount().setAgeInMonths(5);String tarif = engine.getBestOutcome(request); 

Итак, в приведенном выше коде я добавил 4 правила в движок и велел ему выдать исключение, если какое-либо правило не может быть предварительно скомпилировано. Затем я создал TarifRequest, который является входным объектом. Этот объект передается в двигатель, когда я прошу двигатель дать мне лучший результат. В этом случае лучшим результатом будет строка «YT2011», имя наиболее подходящего тарифа для клиента, которое я добавил в запрос тарифа.

Как все это работает? Когда движок получает правила, он выполняет их проверку и предварительно компилирует правила (для улучшения общей производительности). Заметьте, как первые два правила относятся к объекту с именем «input»? Это объект, переданный в метод «getBestOutcome» на движке. Движок передает входной объект в класс MVEL вместе с каждым выражением правил. Каждый раз, когда выражение оценивается как «истина», правило ставится на сторону в качестве кандидата на победу. В конце кандидаты сортируются в порядке приоритета, и поле результата правила с наивысшим приоритетом возвращается механизмом.

Обратите внимание, что третье и четвертое правила содержат символ «#». Это не стандартный язык выражений MVEL. Движок проверяет все правила, когда они передаются ему, и заменяет любой токен, начинающийся с символа хеша, на выражение, найденное в правиле, называемое так же, как токен. Оборачивает выражение в скобки. Регистратор выводит полное правило после разрешения и замены ссылочных правил на тот случай, если вы захотите проверить правило.

В приведенном выше бизнес-примере нас интересовал только лучший тариф для клиента. Точно так же нас может интересовать список возможных тарифов, чтобы мы могли предложить клиенту выбор. В этом случае мы могли бы вызвать метод «getMatchingRules» на движке, который бы возвратил все правила, отсортированные по приоритету. Имена тарифов являются (в данном случае) полем «результат» правил.

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

Rule rule1 = new SubRule("longdistance", "input.distance > 100", "ch.maxant.produkte", null);
Rule rule2 = new SubRule("firstclass", "input.map[\"travelClass\"] == 1", "ch.maxant.produkte", null);
Rule rule3 = new Rule("productA", "#longdistance && #firstclass", "productA", 3, "ch.maxant.produkte", null);
List<Rule> rules = Arrays.asList(rule1, rule2, rule3);

Engine e = new Engine(rules, true);

TravelRequest request = new TravelRequest(150);
request.put("travelClass", 1);
List rs = e.getMatchingRules(request);

 

В приведенном выше коде я строю rule3 из двух подправил. Но я никогда не хочу, чтобы результаты этих строительных блоков выводились из двигателя. Поэтому я создаю их как SubRules. У SubRules нет поля результата или приоритета. Они просто используются для создания более сложных правил. После того, как механизм использует подправил для замены всех токенов, начинающихся с хэша во время инициализации, он отбрасывает SubRules — они не оцениваются.

Приведенный выше TravelRequest занимает расстояние в конструкторе и содержит карту дополнительных параметров. MVEL позволяет вам легко получить доступ к значениям карты, используя синтаксис, показанный в правиле 2.

Далее рассмотрим экономическое обоснование желания настроить систему форума. Код ниже вводит действия. Действия создаются прикладным программистом и передаются движку. Движок принимает результаты (как описано в первом примере), ищет действия с такими же именами, что и эти результаты, и вызывает метод «execute» для этих действий (все они реализуют интерфейс IAction). Эта функциональность полезна, когда система должна быть способна на предопределенные вещи, но выбор того, что делать, должен быть легко настраиваемым и независимым от развертывания.

Rule r1 = new Rule("SendEmailToUser", "input.config.sendUserEmail == true", "SendEmailToUser", 1, "ch.maxant.someapp.config", null);
Rule r2 = new Rule("SendEmailToModerator", "input.config.sendAdministratorEmail == true and input.user.numberOfPostings < 5", "SendEmailToModerator", 2, "ch.maxant.someapp.config", null);
List<Rule> rules = Arrays.asList(r1, r2);

final List<String> log = new ArrayList<String>();

Action<ForumSetup, Void> a1 = new Action<ForumSetup, Void>("SendEmailToUser") {
  @Override
  public Void execute(ForumSetup input) {
    log.add("Sending email to user!");
    return null;
  }
};
Action<ForumSetup, Void> a2 = new Action<ForumSetup, Void>("SendEmailToModerator") {
  @Override
  public Void execute(ForumSetup input) {
    log.add("Sending email to moderator!");
    return null;
  }
};

Engine engine = new Engine(rules, true);

ForumSetup setup = new ForumSetup();
setup.getConfig().setSendUserEmail(true);
setup.getConfig().setSendAdministratorEmail(true);
setup.getUser().setNumberOfPostings(2);

engine.executeAllActions(setup, Arrays.asList(a1, a2));

В приведенном выше коде действия передаются движку, когда мы вызываем метод executeAllActions. В этом случае выполняются оба действия, поскольку объект установки заставляет оба правила принимать значение true. Обратите внимание, что действия выполняются сначала в порядке наивысшего приоритета. Каждое действие выполняется только один раз — его имя записывается после выполнения и больше не будет выполняться до тех пор, пока не будет вызван метод «execute * Action *» движков. Кроме того, если вы хотите, чтобы только действие, связанное с наилучшим результатом, было выполнено, вызовите метод executeBestAction вместо executeAllActions.

Наконец, давайте рассмотрим пример в классе. 

String expression = 
    "for(student : input.students){" +
    "if(student.age < 10) return true;" +
    "}" +
    "return false;";

Rule r1 = new Rule("containsStudentUnder10", expression , "leaveEarly", 1, "ch.maxant.rules", "If a class contains a student under 10 years of age, then the class may go home early");

Rule r2 = new Rule("default", "true" , "leaveOnTime", 0, "ch.maxant.rules", "this is the default");

Classroom classroom = new Classroom();
classroom.getStudents().add(new Person(12));
classroom.getStudents().add(new Person(10));
classroom.getStudents().add(new Person(8));

Engine e = new Engine(Arrays.asList(r1, r2), true);

String outcome = e.getBestOutcome(classroom);

Результат выше — «exitEarly», потому что в классе есть один ученик, чей возраст меньше 10 лет. MVEL позволяет вам писать довольно подробные выражения и действительно является языком программирования сам по себе. Движок просто требует, чтобы правило возвращало истину, если это правило считается кандидатом на стрельбу.

В тестах JUnit содержится больше примеров, содержащихся в исходном коде.

Итак, требования выполнены, за исключением «Должно быть возможно хранить правила в базе данных». Хотя эта библиотека не поддерживает чтение и запись правил в / из базы данных, правила основаны на строках. Так что не составит труда создать некоторый код JDBC или JPA, который считывает правила из базы данных, заполняет объекты Rule и передает их в Engine. Я не добавил это в библиотеку, потому что обычно эти вещи, а также управление правилами являются чем-то весьма специфичным для проекта. И поскольку моя библиотека никогда не будет такой же крутой и популярной, как Drools, я не уверен, что стоит добавить такую ​​функциональность.

Я поместил механизм правил в библиотеку OSGi с лицензией LGPL, и его можно скачать с моего сайта инструментов . Эта библиотека зависит отMVEL, который можно скачать здесь (я использовал версию 2.0.19). Если вам это нравится, дайте мне знать!

От http://blog.maxant.co.uk/pebble/2011/11/12/1321129560000.html