Содержание


Реализация бизнес-логики при помощи процессора правил Drools

Декларативный подход к программированию бизнес-логики приложений

Comments

В настоящее время к программному обеспечению предъявляются повышенные требования, особенно в том, что касается его поведения и функциональности. В результате программные продукты включают в себя компоненты, реализующие сложную бизнес-логику. Наиболее распространенным способом разработки компонентов бизнес-логики в приложениях J2EE и J2SE является написание Java-кода, реализующего требуемые правила. В большинстве случаев сложность и запутанность именно этого кода приводит к тому, что поддержка и развитие бизнес-логики приложения начинает представлять собой серьезную проблему даже для опытных разработчиков. Кроме того, любое изменение логики, даже самое простое, требует перекомпиляции и повторного разворачивания приложения.

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

В настоящее время существует несколько процессоров правил, как коммерческих, так и с открытым кодом. Коммерческие процессоры обычно позволяют описывать правила на специальных языках, напоминающих английский. В остальных же правила описываются на скриптовых языках, таких как Groovy или Python. В данной редакции статьи рассказывается о процессоре правил под названием Drools на примере простой программы, демонстрирующей использование Drools для реализации слоя бизнес-логики Java-приложений.

Drools – это процессор правил с открытым кодом, написанный на Java и выполняющий правила в соответствии с алгоритмом Рете (см. Ресурсы). Благодаря Drools бизнес-правила приложения можно описывать декларативным образом, используя простой для изучения и понимания язык, не связанный с XML. Более того, в файлы правил можно вставлять фрагменты кода на Java, что делает Drools еще более привлекательным. У него есть и другие преимущества, в частности:

  • поддержка со стороны активного сообщества;
  • простота использования;
  • высокая скорость применения правил;
  • растущая популярность в среде Java-разработчиков;
  • соответствие стандартному API для процессоров правил на Java (Java Rule Engine API – JSR 94) (см. Ресурсы);
  • бесплатность.

Текущая версия Drools

На момент написания данной статьи последней версией Drools была 4.0.4. Данная версия существенно отличается от своих предшественников. Несмотря на некоторые проблемы с обратной совместимостью, благодаря ее возможностям Drools стал еще привлекательнее, чем прежде. В частности, новый родной язык для описания правил проще и элегантнее диалекта XML, использующегося в некоторых из предыдущих версий. Новый язык позволяет описывать правила в более сжатой и удобной для понимания форме.

Другим серьезным новшеством является подключаемый модуль Drools для среды разработки Eclipse (версий 3.2 и 3.3). Я настоятельно рекомендую вам использовать данный модуль при работе с Drools, так как он упрощает разработку проектов, в которых применяется Drools, и повышает вашу производительность как разработчика. В частности, модуль автоматически проверяет синтаксис ваших правил, а также обладает функцией дополнения кода (code completion). Кроме того, он помогает отлаживать правила, благодаря чему время на поиск ошибок может сокращаться с часов до считанных минут. Вы можете устанавливать точки останова непосредственно внутри файла правил, получая тем самым возможность проанализировать состояние объектов в определенные моменты во время выполнения правил. Таким образом, вы можете получать информацию о знаниях (с этим термином мы познакомимся ниже), которыми обладает процессор правил в заданные моменты времени.

Пример задачи

В данной статье рассказывается об использовании Drools для реализации слоя бизнес-логики на примере приложения Java. Для понимания статьи необходим определенный уровень знакомства с процессами разработки и отладки Java-приложений в среде Eclipse. Кроме того, необходимо иметь представление об инфраструктуре JUnit для тестирования Java-кода, а также уметь работать с ним в Eclipse.

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

  • Компания под названием XYZ производит два типа компьютеров: «тип 1» и «тип 2». Тип компьютера определяется его архитектурой.
  • Компьютеры фирмы XYZ могут выполнять ряд функций. На данный момент определены четыре функции: сервер DDNS (DDNS Server), сервер DNS (DNS Server), шлюз (Gateway) и маршрутизатор (Router).
  • Каждый компьютер перед выпуском проходит ряд тестов.
  • Тесты, выполняемые над каждым компьютером, зависят от его типа и выполняемой функции. На данный момент определены пять тестов: «тест 1», «тест 2», «тест 3», «тест 4» и «тест 5».
  • Для каждого проверяемого компьютера устанавливается предельный срок тестирования. Все тесты, соответствующие данному компьютеру, должны быть выполнены не позднее указанной даты. Сама дата зависит от тестов, выбранных для каждого конкретного компьютера.
  • Большая часть процесса выполнения тестов в компании XYZ автоматизирована при помощи внутреннего программного обеспечения, которое выбирает конкретный набор тестов и определяет дату тестирования на основе типа и функций компьютеров.
  • На данный момент логика, выбирающая тесты и дату тестирования для конкретного компьютера, являются частью компилируемого кода приложения. Компонент, реализующий эту логику, написан на Java.
  • Данная логика меняется чаще одного раза в месяц. При каждом изменении разработчикам приходится выполнять утомительную работу по переписыванию Java-кода.

Проблема заключается в том, что каждое внесение изменений в логику выбора тестов и даты их выполнения с учетом типа компьютера связано со значительными затратами для компании. Поэтому руководство XYZ поставило перед разработчиками задачу: предложить гибкий способ «обновления» бизнес-правил, находящихся в эксплуатации, который бы требовал минимальных усилий. В этот момент и возникла идея использовать Drools. Разработчики пришли к выводу, что использование процессора правил для описания условий выбора конкретного набора тестов позволит сэкономить время и силы при реализации бизнес-логики. Для внесения изменений в логику будет достаточно отредактировать файл правил и обновить его в составе приложения, находящегося в эксплуатации. Это выглядит значительно проще и должно занимать меньше времени, чем изменение кода, требующего перекомпиляции приложения, так как в последнем случае пришлось бы также выполнить все действия, установленные регламентом данной компании касательно ввода в эксплуатацию измененных компонентов приложения (см. заметку "В каких случаях стоит использовать процессоры правил?").

