Статьи

Встраивание правил в программы Java с помощью компилятора Mandarax

Общей задачей для программистов является реализация бизнес-логики, описанной в документах требований. Часто эта логика выражается в виде бизнес-правил: простых утверждений, которые описывают ограничения и отношения между сущностями («бизнес-объектами»). Использование объектно-ориентированных или функциональных языков программирования для выражения этих правил не очень продуктивно: правила должны быть сопоставлены с объектами, классами, функциями, методами и процедурными конструкциями, такими как условные выражения и циклы. Это подвержено ошибкам, и важная информация о правилах теряется в процессе. Эта проблема решается механизмами правил: библиотеками, которые могут интерпретировать и выполнять правила, формально определенные с использованием языка правил. Mandarax компиляторимеет схожие цели, но использует другой подход: вместо интерпретации правил они компилируются в стандартные классы Java. Эти классы могут быть затем интегрированы в приложения. Основным преимуществом этого подхода является удаление из приложения служебной информации о времени выполнения механизма правил. Уникальная особенность компилятора Mandarax заключается в том, что метаданные правила становятся доступными в сгенерированных классах. С помощью этой функции приложения могут не только вычислять результаты на основе правил, но и получать доступ к объяснению того, как эти результаты были вычислены.

Определение правил

В Mandarax правила определяются с использованием предметно-ориентированного языка (сценарий Mandarax). Язык расширяет синтаксис выражений Java. Каждый сценарий определяет отношения между объектами определенных типов. Например, рассмотрим следующий скрипт: 

package test.org.mandarax.compiler.reldef6;
import test.org.mandarax.compiler.*;
Discount goldDiscount = new Discount(20,true);
Discount silverDiscount = new Discount(10,true);
Discount specialDiscount = new Discount(5,false);
rel Discount(Customer customer,Discount discount) queries
getDiscount(customer),qualifiesForDiscount(customer,discount) {
rule1: c.turnover>1000 -> Discount(c,goldDiscount);
rule2: FrequentCustomer(c) -> Discount(c,silverDiscount);
rule3: c.paymentMethod == "CompanyVisa" -> Discount(c,specialDiscount);
}
rel FrequentCustomer(Customer customer) queries isFrequentCustomer(customer) {
rule1: c.transactionCount>5 -> FrequentCustomer(c);
rule2: c.transactionCount>3 & c.turnover>500 -> FrequentCustomer(c);
}

Этот сценарий определяет два отношения, Discount и FrequentCustomer. Отношение Discount связывает клиентов и скидки, отношение FrequentCustomer применяется к клиентам (технически это не отношение, а так называемый унарный предикат). Правила определяют, когда объекты создают отношения. Это может быть сделано либо путем ссылки на другие отношения (например, во втором правиле для Discount), либо с помощью выражений Java (например, в первом правиле для Discount), либо с помощью комбинации обоих. Правило является общим в том смысле, что используются переменные: c в rule1 для Discount может представлять любой экземпляр Customer.

Используемые в правилах выражения имеют синтаксис, очень похожий на Java. Тем не менее, есть некоторые различия. Например, c.transactionCount — это не ссылка на полеactionCount, а на метод getTransactionCount () объекта bean-компонента. В связи с этим, Mandarax сценарий похож на языки выражения , такие как Juel , MVEL и OGNL .

Правила компиляции

Следующий скрипт скомпилирует определение правила: 

import org.mandarax.compiler.*;
import org.mandarax.compiler.impl.*;
...
private static void compile(File file) throws Exception {
Compiler compiler = new DefaultCompiler();
Location location = new FileSystemLocation(new File("output_folder"));
compiler.compile(location,CompilationMode.RELATIONSHIP_TYPES,file);
compiler.compile(location,CompilationMode.QUERIES,file);
}

