Definindo e executando regras de negócio com Drools
Esse guia demonstra como sua aplicação Quarkus pode usar o Drools para adicionar automação inteligente e potencializá-la com o mecanismo de regras do Drools.
Pré-requisitos
Para concluir este guia, você precisa:
-
Cerca de 15 minutos
-
Um IDE
-
JDK 17+ instalado com
JAVA_HOME
configurado corretamente -
Apache Maven 3.9.9
-
Opcionalmente, o Quarkus CLI se você quiser usá-lo
-
Opcionalmente, Mandrel ou GraalVM instalado e configurado apropriadamente se você quiser criar um executável nativo (ou Docker se você usar uma compilação de contêiner nativo)
Introdução
Drools é um conjunto de projetos que se concentra na automação inteligente e no gerenciamento de decisões, principalmente fornecendo um em mecanismo de regras baseado em inferência de encadeamento para frente e encadeamento para trás, um mecanismo de decisões DMN e outros projetos. Uma mecanismo de regras é um bloco fundamental para criar um sistema especializado que, em inteligência artificial, é um sistema computacional que emula a capacidade de tomada de decisões de um especialista humano. Você pode ler mais informações no site do Drools.
O Drools permite a definição de regras com dois estilos de programação diferentes: um mais tradicional baseado nos conceitos de uma KieBase, que atua como um repositório de regras de negócio e uma KieSession, que armazena e avalia os dados em tempo de execução com base nessas regras; e outro que utiliza uma unidade de regra como uma abstração única que encapsula as definições de um conjunto de regras e os fatos contra os quais essas regras serão comparadas.
Ambos os estilos são totalmente suportados na extensão do Drools para Quarkus, e este documento explica como usar cada um deles, destacando os prós e os contras de cada abordagem.
Integração do modelo de programação tradicional do Drools com o Quarkus
Este primeiro exemplo demonstra como definir um conjunto de regras usando o estilo tradicional do Drools e como expor sua avaliação em um endpoint REST por meio do Quarkus.
O modelo de domínio desse projeto de exemplo é composto por apenas duas classes, uma solicitação de empréstimo
public class LoanApplication {
private String id;
private Applicant applicant;
private int amount;
private int deposit;
private boolean approved = false;
public LoanApplication(String id, Applicant applicant, int amount, int deposit) {
this.id = id;
this.applicant = applicant;
this.amount = amount;
this.deposit = deposit;
}
}
e o solicitante que a requisitou
public class Applicant {
private String name;
private int age;
public Applicant(String name, int age) {
this.name = name;
this.age = age;
}
}
O conjunto de regras é composto por decisões de negócios para aprovar ou rejeitar uma solicitação, além de uma última regra que coleta todas as solicitações aprovadas em uma lista.
global Integer maxAmount;
global java.util.List approvedApplications;
rule LargeDepositApprove when
$l: LoanApplication( applicant.age >= 20, deposit >= 1000, amount <= maxAmount )
then
modify($l) { setApproved(true) }; // loan is approved
end
rule LargeDepositReject when
$l: LoanApplication( applicant.age >= 20, deposit >= 1000, amount > maxAmount )
then
modify($l) { setApproved(false) }; // loan is rejected
end
// ... more loans approval/rejections business rules ...
rule CollectApprovedApplication when
$l: LoanApplication( approved )
then
approvedApplications.add($l); // collect all approved loan applications
end
O objetivo que queremos alcançar é colocar a avaliação dessas regras em um microsserviço, expondo-as em um endpoint REST desenvolvido com Quarkus. Para isso, basta adicionar a extensão Drools do Quarkus entre as dependências do seu projeto.
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-quarkus</artifactId>
</dependency>
e, nesse ponto, é possível obter uma referência à KieSession, que avalia as regras definidas anteriormente, e usá-la em um endpoint REST, da seguinte forma:
@Path("/find-approved")
public class FindApprovedLoansEndpoint {
@Inject
KieRuntimeBuilder kieRuntimeBuilder;
@POST()
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public List<LoanApplication> executeQuery(LoanAppDto loanAppDto) {
KieSession session = kieRuntimeBuilder.newKieSession();
List<LoanApplication> approvedApplications = new ArrayList<>();
session.setGlobal("approvedApplications", approvedApplications);
session.setGlobal("maxAmount", loanAppDto.getMaxAmount());
loanAppDto.getLoanApplications().forEach(session::insert);
session.fireAllRules();
session.dispose();
return approvedApplications;
}
}
onde uma implementação da interface 'KieRuntimeBuilder' é automaticamente gerada e disponibilizada para injeção pela extensão Drools, e permite obter com uma única instrução, uma instância de qualquer KieBases e KieSessions definidos em seu projeto Drools.
Aqui o LoanAppDto
é um POJO simples usado para enviar várias solicitações de empréstimos para a mesma KieSession
public class LoanAppDto {
private int maxAmount;
private List<LoanApplication> loanApplications;
public int getMaxAmount() {
return maxAmount;
}
public void setMaxAmount(int maxAmount) {
this.maxAmount = maxAmount;
}
public List<LoanApplication> getLoanApplications() {
return loanApplications;
}
public void setLoanApplications(List<LoanApplication> loanApplications) {
this.loanApplications = loanApplications;
}
}
assim, tentando, por exemplo, invocar esse endpoint com um conjunto de solicitações de empréstimo
curl -X POST -H 'Accept: application/json' -H 'Content-Type: application/json' -d
'{"maxAmount":5000,"loanApplications":[
{"id":"ABC10001","amount":2000,"deposit":1000,"applicant":{"age":45,"name":"John"}},
{"id":"ABC10002","amount":5000,"deposit":100,"applicant":{"age":25,"name":"Paul"}},
{"id":"ABC10015","amount":1000,"deposit":100,"applicant":{"age":12,"name":"George"}}
]}'
http://localhost:8080/find-approved
o mecanismo de regras vai avaliá-las novamente em relação às regras de negócio que configuramos anteriormente, retornando a única que, nesse caso, pode ser aprovada de acordo com elas
[{"id":"ABC10001","applicant":{"name":"John","age":45},"amount":2000,"deposit":1000,"approved":true}]
Migrando para o modelo de programação de unidades de regra
Uma unidade de regra é um novo conceito introduzido no Drools que encapsula tanto um conjunto de regras quanto os fatos contra os quais essas regras serão avaliadas. Ela traz uma segunda abstração chamada de fonte de dados, que define as fontes pelas quais os fatos são inseridos, agindo na prática como pontos de entrada tipados. Existem dois tipos de fontes de dados:
-
Stream de dados: uma fonte de dados apenas para adição
-
os assinantes recebem apenas mensagens novas (e possivelmente anteriores)
-
não é possível atualizar/remover
-
o fluxo também pode ser quente/frio na terminologia de "fluxos reativos"
-
-
Armazenamento de dados: uma fonte de dados para dados modificáveis
-
os assinantes podem interagir com o armazenamento de dados, atuando sobre o identificador de fatos
-
Para utilizar unidades de regras em nossa aplicação Quarkus, é necessário adicionar uma segunda dependência.
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-ruleunits-engine</artifactId>
</dependency>
Em essência, uma unidade de regra é composta de duas partes estritamente relacionadas: a definição do fato a ser avaliado e o conjunto de regras que o avaliam. A primeira parte é implementada com um POJO, que, no exemplo do empréstimo, poderia ser algo como o seguinte:
package org.loans;
import org.drools.ruleunits.api.DataSource;
import org.drools.ruleunits.api.DataStore;
import org.drools.ruleunits.api.RuleUnitData;
public class LoanUnit implements RuleUnitData {
private int maxAmount;
private DataStore<LoanApplication> loanApplications;
public LoanUnit() {
this(DataSource.createStore(), 0);
}
public LoanUnit(DataStore<LoanApplication> loanApplications, int maxAmount) {
this.loanApplications = loanApplications;
this.maxAmount = maxAmount;
}
public DataStore<LoanApplication> getLoanApplications() {
return loanApplications;
}
public void setLoanApplications(DataStore<LoanApplication> loanApplications) {
this.loanApplications = loanApplications;
}
public int getMaxAmount() {
return maxAmount;
}
public void setMaxAmount(int maxAmount) {
this.maxAmount = maxAmount;
}
}
Aqui, em vez de usarmos o LoanAppDto
que introduzimos para serializar/deserializar as solicitações JSON, estamos vinculando diretamente a classe que representa a unidade de regras. As duas diferenças principais são que essa classe implementa a interface RuleUnitData
e utiliza um DataStore
em vez de um simples List
contendo as solicitações de empréstimo a serem aprovadas. A primeira é apenas uma interface de marcação para indicar ao mecanismo de regras que essa classe faz parte da definição de uma unidade de regras. O uso de um DataStore
é necessário para permitir que o mecanismo de regras reaja às alterações, disparando novas regras e acionando outras regras. No exemplo, as regras modificam a propriedade aprovada das solicitações de empréstimo. Por outro lado, o valor de maxAmount
pode ser considerado um parâmetro de configuração da unidade de regras e deixado como está: ele será automaticamente processado durante a avaliação das regras com a mesma semântica de uma variável global e será definido automaticamente a partir do valor passado na solicitação JSON, como no primeiro exemplo, permitindo que você ainda possa usá-lo em suas regras.
A segunda parte da unidade de regras é o arquivo DRL que contém as regras pertencentes a essa unidade.
package org.loans;
unit LoanUnit; // no need to using globals, all variables and facts are stored in the rule unit
rule LargeDepositApprove when
$l: /loanApplications[ applicant.age >= 20, deposit >= 1000, amount <= maxAmount ] // oopath style
then
modify($l) { setApproved(true) };
end
rule LargeDepositReject when
$l: /loanApplications[ applicant.age >= 20, deposit >= 1000, amount > maxAmount ]
then
modify($l) { setApproved(false) };
end
// ... more loans approval/rejections business rules ...
// approved loan applications are now retrieved through a query
query FindApproved
$l: /loanApplications[ approved ]
end
Esse arquivo de regras deve declarar o mesmo pacote e uma unidade com o mesmo nome da classe Java que implementa a interface RuleUnitData
para indicar que pertencem à mesma unidade de regras.
Esse arquivo também foi reescrito usando a nova notação OOPath: como antecipado, aqui a fonte de dados atua como um ponto de entrada tipado, e a expressão OOPath tem seu nome como raiz, enquanto as restrições estão entre colchetes, como no exemplo a seguir.
$l: /loanApplications[ applicant.age >= 20, deposit >= 1000, amount <= maxAmount ]
Alternativamente, você ainda pode usar a antiga sintaxe do DRL, especificando o nome da fonte de dados como um entry-point, com a desvantagem de que, nesse caso, é necessário especificar novamente o tipo do objeto correspondente, mesmo que o mecanismo possa inferi-lo a partir do tipo da fonte de dados, como mostrado a seguir.
$l: LoanApplication( applicant.age >= 20, deposit >= 1000, amount <= maxAmount ) from entry-point loanApplications
Por fim, note que a última regra que reunia todos os pedidos de empréstimos aprovados em um 'List' global foi substituída por uma consulta que simplesmente os recupera. Uma das vantagens de usar uma unidade de regra é que ela define claramente o contexto da computação, ou seja, os fatos que devem ser passados como entrada para a avaliação das regras. Da mesma forma, a consulta define qual é o resultado esperado por essa avaliação.
Essa definição clara dos limites da computação permite que o Drools também gere automaticamente uma classe que executa a consulta e retorna seus resultados, juntamente com um endpoint REST que recebe a unidade de regra como entrada, a repassa para o executor da consulta e retorna seu resultado como saída.
Você pode ter quantas consultas quiser, e para cada uma delas será gerado um endpoint REST diferente, com o mesmo nome da consulta transformado de camel case (como FindApproved) para um formato separado por hífens (comofind-approved).
Um exemplo mais abrangente
Nesse exemplo mais abrangente e completo, vamos expandir uma aplicação básica do Quarkus com algumas regras simples para inferir possíveis problemas no status de uma configuração de automação doméstica.
Definiremos uma Unidade de Regras do Drools e as regras no formato DRL.
Vamos integrar a Unidade de Regras a um bean CDI padrão do Quarkus, para utilização na aplicação Quarkus (por exemplo, conectando mensagens MQTT do Kafka, etc.).
Pré-requisitos
Para concluir este guia, você precisa:
-
menos de que 15 minutos
-
uma IDE
-
JDK 17+ instalado com
JAVA_HOME
configurado corretamente -
Apache Maven 3.9.3+
-
Docker
-
GraalVM instalado se você quiser executar em modo nativo
Criando o projeto Maven
Primeiro, precisamos de um novo projeto Quarkus. Para criar um novo projeto Quarkus, você pode usar como referência o Guia de Quarkus e Maven
Quando o seu projeto Quarkus estiver configurado, você pode adicionar as extensões do Drools para Quarkus ao seu projeto adicionando as seguintes dependências ao seu pom.xml
:
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-quarkus</artifactId>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-ruleunits-engine</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
Escrevendo o aplicativo
Vamos começar pelo modelo de domínio do aplicativo.
O objetivo desta aplicação é inferir possíveis problemas no status de uma configuração de automação residencial, para isso, criamos os modelos de domínio necessários para representar o status de sensores, dispositivos e outros elementos dentro da casa.
Modelo de domínio do dispositivo de luz:
package org.drools.quarkus.quickstart.test.model;
public class Light {
private final String name;
private Boolean powered;
public Light(String name, Boolean powered) {
this.name = name;
this.powered = powered;
}
// getters, setters, etc.
}
Modelo de domínio da câmera de segurança CCTV:
package org.drools.quarkus.quickstart.test.model;
public class CCTV {
private final String name;
private Boolean powered;
public CCTV(String name, Boolean powered) {
this.name = name;
this.powered = powered;
}
// getters, setters, etc.
}
Modelo de domínio do Smartphone detectado no WiFi:
package org.drools.quarkus.quickstart.test.model;
public class Smartphone {
private final String name;
public Smartphone(String name) {
this.name = name;
}
// getters, setters, etc.
}
Classe de alerta para armazenar informações sobre os possíveis problemas detectados:
package org.drools.quarkus.quickstart.test.model;
public class Alert {
private final String notification;
public Alert(String notification) {
this.notification = notification;
}
// getters, setters, etc.
}
Em seguida, criamos um arquivo de regras rules.drl
dentro da pasta src/main/resources/org/drools/quarkus/quickstart/test
no projeto Quarkus.
package org.drools.quarkus.quickstart.test;
unit HomeRuleUnitData;
import org.drools.quarkus.quickstart.test.model.*;
rule "No lights on while outside"
when
$l: /lights[ powered == true ];
not( /smartphones );
then
alerts.add(new Alert("You might have forgot one light powered on: " + $l.getName()));
end
query "AllAlerts"
$a: /alerts;
end
rule "No camera when present at home"
when
accumulate( $s: /smartphones ; $count : count($s) ; $count >= 1 );
$l: /cctvs[ powered == true ];
then
alerts.add(new Alert("One CCTV is still operating: " + $l.getName()));
end
Nesse arquivo, há algumas regras de exemplo para decidir se o status geral da casa é considerado inadequado, acionando o(s) Alerta
(s) necessário(s).
O paradigma central da Unidade de Regra foi introduzido no Drools 8 para ajudar os usuários a encapsular o conjunto de regras e os fatos contra os quais essas regras serão combinadas; você pode ler mais informações na documentação do Drools.
Os fatos serão inseridos em um DataStore
, um ponto de entrada com segurança de tipo. Para que tudo funcione, precisamos definir tanto a Unidade de Regra quanto o DataStore.
package org.drools.quarkus.quickstart.test;
import org.drools.quarkus.quickstart.test.model.Alert;
import org.drools.quarkus.quickstart.test.model.CCTV;
import org.drools.quarkus.quickstart.test.model.Light;
import org.drools.quarkus.quickstart.test.model.Smartphone;
import org.drools.ruleunits.api.DataSource;
import org.drools.ruleunits.api.DataStore;
import org.drools.ruleunits.api.RuleUnitData;
public class HomeRuleUnitData implements RuleUnitData {
private final DataStore<Light> lights;
private final DataStore<CCTV> cctvs;
private final DataStore<Smartphone> smartphones;
private final DataStore<Alert> alerts = DataSource.createStore();
public HomeRuleUnitData() {
this(DataSource.createStore(), DataSource.createStore(), DataSource.createStore());
}
public HomeRuleUnitData(DataStore<Light> lights, DataStore<CCTV> cctvs, DataStore<Smartphone> smartphones) {
this.lights = lights;
this.cctvs = cctvs;
this.smartphones = smartphones;
}
public DataStore<Light> getLights() {
return lights;
}
public DataStore<CCTV> getCctvs() {
return cctvs;
}
public DataStore<Smartphone> getSmartphones() {
return smartphones;
}
public DataStore<Alert> getAlerts() {
return alerts;
}
}
Testando a aplicação
Podemos criar um teste padrão Quarkus e JUnit para verificar o comportamento da Unidade de Regra e das regras definidas, de acordo com determinado conjunto de cenários.
package org.drools.quarkus.quickstart.test;
@QuarkusTest
public class RuntimeTest {
@Inject
RuleUnit<HomeRuleUnitData> ruleUnit;
@Test
public void testRuleOutside() {
HomeRuleUnitData homeUnitData = new HomeRuleUnitData();
homeUnitData.getLights().add(new Light("living room", true));
homeUnitData.getLights().add(new Light("bedroom", false));
homeUnitData.getLights().add(new Light("bathroom", false));
RuleUnitInstance<HomeRuleUnitData> unitInstance = ruleUnit.createInstance(homeUnitData);
List<Map<String, Object>> queryResults = unitInstance.executeQuery("AllAlerts");
assertThat(queryResults).isNotEmpty().anyMatch(kv -> kv.containsValue(new Alert("You might have forgot one light powered on: living room")));
}
@Test
public void testRuleInside() {
HomeRuleUnitData homeUnitData = new HomeRuleUnitData();
homeUnitData.getLights().add(new Light("living room", true));
homeUnitData.getLights().add(new Light("bedroom", false));
homeUnitData.getLights().add(new Light("bathroom", false));
homeUnitData.getCctvs().add(new CCTV("security camera 1", false));
homeUnitData.getCctvs().add(new CCTV("security camera 2", true));
homeUnitData.getSmartphones().add(new Smartphone("John Doe's phone"));
RuleUnitInstance<HomeRuleUnitData> unitInstance = ruleUnit.createInstance(homeUnitData);
List<Map<String, Object>> queryResults = unitInstance.executeQuery("AllAlerts");
assertThat(queryResults).isNotEmpty().anyMatch(kv -> kv.containsValue(new Alert("One CCTV is still operating: security camera 2")));
}
@Test
public void testNoAlerts() {
HomeRuleUnitData homeUnitData = new HomeRuleUnitData();
homeUnitData.getLights().add(new Light("living room", false));
homeUnitData.getLights().add(new Light("bedroom", false));
homeUnitData.getLights().add(new Light("bathroom", false));
homeUnitData.getCctvs().add(new CCTV("security camera 1", true));
homeUnitData.getCctvs().add(new CCTV("security camera 2", true));
RuleUnitInstance<HomeRuleUnitData> unitInstance = ruleUnit.createInstance(homeUnitData);
List<Map<String, Object>> queryResults = unitInstance.executeQuery("AllAlerts");
assertThat(queryResults).isEmpty();
}
}
Conectando a Unidade de Regra com os beans CDI do Quarkus
Agora podemos conectar a Unidade de Regra a um bean CDI padrão do Quarkus, para uso geral na aplicação Quarkus.
Por exemplo, isso pode ser útil mais tarde para conectar o relatório de status dos dispositivos via MQTT através do Kafka, usando as extensões apropriadas do Quarkus.
Criamos um bean CDI simples para abstrair o uso da API Rule Unit:
package org.drools.quarkus.quickstart.test;
@ApplicationScoped
public class HomeAlertsBean {
@Inject
RuleUnit<HomeRuleUnitData> ruleUnit;
public Collection<Alert> computeAlerts(Collection<Light> lights, Collection<CCTV> cameras, Collection<Smartphone> phones) {
HomeRuleUnitData homeUnitData = new HomeRuleUnitData();
lights.forEach(homeUnitData.getLights()::add);
cameras.forEach(homeUnitData.getCctvs()::add);
phones.forEach(homeUnitData.getSmartphones()::add);
RuleUnitInstance<HomeRuleUnitData> unitInstance = ruleUnit.createInstance(homeUnitData);
var queryResults = unitInstance.executeQuery("AllAlerts");
List<Alert> results = queryResults.stream()
.flatMap(m -> m.values().stream()
.filter(Alert.class::isInstance)
.map(Alert.class::cast))
.collect(Collectors.toList());
return results;
}
}
Os mesmos cenários de teste podem ser refatorados usando esse bean CDI adequadamente.
package org.drools.quarkus.quickstart.test;
@QuarkusTest
public class BeanTest {
@Inject
HomeAlertsBean alerts;
@Test
public void testRuleOutside() {
Collection<Alert> computeAlerts = alerts.computeAlerts(
List.of(new Light("living room", true), new Light("bedroom", false), new Light("bathroom", false)),
Collections.emptyList(),
Collections.emptyList());
assertThat(computeAlerts).isNotEmpty().contains(new Alert("You might have forgot one light powered on: living room"));
}
@Test
public void testRuleInside() {
Collection<Alert> computeAlerts = alerts.computeAlerts(
List.of(new Light("living room", true), new Light("bedroom", false), new Light("bathroom", false)),
List.of(new CCTV("security camera 1", false), new CCTV("security camera 2", true)),
List.of(new Smartphone("John Doe's phone")));
assertThat(computeAlerts).isNotEmpty().contains(new Alert("One CCTV is still operating: security camera 2"));
}
@Test
public void testNoAlerts() {
Collection<Alert> computeAlerts = alerts.computeAlerts(
List.of(new Light("living room", false), new Light("bedroom", false), new Light("bathroom", false)),
List.of(new CCTV("security camera 1", true), new CCTV("security camera 2", true)),
Collections.emptyList());
assertThat(computeAlerts).isEmpty();
}
}