На данный момент наборы тестов и даты их выполнения для конкретных типов компьютеров выбираются в соответствии со следующими бизнес-правилами.

  • Над компьютерами типа 1 должны быть выполнены тесты 1, 2 и 3.
  • Над компьютерами типа 2, выполняющими функцию серверов DNS, должны быть выполнены тесты 4 и 5.
  • Над компьютерами типа 2, выполняющими функцию серверов DDNS, должны быть выполнены тесты 2 и 3.
  • Над компьютерами типа 2, выполняющими функцию шлюза, должны быть выполнены тесты 3 и 4.
  • Над компьютерами типа 2, выполняющиими функцию маршрутизатора, должны быть выполнены тесты 1 и 3.
  • Если среди тестов, выбранных для данного компьютера, есть тест 1, то тестирование должно производиться не позднее чем через три дня после даты производства. Данное правило является приоритетным по отношению ко всем последующим правилам выбора даты тестирования.
  • Если среди тестов, выбранных для данного компьютера, есть тест 2, то тестирование должно производиться не позднее чем через семь дней после даты производства. Данное правило является приоритетным по отношению ко всем последующим правилам выбора даты тестирования.
  • Если среди тестов, выбранных для данного компьютера, есть тест 3, то тестирование должно производиться не позднее чем через 10 дней после даты производства. Данное правило является приоритетным по отношению ко всем последующим правилам выбора даты тестирования.
  • Если среди тестов, выбранных для данного компьютера, есть тест 4, то тестирование должно производиться не позднее чем через 12 дней после даты производства. Данное правило является приоритетным по отношению ко всем последующим правилам выбора даты тестирования.
  • Если среди тестов, выбранных для данного компьютера, есть тест 5, то тестирование должно производиться не позднее чем через 14 дней после даты производства.

В настоящее время сформулированные выше бизнес-правила для выбора тестов и предельной даты тестирования реализованы на Java. При этом код выглядит примерно так, как показано в листинге 1.

Листинг 1. Реализация бизнес-правил при помощи операторов if-else
Machine machine = ...
// Выбор тестов
Collections.sort(machine.getFunctions());
int index;

if (machine.getType().equals("Type1")) {
   Test test1 = ...
   Test test2 = ...
   Test test5 = ...
   machine.getTests().add(test1);
   machine.getTests().add(test2);
   machine.getTests().add(test5);
} else if (machine.getType().equals("Type2")) {
   index = Collections.binarySearch(machine.getFunctions(), "Router");
   if (index >= 0) {
      Test test1 = ...
      Test test3 = ...
      machine.getTests().add(test1);
      machine.getTests().add(test3);
   }
   index = Collections.binarySearch(machine.getFunctions(), "Gateway");
   if (index >= 0) {
      Test test4 = ...
      Test test3 = ...
      machine.getTests().add(test4);
      machine.getTests().add(test3);
   }
...
}

// Присвоение срока тестирования
Collections.sort(machine.getTests(), new TestComparator());
...
Test test1 = ...
index = Collections.binarySearch(machine.getTests(), test1);
if (index >= 0) {
   // Установить срок - 3 дня с момента производства компьютера
   Timestamp creationTs = machine.getCreationTs();
   machine.setTestsDueTime(...);
   return;
}

index = Collections.binarySearch(machine.getTests(), test2);
if (index >= 0) {
   // Установить срок - 7 дней с момента производства компьютера
   Timestamp creationTs = machine.getCreationTs();
   machine.setTestsDueTime(...);
   return;
}
...

Хотя код в листинге 1 не отличается особой сложностью, он далеко не тривиален. Внесение в него изменений требует повышенной осторожности, так как данный запутанный набор операторов if-else реализует бизнес-логику приложения. Таким образом, если вы малознакомы (или вовсе незнакомы) с бизнес-правилами, то смысл данного кода будет отнюдь не очевиден с первого взгляда.

Импортирование демонстрационного приложения

Пример приложения, в котором используется Drools, находится в ZIP-архиве к данной статье. В состав данного приложения входит файл правил Drools, которые декларативно описывают бизнес-правила, сформулированные выше. В архиве находится Java-проект для Eclipse 3.2, созданный с помощью Drools версии 4.0.4, а также подключаемого модуля Drools. Для создания демонстрационного проекта выполните следующие действия.

  1. Загрузите ZIP-архив (см. раздел Загрузка).
  2. Загрузите и установите модуль Drools для Eclipse (см. Ресурсы).
  3. Запустите Eclipse и выберите опцию Import Existing Projects into Workspace, как показано на рисунке 1.
    Рисунок 1. Импортирование демонстрационного проекта в рабочее пространство Eclipse
    Importing the sample program into your Eclipse workspace
    Importing the sample program into your Eclipse workspace
  4. Выберите ранее загруженный файл архива и импортируйте его в ваше рабочее пространство (workspace). В результате появится новый Java-проект под названием DroolsDemo (рисунок 2).
    Рисунок 2. Демонстрационный проект в вашем рабочем пространстве
    Importing the sample program into your Eclipse workspace

Если в Eclipse установлена опция Build automatically (автоматическая сборка проекта), то приложение уже должно быть скомпилировано и готово к запуску. В противном случае запустите сборку проекта DroolsDemo.

Реализация демонстрационного приложения

