JBoss Rules / Drools silnik reguł biznesowych.
Co to jest Drools?
W ogólności jest to biblioteka do przetwarzania reguł biznesowych. Regułą nazywamy w ogólności zestaw dwóch zbiorów. Pierwszy to część decyzyjna. W niej odbywa się sprawdzanie, czy podanych faktach zachodzą podane warunki. Drugi to część operacyjna, w której odbywają się działania na danych. Zadaniem silnika reguł jest zarządzanie uruchamianiem reguł, kolejnością ich wykonania, sprawdzaniem warunków oraz co jest dość rzadko spotykane optymalizacją. Mówiąc prościej, silnik reguł na podstawie pewnych danych potrafi stworzyć hierarchię zajebiście wielkich ifów…
Gdzie używać?
Drools są przewidziane do korzystania w przypadkach, gdy reguła biznesowa jest trudna do przełożenia na algorytm, albo poziom komplikacji algorytmu znacznie przekracza możliwości jego implementacji. Drools nie są przewidziane do pracy ciągłej tak jak CEPW. Nie są też silnikiem zdarzeniowym. Znacznie bliżej mu do programowania logicznegoW w stylu PrologaW.
Jak używać?
Czas przyjrzeć się kodowi prostej aplikacji w Drools. Zrobimy tak. Przygotujemy prosty formularz, w którym będą dwie dane. Pierwsza to marka sprzedanego samochodu (wybór z listy). Druga to cena, po jakiej sprzedano samochód. Reguły będą następujące:
- Jeżeli handlowiec sprzedał Malucha, to dostanie premię wysokości 8%.
- Jeżeli sprzedał Ferrari, dostanie premię 4%.
- Jeżeli dodatkowo sprzedał samochód po cenie wyższej niż 150% ceny nominalnej, to dostanie dodatkowo 5% premii od nadwyżki.
- Jeżeli sprzedał samochód za mniej niż 80% wartości, to jego premia zostanie zmniejszona o 10% różnicy.
- Jeżeli sprzedał samochód za ponad 200% wartości, to otrzyma 3% premii od nadwyżki. Premia ta jest nadpisująca w stosunku to rej z pkt 3.
Jak widać, reguły są dość proste. Jedyny problem stanowią reguły 3 i 5, które muszą się nadpisywać. Można do nich podejść na dwa sposoby. Zamienić je tak, że otrzymamy reguły:
- Jeżeli cena sprzedaży należy do przedziału [150, 200)%, to handlowiec dostaje 5% od nadwyżki.
- Jeżeli cena sprzedaży jest większa niż 200%, to handlowiec dostaje 3% od nadwyżki.
Drugą metodą jest dodanie jeszcze jednej reguły:
- Jeżeli istnieje już prowizja za cenę, to ją nadpisz.
Przedstawię tu pierwsze podejście, a drugie będzie rozważane przy okazji omawiania DP.
Zaczynamy kodowanie. Na początek tradycyjnie pom.xml:
Listing 1. pom.xml
<?xml version="1.0"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>eu.runelord.drools</groupId>
<artifactId>DroolsExample</artifactId>
<name>Drools Example</name>
<version>0.0.1-SNAPSHOT</version>
<description>Przykładowy projekt Drools</description>
<url>http://blog.runelord.eu/?p=30</url>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-help-plugin</artifactId>
<version>2.0.2</version>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>jboss</id>
<url>http://repository.jboss.com/maven2/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-decisiontables</artifactId>
<version>4.0.7</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-compiler</artifactId>
<version>4.0.7</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-analytics</artifactId>
<version>4.0.7</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-jsr94</artifactId>
<version>4.0.7</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-core</artifactId>
<version>4.0.7</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-repository</artifactId>
<version>4.0.7</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-ant</artifactId>
<version>4.0.7</version>
</dependency>
</dependencies>
</project>
Ok. Mamy już pom.xml czas na trochę kodowania w Swingu. Tworzymy klasę Kontrakt:
Listing 2. Klasa Kontrakt
package eu.runelord.drools;
import java.util.HashSet;
import java.util.Set;
public class Kontrakt {
private String name;
private Double cena;
private Set<Double> prowizja = new HashSet<Double>();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getCena() {
return cena;
}
public void setCena(Double cena) {
this.cena = cena;
}
public Set<Double> getProwizja() {
return prowizja;
}
public void setProwizja(Double prowizja) {
this.prowizja.add(prowizja);
}
@Override
public String toString() {
return "Nazwa: " + getName() + " Cena: " + getCena() + " prowizja: "
+ getProwizja();
}
}
Następnie tworzymy klasę main:
Listing 3. Klasa DroolsTest. Główna klasa programu.
package eu.runelord.drools;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import org.drools.StatefulSession;
public class DroolsTest {
public static final String RESOURCE_DIR = "src/main/resources/";
public static void main(String[] args) throws Exception {
final RuleFactory ruleFactory = new RuleFactory();
final StatefulSession session = ruleFactory.getStatefulSession(ruleFactory
.createRuleBase(ruleFactory.createPackage()));
final MainFrame mainFrame = new MainFrame();
mainFrame.getWylicz().addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
Kontrakt kontrakt = new Kontrakt();
kontrakt.setName(mainFrame.getMarkaCombo().getSelectedItem()
.toString());
kontrakt.setCena(Double.parseDouble(mainFrame.getCena()
.getText()));
session.insert(kontrakt);
session.setGlobal("bMaluch", 100.0);
session.setGlobal("bFerrari", 1000.0);
session.fireAllRules();
System.out.println(kontrakt);
}
});
mainFrame.setVisible(true);
}
}
i formularz:
Listing 4. Formularz do pobierania danych i wyświetlania wyników
package eu.runelord.drools;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;
public class MainFrame extends JFrame {
private static final long serialVersionUID = 2686859320327208582L;
private JButton wylicz;
private JLabel prowizja;
private List<Character> dozwolone;
private JTextField cena;
private JComboBox markaCombo;
private void init() {
setDefaultCloseOperation(EXIT_ON_CLOSE);
setMinimumSize(new Dimension(300, 128));
setLayout(new GridLayout(4, 1));
setTitle("Drools Example");
markaCombo = new JComboBox();
Vector<String&ht; marka = new Vector<String&ht;();
marka.add("Maluch");
marka.add("Ferrari");
markaCombo.setModel(new DefaultComboBoxModel(marka));
cena = new JTextField();
dozwolone = new LinkedList<Character>();
dozwolone.add('1');
dozwolone.add('2');
dozwolone.add('3');
dozwolone.add('4');
dozwolone.add('5');
dozwolone.add('6');
dozwolone.add('7');
dozwolone.add('8');
dozwolone.add('9');
dozwolone.add('0');
cena.addKeyListener(new KeyListener() {
public void keyPressed(KeyEvent e) {
}
public void keyReleased(KeyEvent e) {
}
public void keyTyped(KeyEvent e) {
if (!dozwolone.contains(e.getKeyChar()))
e.consume();
}
});
prowizja = new JLabel();
wylicz = new JButton("Wylicz prowizję");
add(markaCombo);
add(cena);
add(prowizja);
add(wylicz);
}
public MainFrame() {
init();
}
public JButton getWylicz() {
return wylicz;
}
public void setWylicz(JButton wylicz) {
this.wylicz = wylicz;
}
public JLabel getProwizja() {
return prowizja;
}
public void setProwizja(JLabel prowizja) {
this.prowizja = prowizja;
}
public List<Character> getDozwolone() {
return dozwolone;
}
public void setDozwolone(List<Character> dozwolone) {
this.dozwolone = dozwolone;
}
public JTextField getCena() {
return cena;
}
public void setCena(JTextField cena) {
this.cena = cena;
}
public JComboBox getMarkaCombo() {
return markaCombo;
}
public void setMarkaCombo(JComboBox markaCombo) {
this.markaCombo = markaCombo;
}
}
Teraz czas na RuleFactory:
Listing 5. Klasa RuleFactory, która będzie nam ładowała reguły
package eu.runelord.drools;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import org.drools.RuleBase;
import org.drools.RuleBaseFactory;
import org.drools.StatefulSession;
import org.drools.StatelessSession;
import org.drools.compiler.DroolsParserException;
import org.drools.compiler.PackageBuilder;
import org.drools.rule.Package;
public class RuleFactory {
public Package createPackage() throws DroolsParserException, IOException{
PackageBuilder builder = new PackageBuilder();
Reader source = new FileReader(new File(DroolsTest.RESOURCE_DIR + "package1.drl"));
builder.addPackageFromDrl(source);
Package pkg = builder.getPackage();
return pkg;
}
public RuleBase createRuleBase(Package _package) throws Exception{
RuleBase ruleBase = RuleBaseFactory.newRuleBase();
ruleBase.addPackage(_package);
return ruleBase;
}
public StatefulSession getStatefulSession(RuleBase ruleBase){
StatefulSession session = ruleBase.newStatefulSession();
return session;
}
public StatelessSession getStatelessSession(RuleBase ruleBase){
StatelessSession session = ruleBase.newStatelessSession();
return session;
}
}
Tak oto mamy cały kod naszego rozwiązania od strony javy. Na razie wygląda to troszkę przerażająco, ale w ogólności kod ten posłuży mi do kolejnej opowieści o refaktoryzacji i testach.
Trzeba teraz wytłumaczyć kilka rzeczy. Klasa RuleBase jest sercem silnika. To w niej przechowywane są zasady w pakietach (reprezentowane przez klasę Package) i to z niej pochodzą wszystkie sesje.
Rozróżniamy dwa rodzaje sesji reprezentowane przez klasy StatefulSession i StatelessSession. Różnica polega na sposobie połączenia z pamięcią. Pierwszy rodzaj sesji jest związany z pamięcią na stałe, co oznacza, że będzie reagował na zmiany stanu pamięci oraz na zmiany w samych regułach. Drugi rodzaj jest swego rodzaju zdjęciem (snapshot) stanu pamięci i reguł w pewnej chwili. Po utworzeniu jest odłączany i osierocony stoi sobie w rzeczywistości.
Każda reguła musi należeć do pakietu. Obowiązują takie same zasady jak przy pakietach javy z tą różnicą, że plik .drl nie musi być w strukturze katalogów odpowiadającej strukturze pakietów.
Całość działa dość prosto i nie przysparza problemów. Zresztą to jest cała magia Drools, że można całą pracę przerzucić na analityka, a samemu wykonać proste zadanie. Problemem jest zazwyczaj zrozumienie przez programistę, że jego rola ograniczona jest do tworzenia kodu wiążącego obiekty dostarczane przez użytkownika z sesją Drools oraz wyzwalacza reguł.
Zajmijmy się teraz implementacją reguł. Na początek ustalmy, że ceną bazową Malucha jest 100, a Ferrari 1000 przyda się to przy liczeniu prowizji. Niestety nie udało mi się zmusić Drools do pracy z tymi wartościami jako stałymi. Same reguły definiuje się dość prosto. Najpierw podajemy nazwę reguły, potem modyfikatory (no-loop oznacza niezapętlanie się reguły), a następnie ciało. Na koniec należy zaktualizować WorkingMemory za pomocą update lub zdjąć obiekt ze stosu za pomocą retract.
Listing 7. Plik reguł
package eu.runelord.drools
global java.lang.Double bMaluch;
global java.lang.Double bFerrari;
# 1
rule "reguła bazowa dla malucha"
no-loop
when
$kontrakt : Kontrakt ( name == "Maluch" )
then
System.out.println($kontrakt.getName());
$kontrakt.setProwizja(bMaluch * 0.08);
update($kontrakt);
end
# 2
rule "reguła bazowa dla ferrari"
no-loop
when
$kontrakt : Kontrakt ( name == "Ferrari" && prowizja > 0 )
then
System.out.println($kontrakt.getName());
$kontrakt.setProwizja(bMaluch * 0.04);
update($kontrakt);
end
# 3
rule "prowizja za [150, 200) procent ceny dla Ferrari"
no-loop
when
$kontrakt : Kontrakt ( name == "Ferrari" && cena >= (1.5 * 1000) && cena = (1.5 * 100) && cena 0)
then
System.out.println( "kontrakt na malucha za mniej niż 80%" );
$kontrakt.setProwizja(($kontrakt.getCena() - bMaluch) * 0.1);
retract($kontrakt);
end
# 5
rule "prowizja za ponad 200 procent ceny dla Ferrari"
no-loop
when
$kontrakt : Kontrakt ( name == "Ferrari" && cena >= (2 * 1000))
then
System.out.println( "kontrakt na ferrari za ponad 200%" );
$kontrakt.setProwizja(($kontrakt.getCena() - bFerrari) * 0.03);
retract($kontrakt);
end
rule "prowizja za ponad 200 procent ceny dla Malucha"
no-loop
when
$kontrakt : Kontrakt ( name == "Maluch" && (cena >= (2 * 100)))
then
System.out.println( "kontrakt na malucha za ponad 200%");
$kontrakt.setProwizja(($kontrakt.getCena() - bMaluch) * 0.03);
retract($kontrakt);
end
Całość odpalamy i jesteśmy szczęśliwi.