Компилятор генерирует исходный код Java. Экземпляр Location используется для указания места хранения сгенерированного кода. Сам компилятор имеет метод компиляции, который вызывается дважды: первый вызов (с параметром CompilationMode.RELATIONSHIP_TYPES) ​​генерирует классы, представляющие отношения, в то время как второй вызов (с параметром CompilationMode.QUERIES) генерирует классы с методами для запроса экземпляров отношения.

  Классы, созданные для представления отношений, представляют собой очень простые структуры, представляющие отношения. У них есть открытые поля для каждого слота отношений, а также методы equals () и hashCode (). Эти методы реализованы с использованием утилит Apache commons EqualsBuilder и HashCodeBuilder соответственно.

public class DiscountRel {
public Customer customer = null;
public Discount discount = null;
public DiscountRel() {
super();
}
public DiscountRel(Customer customer, Discount discount) {
super();
this.customer = customer;
this.discount = discount;
}
@Override public boolean equals(Object obj) {...}
@Override public int hashCode() {...}
}

 

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

 

public class DiscountRelInstances {
public staticDiscount goldDiscount = newDiscount(20, true);
public static Discount silverDiscount = newDiscount(10, true);
public staticDiscount specialDiscount = newDiscount(5, false);

// interface generated for queries
public static ResultSet<discountrel> getDiscount(Customer customer) {...}
public static ResultSet<discountrel> qualifiesForDiscount(Customer customer, Discount discount) {...}

// private methods
...
}

Существует несколько внутренних методов, содержащих действительную логику программирования, представляющую правила. Этот код довольно сложный. Основными проблемами являются связывание переменных (унификация) и возврат в обратном направлении, если оценка предусловия правила не удалась. Реализация основана на комбинации итераторов, таких как фильтрация, создание цепочек и вложение, с использованием идей из функционального программирования и таких проектов, как
Apache Commons Collection и
Google Guava

Использование сгенерированного кода

Методы запроса возвращают ResultSet of DiscountRel. ResultSet расширяет итератор. Следовательно, приложения могут выполнять итерации по вычисленным результатам следующим образом:

Customer customer = new Customer("John");
… // set customer properties here
ResultSet<discountrel> rs = DiscountRelInstances.getDiscount(customer);
while (rs.HasNext()) {
DiscountRel record = rs.next();
System.out.println("Customer " + customer + " qualifies for the following discount: " + record.discount);
}
rs.close();

Этот скрипт распечатает все скидки, на которые претендует соответствующий клиент. Порядок возвращаемых результатов зависит от порядка, в котором определены правила. Поэтому порядок правил часто рассматривается как осмысленный, и многие приложения будут использовать только первый результат, возвращаемый запросом. Если результата нет, запрос вернет пустой итератор. Это итератор, для которого hasNext () всегда возвращает false, а next () всегда выдает исключение.

Обратите внимание, что ResultSet также имеет метод close (). Цель этого метода — высвободить ресурсы (такие как соединения с базой данных), выделенные вычислением. Расчет результатов ленив . То есть вычисление выполняется только тогда, когда клиентское приложение вызывает методы next (). Поэтому методы запроса возвращают наборы результатов почти мгновенно в приложение.

Семантическое Отражение

При реализации набора правил с использованием основного языка программирования пишутся методы для выполнения правил и вычисления результата. Как правило, невозможно узнать, как вычисляется результат. Это часто представляет интерес, поскольку пользователи хотят понять логику компьютерных систем, прежде чем принимать важные решения. Способность системы объяснить, как был вычислен результат, делает его более надежным. В некоторых случаях для этого можно добавить операторы журнала в программный код. Однако, если алгоритмы для оценки правил являются сложными, файлы журнала также становятся очень сложными. В частности, если при оценке правил необходимо изучить и затем отбросить определенные вычисления (поскольку условия не выполняются, это называется обратным отслеживанием), уже записанные операторы журнала должны быть помечены как недействительные.

Код, сгенерированный компилятором Mandarax, поддерживает интерфейс для извлечения информации о правилах, используемых во время выполнения. Мы называем это семантическим отражением — оно похоже на стандартное отражение, используемое в объектно-ориентированных языках программирования. Но вместо раскрытия информации о типах, используемых в сигнатурах методов и классов, он извлекает информацию о логике, используемой в (запросе) методах. Рассмотрим следующий обновленный набор правил для политики скидок:

Discount goldDiscount = new Discount(20,true);
Discount silverDiscount = new Discount(10,true);
Discount specialDiscount = newDiscount(5,false);
@author="jens"
@lastupdated="26/10/10"
rel Discount(Customer customer,Discount discount) queries
getDiscount(customer),qualifiesForDiscount(customer,discount) {
@lastupdated="27/10/10"
@description="golden rule"
rule1: c.turnover>1000 -> Discount(c,goldDiscount);
@description="silver rule"
rule2: FrequentCustomer(c) -> Discount(c,silverDiscount);
@description="special rule"
rule3: c.paymentMethod == "CompanyVisa" -> Discount(c,specialDiscount);
}
@author="jens"
rel FrequentCustomer(Customer customer) queries isFrequentCustomer(customer) {
rule1: c.transactionCount>5 -> FrequentCustomer(c);
rule2: c.transactionCount>3 & c.turnover>500 -> FrequentCustomer(c);
}

Определения правил не изменились, но есть некоторые дополнительные аннотации (строки начинающиеся с @). Аннотации — это простые пары ключ-значение. Компилятор по существу исключает правила и превращает их в методы с внутренней логикой, представляющей правила. При этом компилятор добавляет аннотации к сгенерированным классам и создает API для запроса наборов результатов для этих аннотаций. Аннотации представляют метаданные о правиле и могут быть чрезвычайно полезны для отслеживания кода в соответствии с требованиями.

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

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

Customer customer = new Customer("John");
// set customer properties here
ResultSet<discountrel> rs = DiscountRelInstances.getDiscount(customer);
DiscountRel discount = rs.next();
List<derivationlogentry> computation = rs.getDerivationLog();
DiscountRel record = rs.next();
System.out.println("Customer" + customer + " qualifies for the following discount: " + record.discount);
System.out.println("The following rules have been applied to compute this result:");
for (DerivationLogEntry e:computation) {
System.out.println("rule id: " + e.getName());
System.out.println("description: " + e.getAnnotation("description"));
System.out.println("author: " + e.getAnnotation("author"));
System.out.println("last updated: " + e.getAnnotation("lastupdated"));
}</derivationlogentry></discountrel>

Сложный пример: приложение UServ

 

Страховое приложение UServ — это пример, основанный на сценарии дерби Userv Product, опубликованном
Форумом бизнес-правил для сравнения механизмов бизнес-правил. Полное приложение доступно в репозитории проекта Mandarax, и его можно запустить с помощью веб-запуска, указав в браузере следующий URL:
http://www-ist.massey.ac.nz/JBDietrich/userv-mdrx/userv.jnlp

 

Приложение состоит из 69 правил, организованных в 16 наборах правил, каждый из которых определяет одно отношение. Приложение использует простой пользовательский интерфейс, который организован следующим образом:

 

Скриншот приложения Userv 1

 

В верхней половине экрана с помощью элементов управления на этой панели можно управлять тремя объектами, представляющими автомобиль, водителя и политику. Всякий раз, когда происходит изменение, все правила пересматриваются (т. Е. Все запросы выполняются), и соответствующие результаты отображаются в нижней половине экрана. Нажав на «?» кнопки рядом с результатами или двойной щелчок по значениям в списках, всплывающее окно, показывающее вывод этого результата:

 

Наконец, при нажатии кнопок «показать правила» на главной панели инструментов отображаются наборы правил, из которых было сгенерировано приложение.

 

Скриншот приложения Userv 2

 

Это приложение использует некоторые функции, не обсуждаемые в этом документе, включая отрицание как сбой (например, используемый в правилах DriverCategory) и функции агрегирования (используемые в правилах InsuranceEligibility). Эти функции описаны в руководстве
Mandarax .

 

Более подробную информацию о проекте Mandarax можно найти на
веб-сайте проекта Mandarax .