Далее рассмотрим реализацию демонстрационного приложения. Основные Java-классы расположены в пакете demo. К ним относятся классы Machine и Test, описывающие объекты предметной области. Компьютеры, для которых должны быть выбраны тесты и даты тестирования, представлены в виде экземпляров класса Machine, код которого показан в листинге 2.

Листинг 2. Переменные-члены класса Machine
public class Machine {

   private String type;
   private List functions = new ArrayList();
   private String serialNumber;
   private Collection tests = new HashSet();
   private Timestamp creationTs;
   private Timestamp testsDueTime;

   public Machine() {
     super();
     this.creationTs = new Timestamp(System.currentTimeMillis());
   }
   ...

Как видно из листинга 2, класс Machine содержит следующие свойства:

  • type (тип String): предназначено для хранения типа компьютера;
  • functions (тип List): предназначено для хранения списка функций, выполняемых компьютером;
  • testsDueTime (тип Timestamp): предназначено для хранения предельной даты выполнения тестов;
  • tests (тип Collection): предназначено для хранения списка тестов, выбранных для данного компьютера.

Обратите внимание, что компьютеры могут выполнять различные функции. Кроме того, над каждым компьютером может быть выполнено несколько тестов.

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

Экземпляры класса Test служат для представления тестов, выполняемых над компьютерами. Каждый объект Test уникально характеризуется свойствами id и name (листинг 3).

Листинг 3. Переменные-члены класса Test
public class Test {

   public static Integer TEST1 = new Integer(1);
   public static Integer TEST2 = new Integer(2);
   public static Integer TEST3 = new Integer(3);
   public static Integer TEST4 = new Integer(4);
   public static Integer TEST5 = new Integer(5);

   private Integer id;
   private String name;
   private String description;
   public Test() {
      super();
   }
   ...

В данном приложении Drools используется для анализа экземпляров класса Machine. Основываясь на значениях свойств type и functions каждого экземпляра Machine, система правил присваивает нужные значения свойствам tests и testsDueTime данного объекта.

В пакете demo также находится класс TestDAOImpl, обеспечивающий доступ к объектам класса Test. С его помощью можно осуществлять поиск экземпляров Test по их идентификаторам (свойство id). Данный класс очень прост, он не обращается ни к каким внешним ресурсам, наподобие реляционных баз данных, для выборки экземпляров Test. Вместо этого набор объектов Test создается непосредственно в коде TestDAOImpl. В реальном приложении подобный класс для доступа к данным скорее всего устанавливал бы соединение с внешним ресурсом для извлечения экземпляров Test.

Класс RulesEngine

Одним из наиболее важных классов (если не самым важным) в пакете demo является класс RulesEngine. Экземпляр данного класса представляет собой объект-обертку, заключая в себе логику обращения к классам Drools. Вы можете свободно использовать RulesEngine в своих проектах, так как он никак не привязан к нашему демонстрационному приложению. Свойства и конструктор данного класса показаны в листинге 4.

Листинг 4. Переменные-члены и конструктор класса RulesEngine
public class RulesEngine {

   private RuleBase rules;
   private boolean debug = false;

   public RulesEngine(String rulesFile) throws RulesEngineException {
      super();
      try {
         // Чтение файла правил
         Reader source = new InputStreamReader(RulesEngine.class
            .getResourceAsStream("/" + rulesFile));
         // Создание пакета правил при помощи специального компоновщика 
         // (package builder)
         PackageBuilder builder = new PackageBuilder();
         // Разбор и компиляция пакета правил
         builder.addPackageFromDrl(source);
         // Получение ссылки на пакет откомпилированных правил
         Package pkg = builder.getPackage();
         // Развертывание пакета правил (добавление его в базу правил)
         rules = RuleBaseFactory.newRuleBase();
         rules.addPackage(pkg);
      } catch (Exception e) {
         throw new RulesEngineException(
            "Could not load/compile rules file: " + rulesFile, e);
      }
   }
   ...

Как видно из листинга 4, конструктор RulesEngine принимает на вход строковый параметр, представляющий собой имя файла, содержащего набор бизнес-правил. Разбор данного файла и компиляция правил осуществляются при помощи экземпляра класса PackageBuilder. (Замечание: в данном коде подразумевается, что файл правил размещен в каталоге rules, находящемся в classpath приложения). Как только этот шаг выполнен, экземпляр PackageBuilder используется для объединения всех скомпилированных правил в виде объекта класса Package, при помощи которого затем происходит конфигурирование экземпляра Drools-класса RuleBase. Данный экземпляр, который, по сути, является представлением скомпилированных исходных правил в памяти компьютера, далее присваивается свойству rules класса RulesEngine.

Метод executeRules() класса RulesEngine приведен в листинге 5.

Листинг 5. Метод executeRules() класса RulesEngine
public void executeRules(WorkingEnvironmentCallback callback) {
   WorkingMemory workingMemory = rules.newStatefulSession();
   if (debug) {
      workingMemory
         .addEventListener(new DebugWorkingMemoryEventListener());
   }
   callback.initEnvironment(workingMemory);
   workingMemory.fireAllRules();
}

Метод executeRules() фактически является центральным звеном в Java-коде приложения. При вызове данного метода происходит выполнение всех правил, которые были загружены из исходного файла конструктором класса. Экземпляр класса WorkingMemory, входящего в состав Drools, используется для объявления или проверки знаний, которыми должен руководствоваться процессор правил для определения того, следствия каких правил должны быть выполнены (следствия выполняются, если удовлетворены все условия данного правила). Знания – это та информация (или данные), которая используется процессором правил для принятия решения о применении того или иного правила. Например, знания процессора правил могут представлять собой данные о текущем состоянии одного или нескольких объектов и их свойств.

Выполнение следствий правил происходит внутри метода fireAllRules() класса WorkingMemory. Вероятно, вы задумаетесь (по крайней мере я на это надеюсь), как знания попадают внутрь экземпляра WorkingMemory. При ближайшем рассмотрении сигнатуры данного метода вы заметите, что он принимает дополнительный параметр – экземпляр класса, реализующего интерфейс WorkingEnvironmentCallback. Данный экземпляр должен создаваться объектами, вызывающими метод executeRules(). Реализация этого интерфейса заключается в создании единственного метода, как показано в листинге 6.

Листинг 6. Интерфейс WorkingEnvironmentCallback
public interface WorkingEnvironmentCallback {
   void initEnvironment(WorkingMemory workingMemory) throws FactException;
}

Таким образом, передача знаний экземпляру WorkingMemory является обязанностью объекта, вызывающего метод executeRules(). Об этом будет рассказано ниже.

Класс TestsRulesEngine

В листинге 7 показан класс TestsRulesEngine, также находящийся в пакете demo.

Листинг 7. Класс TestsRulesEngine
public class TestsRulesEngine {

