Перейти к основному содержимому

Введение в механизм правил Evrete

· 7 мин. чтения

1. Введение

В этой статье представлен первый практический обзор Evette — нового механизма правил Java с открытым исходным кодом.

Исторически Evrete разрабатывался как облегченная альтернатива Drools Rule Engine . Он полностью соответствует спецификации Java Rule Engine и использует классический алгоритм RETE прямой цепочки с несколькими настройками и функциями для обработки больших объемов данных.

Он требует Java 8 и выше, не имеет зависимостей, без проблем работает с объектами JSON и XML и допускает функциональные интерфейсы в качестве условий и действий правил .

Большинство его компонентов расширяемы с помощью интерфейсов поставщиков услуг, и одна из этих реализаций SPI превращает аннотированные классы Java в исполняемые наборы правил. Сегодня тоже попробуем.

2. Зависимости Maven

Прежде чем мы перейдем к коду Java, нам нужно объявить зависимость evrete-core Maven в pom.xml нашего проекта :

<dependency>
<groupId>org.evrete</groupId>
<artifactId>evrete-core</artifactId>
<version>2.1.04</version>
</dependency>

3. Сценарий использования

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

Наша модель данных домена будет включать в себя два простых класса — Customer и Invoice :

public class Customer {
private double total = 0.0;
private final String name;

public Customer(String name) {
this.name = name;
}

public void addToTotal(double amount) {
this.total += amount;
}
// getters and setters
}
public class Invoice {
private final Customer customer;
private final double amount;

public Invoice(Customer customer, double amount) {
this.customer = customer;
this.amount = amount;
}
// getters and setters
}

Кстати, движок поддерживает Java Records из коробки и позволяет разработчикам объявлять произвольные свойства класса как функциональные интерфейсы .

Далее в этом введении нам будет предоставлен набор счетов и клиентов, и логика подсказывает, что нам нужны два правила для обработки данных:

  • Первое правило очищает общую стоимость продаж каждого клиента.
  • Второе правило сопоставляет счета и клиентов и обновляет общую сумму каждого клиента.

И снова мы будем реализовывать эти правила с помощью изменчивых интерфейсов построителей правил и в виде аннотированных классов Java. Начнем с API конструктора правил.

4. API построителя правил

Построители правил являются центральными строительными блоками для разработки предметно-ориентированных языков (DSL) для правил. Разработчики будут использовать их при анализе источников Excel, обычного текста или любого другого формата DSL, который необходимо превратить в правила.

Однако в нашем случае нас в первую очередь интересует их способность встраивать правила прямо в код разработчика.

4.1. Декларация набора правил

С помощью построителей правил мы можем объявить наши два правила, используя плавные интерфейсы:

KnowledgeService service = new KnowledgeService();
Knowledge knowledge = service
.newKnowledge()
.newRule("Clear total sales")
.forEach("$c", Customer.class)
.execute(ctx -> {
Customer c = ctx.get("$c");
c.setTotal(0.0);
})
.newRule("Compute totals")
.forEach(
"$c", Customer.class,
"$i", Invoice.class
)
.where("$i.customer == $c")
.execute(ctx -> {
Customer c = ctx.get("$c");
Invoice i = ctx.get("$i");
c.addToTotal(i.getAmount());
});

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

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

Те, кто знаком с механизмом правил Drools, найдут наши объявления правил семантически эквивалентными следующей версии той же логики DRL :

rule "Clear total sales"
when
$c: Customer
then
$c.setTotal(0.0);
end

rule "Compute totals"
when
$c: Customer
$i: Invoice(customer == $c)
then
$c.addToTotal($i.getAmount());
end

4.2. Имитация тестовых данных

Мы протестируем наш набор правил на трех клиентах и 100 000 счетов со случайными суммами, распределенными среди клиентов случайным образом:

List<Customer> customers = Arrays.asList(
new Customer("Customer A"),
new Customer("Customer B"),
new Customer("Customer C")
);

Random random = new Random();
Collection<Object> sessionData = new LinkedList<>(customers);
for (int i = 0; i < 100_000; i++) {
Customer randomCustomer = customers.get(random.nextInt(customers.size()));
Invoice invoice = new Invoice(randomCustomer, 100 * random.nextDouble());
sessionData.add(invoice);
}

Теперь переменная sessionData содержит сочетание экземпляров Customer и Invoice , которые мы будем вставлять в сеанс правила.

4.3. Выполнение правила