   private RulesEngine rulesEngine;
   private TestDAO testDAO;

   public TestsRulesEngine(TestDAO testDAO) throws RulesEngineException {
      super();
      rulesEngine = new RulesEngine("testRules1.drl");
      this.testDAO = testDAO;
   }

   public void assignTests(final Machine machine) {
      rulesEngine.executeRules(new WorkingEnvironmentCallback() {
         public void initEnvironment(WorkingMemory workingMemory) {
            // Глобальные объекты следует сохранять до вставки/проверки знаний!
            workingMemory.setGlobal("testDAO", testDAO);
            workingMemory.insert(machine);
         };
      });
   }
}

Класс TestsRulesEngine содержит всего два свойства – rulesEngine (типа RulesEngine) и testDAO, в котором хранится ссылка на объект, реализующий интерфейс TestDAO. При создании объекта rulesEngine в конструктор передается строка "testRules1.drl", задающая имя файла, в котором содержится декларативное описание бизнес-правил, сформулированных выше (см. раздел «Пример задачи»). Метод assignTests() класса TestsRulesEngine создает анонимный объект, реализующий интерфейс WorkingEnvironmentCallback, а затем передает его в метод executeRules() класса RulesEngine.

Обратив внимание на код метода assignTests(), легко увидеть, как знания передаются в экземпляр класса WorkingMemory. Данный класс содержит метод insert(), в который передается объект, представляющий собой знания, используемые Drools в процессе обработки правил. В нашем случае этот объект является экземпляром класса Machine. Передаваемые таким образом объекты используются для проверки условий применения правил.

В некоторых ситуациях необходимо, чтобы процессор правил содержал ссылки на объекты, отличные от тех, которые используются для проверки условий применения правил. Для передачи ссылок на такие объекты служит метод setGlobal() класса WorkingMemory. В нашем приложении данный метод используется для передачи процессору правил ссылки на экземпляр TestDAO, который будет использоваться для поиска необходимых объектов класса Test.

Класс TestsRulesEngine – это единственный компонент приложения, написанный на Java, который содержит логику, специфичную для данных бизнес-правил (выбор тестов и времени тестирования в зависимости от типа и функций компьютеров). При этом реализация этого класса будет оставаться неизменной даже при модифицировании самих бизнес-правил.

Файл правил Drools

Как было замечено выше, файл testRules1.drl содержит объявления правил выбора тестов и дат тестирования для компьютеров. Правила в данном файле описываются на собственном языке Drools.

Любой файл правил Drools содержит одно или несколько объявлений правил (конструкция rule). Каждое подобное объявление состоит из одного или более условных элементов, а также одного или нескольких следствий или действий, которые выполняются при применении правила. Кроме того, файл может содержать несколько (ноль или более) конструкций import,global и function.

Понять принципы создания файлов правил Drools легче всего на конкретном примере. Взгляните на первую секцию файла testRules1.drl, показанную в листинге 8.

Листинг 8. Первая секция файла testRules1.drl
package demo;

import demo.Machine;
import demo.Test;
import demo.TestDAO;
import java.util.Calendar;
import java.sql.Timestamp;
global TestDAO testDAO;

Как видно из листинга 8, при помощи конструкций import задаются полные имена классов для объектов, которые затем будут использоваться при описании правил. Благодаря конструкциям global процессору правил передается информация об объектах, которые должны быть доступны внутри правил, но которые не являются частью знаний, необходимых для проверки условий применения правил. Другими словами, объявления global играют роль глобальных переменных. Каждое объявление должно включать тип (имя класса) и идентификатор (имя переменной), который будет использоваться для обращения к объекту. Идентификаторы в объявлениях global должны соответствовать идентификаторам, которые передавались в качестве аргументов в метод setGlobal() класса WorkingMemory. В нашем случае таким идентификатором был "testDAO" (см. листинг 7).

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

Листинг 9. Функции Java, определенные в файле testRules1.drl
function void setTestsDueTime(Machine machine, int numberOfDays) {
   setDueTime(machine, Calendar.DATE, numberOfDays);
}

function void setDueTime(Machine machine, int field, int amount) {
   Calendar calendar = Calendar.getInstance();
   calendar.setTime(machine.getCreationTs());
   calendar.add(field, amount);
   machine.setTestsDueTime(new Timestamp(calendar.getTimeInMillis()));
}
 ...

Первое правило, описанное в файле testRules1.drl, показано в листинге 10.

Листинг 10. Первое правило, описанное в файле testRules1.drl
rule "Tests for type1 machine"
salience 100
when
   machine : Machine( type == "Type1" )
then
   Test test1 = testDAO.findByKey(Test.TEST1);
   Test test2 = testDAO.findByKey(Test.TEST2);
   Test test5 = testDAO.findByKey(Test.TEST5);
   machine.getTests().add(test1);
   machine.getTests().add(test2);
   machine.getTests().add(test5);
   insert( test1 );
   insert( test2 );
   insert( test5 );
end

Как видно из листинга 10, конструкция rule включает в себя имя, уникально идентифицирующее данное правило. Кроме того, она также содержит ключевые слова when и then, которые обозначают секции для описания условий и следствий соответственно. Правило, приведенное в листинге 10, содержит один условный элемент, в котором происходит обращение к объекту класса Machine. Если вернуться назад к листингу 7, то можно заметить, что экземпляр Machine был передан в объект WorkingMemory. Именно к этому экземпляру происходит обращение в данном правиле. Условная часть заключается в анализе объекта Machine (который является частью знаний) с целью определения надо ли выполнять следствия данного правила. Если объект удовлетворяет условию (результат проверки true), то происходит срабатывание или выполнение следствий правила. Как показано в листинге 10, в данном случае следствием является фрагмент кода на Java. С первого взгляда видно, что данное правило Drools реализует следующее бизнес-правило:

  • Над компьютерами типа 1 должны выполняться только тесты 1, 2 и 5.

Таким образом, условная часть правила заключается в проверке того, что свойство type данного экземпляра Machine содержит значение Type1 (тип 1). В данной секции правила можно обращаться к свойствам объектов, соответствующим спецификации Java Bean, без необходимости вызова get-методов. Если результатом проверки является true, то ссылке на данный объект Machine присваивается идентификатор machine. Эта ссылка затем используется при выполнении следствия правила, в результате которого экземпляру Machine присваивается конкретный набор тестов.

Единственное, что может выглядеть несколько странно в описании этого правила – это три последние строки в секции следствия. Как упоминалось в разделе "Пример задачи", выбор предельного срока тестирования зависит от конкретного набора тестов, выбранных для данного компьютера. Поэтому выбранные тесты должны стать частью знаний, которые будут далее использоваться процессором при проверке условий правил. Для этого и служат три последних строки: в них вызывается метод insert, который обновляет базу знаний процессора правил.

Определение порядка выполнения правил

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

Следующие четыре правила, определенные в файле testRules1.drl, реализуют оставшиеся бизнес-правила выбора тестов для компьютеров (листинг 11). Они аналогичны первому правилу, рассмотренному выше. Обратите внимание, что первые пять правил имеют одинаковое значение атрибута salience, так как результат не зависит от порядка их применения. В ситуациях, когда порядок применения правил влияет на итоговый результат, необходимо задавать различные значения атрибута salience.

Листинг 11. Остальные правила в файле testRules1.drl, касающиеся выбора тестов
rule "Tests for type2, DNS server machine"
salience 100
when
   machine : Machine( type == "Type2", functions contains "DNS Server")
then
   Test test5 = testDAO.findByKey(Test.TEST5);
   Test test4 = testDAO.findByKey(Test.TEST4);
   machine.getTests().add(test5);
   machine.getTests().add(test4);
   insert( test4 );
   insert( test5 );
end

rule "Tests for type2, DDNS server machine"
salience 100
when
   machine : Machine( type == "Type2", functions contains "DDNS Server")
then
   Test test2 = testDAO.findByKey(Test.TEST2);
   Test test3 = testDAO.findByKey(Test.TEST3);
   machine.getTests().add(test2);
   machine.getTests().add(test3);
   insert( test2 );
   insert( test3 );
end

rule "Tests for type2, Gateway machine"
salience 100
when
   machine : Machine( type == "Type2", functions contains "Gateway")
then
   Test test3 = testDAO.findByKey(Test.TEST3);
   Test test4 = testDAO.findByKey(Test.TEST4);
   machine.getTests().add(test3);
   machine.getTests().add(test4);
   insert( test3 );
   insert( test4 );
end

rule "Tests for type2, Router machine"
salience 100
when
   machine : Machine( type == "Type2", functions contains "Router")
then
   Test test3 = testDAO.findByKey(Test.TEST3);
   Test test1 = testDAO.findByKey(Test.TEST1);
   machine.getTests().add(test3);
   machine.getTests().add(test1);
   insert( test1 );
   insert( test3 );
end
...

Остальные правила, описанные в файле правил Drools, показаны в листинге 12. Как вы, наверное, догадались, они служат для присвоения сроков тестирования.

Листинг 12. Правила в testRules1.drl, отвечающие за установку сроков тестирования
rule "Due date for Test 5"
salience 50
when
   machine : Machine()
   Test( id == Test.TEST5 )
then
   setTestsDueTime(machine, 14);
end

rule "Due date for Test 4"
salience 40
when
   machine : Machine()
   Test( id == Test.TEST4 )
then
   setTestsDueTime(machine, 12);
end

rule "Due date for Test 3"
salience 30
when
   machine : Machine()
   Test( id == Test.TEST3 )
then
   setTestsDueTime(machine, 10);
end

rule "Due date for Test 2"
salience 20
when
   machine : Machine()
   Test( id == Test.TEST2 )
then
   setTestsDueTime(machine, 7);
end

rule "Due date for Test 1"
salience 10
when
   machine : Machine()
   Test( id == Test.TEST1 )
then
   setTestsDueTime(machine, 3);
end

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

Во-первых, обратите внимание, что данные правила должны применяться в определенном порядке. Итоговый результат, а именно значение свойства testsDueTime экземпляра Machine, зависит от того, в какой последовательности будут применены правила. Как вы помните из раздела "Пример задачи", бизнес-правила, регулирующие сроки тестирования, имеют приоритет. Например, если над компьютером будут выполнены тесты 3, 4 и 5, то срок тестирования не должен превышать 10 дней с момента производства, так как бизнес-правило, касающееся даты выполнения теста 3, более приоритетно, чем аналогичные правила, соответствующие тестам 4 и 5. Возникает вопрос, как отразить этот порядок в файле правил Drools? Для этого служит атрибут salience. Правила, присваивающие значения свойства testsDueTime, имеют разные значения данного атрибута. В частности, правило выбора срока тестирования для теста 1 является приоритетным по отношению к правилам, касающихся всех остальных тестов, поэтому оно должно применяться в последнюю очередь. Иными словами, значение, присвоенное этим правилом, должно превалировать над значениями, присвоенными другими правилами, в случае, если тест 1 является лишь одним из тестов, выбранных для данного компьютера. Таким образом, значение атрибута salience для правила, относящегося к тесту 1, будет наименьшим: 10.

Во-вторых, каждое из этих правил содержит два условных элемента. Первое условие служит для проверки наличия экземпляра Machine в рабочей памяти (объект WorkingMemory). Обратите внимание, что в этом условии не проверяются значения свойств экземпляра Machine. Если результатом проверки оказывается true, то объекту Machine присваивается именная ссылка, которая затем используется в секции следствий. Без этой ссылки было бы невозможно установить срок тестирования для данного экземпляра Machine. Второе условие проверяет значение свойства id объекта Test. Следствия каждого правила выполняются только в том случае, когда оба условия оказываются истинными.

В-третьих, условия этих правил не проверяются (и не могут быть проверены) процессором Drools до тех пор, пока экземпляр класса Test не станет частью знаний, другими словами - пока он не будет включен в рабочую память. Это вполне логично, так как до помещения объекта Test в рабочую память, процессор правил не может выполнить проверки, описанные в условной части данных правил. Если вы задумались о том, в какой момент экземпляр Test становится частью знаний, то вспомните код следствий правил выбора тестов для компьютеров (см. листинги 10 и 11), в конце которого есть строки, вставляющие один или несколько объектов Test в рабочую память.

В-четвертых, обратите внимание, что код следствий данных правил достаточно короткий и простой. Это является результатом использования Java-метода setTestsDueTime(), определенного выше в файле правил с использованием ключевого слова function. Именно в этом методе происходит присвоение значения свойству testsDueTime.

Тестирование кода

Теперь, рассмотрев весь код реализации бизнес-логики, самое время проверить его в работе. Для выполнения программы запустите JUnit-тест под названием TestsRulesEngineTest, расположенный в пакете demo.test.

Данный JUnit-тест создает пять компьютеров (экземпляров Machine), каждый с разным набором значений свойств «серийный номер», «тип» и «функции». Для каждого из компьютеров вызывается метод assignTests() класса TestsRulesEngine. Как только данный метод заканчивает свою работу, выполняется ряд проверок (assert), которые контролируют корректность реализации бизнес-логики, описанной в файле правил testRules1.drl (листинг 13). Вы можете изменить данный JUnit-тест, добавив еще несколько экземпляров Machine с различным набором свойств, а затем проверить корректность выполнения правил.

Листинг 13. Контроль корректности реализации бизнес-логики в методе testTestsRulesEngine()
public void testTestsRulesEngine() throws Exception {
   while (machineResultSet.next()) {
      Machine machine = machineResultSet.getMachine();
      testsRulesEngine.assignTests(machine);
      Timestamp creationTs = machine.getCreationTs();
      Calendar calendar = Calendar.getInstance();
      calendar.setTime(creationTs);
      Timestamp testsDueTime = machine.getTestsDueTime();

      if (machine.getSerialNumber().equals("1234A")) {
         assertEquals(3, machine.getTests().size());
         assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST1)));
         assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST2)));
         assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST5)));
         calendar.add(Calendar.DATE, 3);
         assertEquals(calendar.getTime(), testsDueTime);

      } else if (machine.getSerialNumber().equals("1234B")) {
         assertEquals(4, machine.getTests().size());
         assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST5)));
         assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST4)));
         assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST3)));
         assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST2)));
         calendar.add(Calendar.DATE, 7);
         assertEquals(calendar.getTime(), testsDueTime);
...

Еще немного о знаниях

Стоит упомянуть, что кроме добавления объектов в рабочую память вы также можете изменять или удалять существующие в ней объекты. Это можно делать в блоке следствий правил. Если некоторый объект, являющийся частью знаний, был изменен следствием правила, а также если модифицированное свойство проверяется в условных блоках этого или других правил с целью определить, нужно ли их применять, то необходимо вызвать метод update() после изменения объекта. Таким образом, процессор Drools узнает, что объект был модифицирован, и поэтому блоки условий всех правил, в которых происходит обращение к данному объекту (например, для проверки значения одного или нескольких свойств) должны быть выполнены повторно. Это означает, что условия даже текущего правила, которое модифицировало объект в своем блоке следствий, могут быть перепроверены, что в свою очередь может привести к повторному применению правила и даже к бесконечной рекурсии. В случае, если вы не хотите, чтобы была выполнена повторная проверка условий данного правила, в конструкцию rule необходимо добавить необязательный атрибут no-loop со значением true.

Пример подобной ситуации показан в листинге 14, в котором приведен псевдокод описания двух правил. Первое правило (Rule 1) изменяет значение свойства property1 объекта objectA. Затем правило вызывает метод update(), чтобы сообщить процессору правил об этом изменении. Это должно привести к повторной проверке условий всех правил, обращающихся к objectA. Таким образом, условия применения правила Rule 1 также должны быть перепроверены. Более того, условие данного правила вновь окажется истинным (так как значение свойства property2 осталось неизменным), поэтому правило Rule 1 должно быть применено снова, что приведет к бесконечной рекурсии. Для предотвращения этого добавляется атрибут no-loop со значением true, который говорит о том, что данное правило не должно применяться повторно.