Все, что нам нужно сделать сейчас, это передать все 100 003 объекта (100 000 счетов плюс три клиента) в новый экземпляр сеанса и вызвать его метод fire() :

knowledge
.newStatelessSession()
.insert(sessionData)
.fire();

for(Customer c : customers) {
System.out.printf("%s:\t$%,.2f%n", c.getName(), c.getTotal());
}

Последние строки выведут полученные объемы продаж для каждого клиента:

Customer A: $1,664,730.73
Customer B: $1,666,508.11
Customer C: $1,672,685.10

5. Аннотированные правила Java

Хотя наш предыдущий пример работает так, как ожидалось, он не делает библиотеку совместимой со спецификацией, которая ожидает, что механизмы правил будут:

  • «Продвижение декларативного программирования путем экстернализации бизнес-логики или логики приложений».
  • «Включите документированный формат файла или инструменты для создания правил и наборов выполнения правил, внешних по отношению к приложению».

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

И модуль расширения Evrete Annotated Java Rules отвечает этому требованию. Модуль, по сути, представляет собой «демонстрационный» DSL, который опирается исключительно на основной API библиотеки.

Давайте посмотрим, как это работает.

5.1. Монтаж

Аннотированные правила Java — это реализация одного из интерфейсов поставщика услуг (SPI) Evrete, для которого требуется дополнительная зависимость evrete-dsl-java Maven:

<dependency>
<groupId>org.evrete</groupId>
<artifactId>evrete-dsl-java</artifactId>
<version>2.1.04</version>
</dependency>

5.2. Декларация набора правил

Давайте создадим тот же набор правил, используя аннотации. Мы выберем простой исходный код Java вместо классов и связанных jar-файлов:

public class SalesRuleset {

@Rule
public void rule1(Customer $c) {
$c.setTotal(0.0);
}

@Rule
@Where("$i.customer == $c")
public void rule2(Customer $c, Invoice $i) {
$c.addToTotal($i.getAmount());
}
}

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

  • наш исходный файл содержит все необходимые импорты
  • сторонние зависимости и доменные классы находятся в пути к классам движка

Затем мы говорим движку прочитать определение нашего набора правил из внешнего места:

KnowledgeService service = new KnowledgeService();
URL rulesetUrl = new URL("ruleset.java"); // or file.toURI().toURL(), etc
Knowledge knowledge = service.newKnowledge(
"JAVA-SOURCE",
rulesetUrl
);

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

Несколько замечаний по этому конкретному примеру:

  • Мы решили строить правила из простой Java ( аргумент «JAVA-SOURCE» ), что позволяет движку выводить имена фактов из аргументов метода.
  • Если бы мы выбрали источники .class или .jar , аргументы метода потребовали бы аннотаций @Fact .
  • Движок автоматически упорядочил правила по имени метода. Если мы поменяем имена местами, правило сброса очистит ранее вычисленные тома. В результате мы увидим нулевые объемы продаж.

5.3. Как это работает

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

Следовательно, переменные класса, если они определены, становятся доступными для методов правил.

Если бы мы определили условные методы или объявили новые поля как методы, эти методы также имели бы доступ к переменным класса.

Как и обычные классы Java, такие наборы правил можно расширять, повторно использовать и упаковывать в библиотеки.

5.4. Дополнительные возможности

Простые примеры хорошо подходят для введения, но оставляют позади многие важные темы. Для аннотированных правил Java к ним относятся:

  • Условия как методы класса
  • Произвольные объявления свойств как методы класса
  • Слушатели фаз, модель наследования и доступ к среде выполнения
  • И, прежде всего, повсеместное использование полей класса — от условий до действий и определений полей.

6. Заключение

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

  1. Другие движки могут лучше предоставлять готовые к использованию DSL-решения и репозитории правил.
  2. Вместо этого Evrete предназначен для разработчиков, которые могут создавать произвольные DSL .
  3. Те, кто привык создавать правила на Java, могут найти пакет «Аннотированные правила Java» как лучший вариант.

Стоит упомянуть и другие функции, не описанные в этой статье, но упомянутые в API библиотеки:

  • Объявление произвольных свойств фактов
  • Условия как предикаты Java
  • Изменение условий правил и действий на лету
  • Методы разрешения конфликтов
  • Добавление новых правил к живым сеансам
  • Пользовательские реализации интерфейсов расширяемости библиотеки

Официальная документация находится по адресу https://www.evrete.org/docs/ .

Примеры кода и модульные тесты доступны на GitHub .