Листинг 14. Изменение объекта в рабочей памяти и пример использования атрибута no-loop
...
rule "Rule 1"
salience 100
no-loop true
when
   objectA : ClassA (property2().equals(...))
then
   Object value = ...
   objectA.setProperty1(value);
   update( objectA );
end

rule "Rule 2"
salience 100
when
   objectB : ClassB()
   objectA : ClassA ( property1().equals(objectB) )
   ...
then
   ...
end
...

Если объект более не должен являться частью знаний, то его необходимо удалить из рабочей памяти (листинг 15). Для этого нужно вызвать метод retract() в блоке следствий правила. После того, как объект удален из рабочей памяти, ни одно из условий правил, в которых происходит обращение к данному объекту, не может быть проверено. Вследствие того, что объект более не является частью знаний, подобные правила не будут применяться.

Листинг 15. Удаление объекта из рабочей памяти
...
rule "Rule 1"
salience 100
when
   objectB : ...
   objectA : ...
then
   Object value = ...
   objectA.setProperty1(value);
   retract(objectB);
end

rule "Rule 2"
salience 90
when
   objectB : ClassB ( property().equals(...) )
then
  ...
end
...

В листинге 15 приведен псевдокод определения двух правил. Допустим, результатом проверки условий применения обоих правил будет true. В этом случае правило Rule 1 будет применено первым, так как его значение salience выше, чем у правила Rule 2. Теперь обратите внимание, что в блоке следствий Rule 1 объект objectB удаляется из рабочей памяти (т.е. он больше не является частью знаний). Этот факт нарушает план применения правил, так как теперь правило Rule 2 не может быть выполнено. Несмотря на то что результатом проверки условия Rule 2 было true, данное условие более не является истинным, потому что в нем происходит обращение к объекту, не являющемуся частью знаний (objectB). Если бы в листинге 15 были определены другие, еще не применявшиеся правила, обращающиеся к objectB, то и они более не могли бы быть применены.

В целях рассмотрения конкретного примера изменения текущего состояния знаний в рабочей памяти мы перепишем файл правил, рассмотренный выше. Бизнес-правила останутся в том же виде, что и ранее (см. раздел "Пример задачи"). При этом мы изменим реализацию правил Drools, хотя итоговый результат их применения будет тем же самым. Теперь единственным объектом знаний в рабочей памяти будет являться экземпляр Machine. Таким образом, проверка условий применения правил будет выполняться исключительно на основе свойств этого объекта. Этим данный подход будет отличаться от использовавшегося ранее, в котором также проверялись свойства объектов Test (см. листинг 12). Новая реализация бизнес-правил содержится в файле testRules2.drl. Правила, отвечающие за выбор тестов для компьютера, показаны в листинге 16.

Листинг 16. Правила назначения тестов в файле testRules2.drl
rule "Tests for type1 machine"
lock-on-active true
salience 100

when
   machine : Machine( type == "Type1" )
then
   Test test1 = testDAO.findByKey(Test.TEST1);
   Test test2 = testDAO.findByKey(Test.TEST2);
   Test test5 = testDAO.findByKey(Test.TEST5);
   machine.getTests().add(test1);
   machine.getTests().add(test2);
   machine.getTests().add(test5);
   update( machine );
end

rule "Tests for type2, DNS server machine"
lock-on-active true
salience 100

when
   machine : Machine( type == "Type2", functions contains "DNS Server")
then
   Test test5 = testDAO.findByKey(Test.TEST5);
   Test test4 = testDAO.findByKey(Test.TEST4);
   machine.getTests().add(test5);
   machine.getTests().add(test4);
   update( machine );
end

rule "Tests for type2, DDNS server machine"
lock-on-active true
salience 100

when
   machine : Machine( type == "Type2", functions contains "DDNS Server")
then
   Test test2 = testDAO.findByKey(Test.TEST2);
   Test test3 = testDAO.findByKey(Test.TEST3);
   machine.getTests().add(test2);
   machine.getTests().add(test3);
   update( machine );
end

rule "Tests for type2, Gateway machine"
lock-on-active true
salience 100

when
   machine : Machine( type == "Type2", functions contains "Gateway")
then
   Test test3 = testDAO.findByKey(Test.TEST3);
   Test test4 = testDAO.findByKey(Test.TEST4);
   machine.getTests().add(test3);
   machine.getTests().add(test4);
   update( machine );
end

rule "Tests for type2, Router machine"
lock-on-active true
salience 100

when
   machine : Machine( type == "Type2", functions contains "Router")
then
   Test test3 = testDAO.findByKey(Test.TEST3);
   Test test1 = testDAO.findByKey(Test.TEST1);
   machine.getTests().add(test3);
   machine.getTests().add(test1);
   update( machine );
end
...

Если сравнить определения первого правила в листингах 16 и 10, то становится понятным, что вместо добавления экземпляров Test, выбранных в качестве тестов для компьютера (объекта Machine) в рабочую память, теперь в блоке следствий правила вызывается метод update(), сигнализирующий процессору об изменении объекта Machine. В данном случае изменения объекта заключаются в добавлении к нему набора экземпляров Test. Обратив внимание на остальные правила в листинге 16, вы увидите, что аналогичные действия выполняются во всех случаях добавления тестов к объекту Machine. Добавление экземпляров Test к свойству объекта Machine означает изменение рабочей памяти, о чем посылается уведомление процессору правил.

Обратите внимание на то, что в определении всех правил в листинге 16 используется атрибут active-lock. Его значением всегда является true, в противном случае применение данных правил привело бы к бесконечной рекурсии. Установка этого атрибута в true гарантирует, что правила не будут повторно проверяться и применяться в случае обновления знаний в рабочей памяти (иначе существует опасность бесконечной рекурсии). Атрибут active-lock, по сути, является усиленной версией атрибута no-loop. В то время как последний предотвращает повторное применение правила, в блоке следствий которого произошло изменение рабочей памяти, атрибут active-lock со значением true гарантирует, что ни одно правило в файле не будет применено повторно в результате обновления знаний.

В листинге 17 показаны новые версии остальных правил.

Листинг 17. Правила в файле testRules2.drl, устанавливающие предельный срок тестирования
rule "Due date for Test 5"
salience 50
when
   machine : Machine(tests contains (testDAO.findByKey(Test.TEST5)))
then
   setTestsDueTime(machine, 14);
end

rule "Due date for Test 4"
salience 40
when
   machine : Machine(tests contains (testDAO.findByKey(Test.TEST4)))
then
   setTestsDueTime(machine, 12);
end

rule "Due date for Test 3"
salience 30
when
   machine : Machine(tests contains (testDAO.findByKey(Test.TEST3)))
then
   setTestsDueTime(machine, 10);
end

rule "Due date for Test 2"
salience 20
when
   machine : Machine(tests contains (testDAO.findByKey(Test.TEST2)))
then
   setTestsDueTime(machine, 7);
end

rule "Due date for Test 1"
salience 10
when
   machine : Machine(tests contains (testDAO.findByKey(Test.TEST1)))
then
   setTestsDueTime(machine, 3);
end

Теперь условия этих правил проверяют свойство tests объекта Machine в поисках конкретных экземпляров Test. Таким образом, как упоминалось ранее, при таком подходе правила обращаются только к одному объекту в рабочей памяти (экземпляру Machine), в отличие от предыдущего подхода, при котором происходило обращение к экземплярам Machine и Test.

Для того чтобы протестировать правила в файле testRules2.drl, отредактируйте класс TestsRulesEngine в нашем приложении (см. листинг 7) следующим образом: замените строку "testRules1.drl" на "testRules2.drl". Затем запустите JUnit-тест TestsRulesEngineTest. Все тесты должны завершиться успешно, как и ранее, когда использовался файл testRules1.drl.

О точках останова

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

Например, допустим, что необходимо отладить JUnit-тест TestsRulesEngineTest в виде "Drools Application". Откройте общий диалог Debug в Eclipse. В этом окне есть категория "Drools Application", в которой надо будет создать новую конфигурацию запуска. На закладке Main новой конфигурации есть поля Project (проект) и Main class (главный класс). В первом поле выберите проект Drools4Demo, а во втором введите junit.textui.TestRunner (рисунок 3).

Рисунок 3. Конфигурация запуска класса TestsRulesEngineTest в виде "приложения Drools" (закладка "Main")
Drools Application launch configuration for TestsRulesEngineTest class (Main tab)
Drools Application launch configuration for TestsRulesEngineTest class (Main tab)

Далее перейдите на закладку Arguments и введите -t demo.test.TestsRulesEngineTest в качестве аргумента приложения (рисунок 4). После этого сохраните конфигурацию запуска, нажав на кнопку Apply в правом нижнем углу диалогового окна. Теперь при нажатии на кнопку Debug JUnit-класс TestsRulesEngineTest запустится в режиме отладки приложения Drools. Если добавить точки останова в файл testRules1.drl или testRules2.drl и запустить приложение в данной конфигурации, то отладчик должен остановиться на них.

Рисунок 4. Конфигурация запуска класса TestsRulesEngineTest в виде "приложения Drools" (закладка "Arguments")
Drools Application launch configuration for TestsRulesEngineTest class (Arguments tab)
Drools Application launch configuration for TestsRulesEngineTest class (Arguments tab)

Заключение

Применение процессора правил позволяет значительно упростить реализацию компонентов, отвечающих за бизнес-логику в ваших приложениях Java. Приложения, использующие системы правил для декларативного описания бизнес-логики, часто оказываются легче в сопровождении и дальнейшем развитии, чем остальные. В данной статье было показано, что Drools – это мощная и гибкая реализация процессора правил. Благодаря возможностям и функциям Drools сложная бизнес-логика вашего приложения может быть описана декларативным образом. Кроме того, Drools облегчает процесс изучения и использования декларативного программирования для Java-разработчиков.

Классы, продемонстрированные в данной статье, являются специфичными для Drools. Поэтому, если вы захотите использовать другой процессор правил в данном приложении, то вам придется несколько изменить его код. Благодаря тому что Drools следует спецификации JSR-94, для взаимодействия с классами Drools можно использовать стандартный Java-интерфейс для систем обработки правил (Java Rule Engine API), который играет ту же роль для процессоров правил, что и JDBC для баз данных. Использование данного интерфейса позволяет поменять одну реализацию процессора правил на другую, тоже соответствующую JSR-94, без необходимости внесения изменений в Java-код. Между тем структура файла, в котором содержатся бизнес-правила (в нашем приложении он назывался testRules1.drl), не регламентируется спецификацией JSR-94 и по-прежнему определяется исключительно выбранным процессором правил. В качестве упражнения вы можете модифицировать демонстрационное приложение таким образом, чтобы обращение к классам Drools происходило через интерфейс Java Rule Engine API, а не через Java-классы, специфичные для Drools.


Ресурсы для скачивания


Похожие темы


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java, Open source
ArticleID=367541
ArticleTitle=Реализация бизнес-логики при помощи процессора правил Drools
publish-date=01302009