Toda plataforma de cierta complejidad requiere gestionar reglas de negocio, lógica que cambia con frecuencia o que conviene desacoplar del código. Este artículo analiza en qué casos resulta conveniente externalizar dicha lógica mediante un motor de reglas y en cuáles no, centrándose en Drools, sus distintas formas de definición de reglas (DRL, DMN, tablas de decisión). El ejemplo de código práctico muestra una integración entre Drools con Spring Boot exponiendo una regla de negocio como un endpoint REST que otros servicios pueden consumir.
Son muchas las necesidades de una plataforma para dar servicio a sus usuarios. Almacenamiento de datos, autenticación, catálogo, inventario, contenido, compras, envíos, mensajes, pagos, antifraude, indexación, feature flags, analítica de datos. Y sus correspondientes servicios que dan solución a cada una de esas necesidades para gestionar toda esa complejidad.
Una de esas necesidades son los procesos y en el tema de este artículo las reglas de negocio.
Reglas de negocio
Las reglas de negocio recopilan el conocimiento de los expertos de negocio y automatizan la toma de decisiones, las reglas de negocio definen cómo se comporta el sistema.
La particularidad de las reglas de negocio es que cambian en base a requerimientos de negocio con mucha frecuencia o por requerimientos legales, siendo necesario actualizarlas con cierta prontitud y de forma simple.
Las reglas de negocio se pueden codificar en código sin embargo el código es más difícil de cambiar y complejo cuando las reglas son complejas. Además, los motores que ejecutan reglas de negocio ofrecen características adicionales en la inferencia que un lenguaje de programación no tiene.
Por ello, y debido a que estas reglas idealmente son cambiables por personas de negocio, las reglas de negocio se suelen externalizar del código. Las personas de negocio mantienen las reglas y el sistema simplemente las evalúa.
Una herramienta para definir reglas de negocio de código abierto es Drools. Otras soluciones son Easy Rules pero que está en modo mantenimiento aunque no tiene las mismas características de Drools en cuanto a inferencia.
Por ejemplo, una regla de negocio puede ser cobrar un 5% más a los usuarios de Reino Unido, cuando quede menos de 5 días de una determinada fecha o hacer un descuento para una determinada categoría de productos en cierto periodo de tiempo.
¿Cuándo usar un motor de reglas y cuándo no?
Una pregunta es cuándo compensa añadir la complejidad de un motor de reglas al sistema. La respuesta es cuando las reglas son complejas y la inferencia de los motores de reglas son una funcionalidad deseada o cuando se desea desacoplar el ciclo de vida del código del ciclo de vida de las reglas.
Los cambios en el negocio o los requerimientos legales requieren que el comportamiento del sistema cambie, quizá con cierta frecuencia y urgencia.
Con un motor de reglas el código del sistema es el mismo y el cambio de comportamiento se delega en las reglas, más fáciles de cambiar que el código equivalente que las implemente. Si es posible implementar las reglas con simples ifs en código y no cambian con frecuencia la complejidad de un motor de reglas no compensa.
Procesos de negocio
Relacionado con las reglas de negocio están los procesos de negocio, los procesos son también una de las necesidades habituales de un sistema de cierta complejidad. Hay herramientas para modelar procesos y que estos puedan ser cambiados por personas de negocio.
Sin embargo, modelar estos procesos es complejo aún ofreciendo herramientas gráficas que aparentemente lo facilitan y no llegan a la capacidad de un lenguaje de programación. Por otro lado, la realidad es que estos procesos no suelen ser cambiados por personas de negocio.
Para definir procesos están las herramientas BPMN, aunque para procesos han surgido herramientas de nueva generación como Temporal.
Drools
Drools es una herramienta dentro del ecosistema de KIE en el que hay otros proyectos como Kogito para la automatización de negocio para construir sistemas inteligentes, OptaPlanner un solucionador de restricciones y jBPM para procesos de negocio.
Los motores de reglas de negocio ofrecen un sistema para la inferencia, son un sistema avanzado que se basa en los hechos o datos de entrada junto con las reglas. Las reglas definen cuando los hechos cumplen las condiciones y las acciones que se realizan.
Los motores de reglas en esencia son ifs pero que usan algoritmos como RETE para evaluar los hechos contra las reglas de forma eficiente y efectiva, en el caso de Drools se usa el algoritmo Phreak.
Kogito es una evolución cloud native para reglas de negocio y procesos que los expone mediante una interfaz REST.
El lenguaje de las reglas
Las reglas de Drools se definen con un lenguaje especializado en archivos de texto. Los archivos de las reglas tienen varias secciones en las que destacan las secciones de when que definen cuando se aplica la regla a los hechos en la memoria de trabajo y la sección then que define que acciones se aplican.
La sección when tiene reglas de lógica booleana y filtrado. La sección then puede establecer valores, insertar, actualizar o eliminar hechos de la memoria de trabajo.
Una sección query permite consultar hechos de la memoria de trabajo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package io.github.picodotdev.blogbitix.drools;
import io.github.picodotdev.blogbitix.drools.domain.Applicant;
import io.github.picodotdev.blogbitix.drools.domain.LoanApplication;
rule "Underage"
salience 15
ruleflow-group "applicationGroup"
when
$application : LoanApplication($applicantId: applicantId)
Applicant(id == $applicantId && age < 21)
then
$application.setApproved(false);
$application.setExplanation("Underage");
end
rule "Overage"
salience 15
ruleflow-group "applicationGroup"
when
$application : LoanApplication($applicantId: applicantId)
Applicant(id == $applicantId && age >= 21)
then
$application.setApproved(true);
$application.setExplanation("Old enough");
end
|
loan-application-age-limit.drl
Decision Model and Notation
Otra forma de crear reglas son mediante archivos DMN, un estándar de Object Management Group (OMG) para describir y modelar decisiones operacionales. Estos se crean de forma gráfica con un editor especializado que las guarda las reglas en un archivo XML.
Tablas de decisión en hojas de cálculo
La tercera forma posible de definir reglas es mediante una hoja de cálculo. Una hoja de cálculo tiene la particularidad de que es una herramienta a la que están habituadas las personas de negocio, además de ver en una tabla los diferentes valores de las reglas y una fácil edición. Lo que pueden ser varios archivos de reglas es posible implementarlo en una hoja de cálculo con cierta estructura.
Ejemplo de Drools con Spring Boot
El siguiente es un ejemplo de Drools con Spring Boot que implementa un endpoint REST para invocar la regla. El motor de reglas está embebido en la propia aplicación, actualizar las reglas requeriría actualizar la aplicación.
Exponer las reglas como un endpoint REST permite consumirlas desde otros servicios de la plataforma.
Los modelos de datos.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
package io.github.picodotdev.blogbitix.drools.domain;
public class Applicant {
private String id;
private int age;
public Applicant() {
}
public Applicant(String id, int age) {
this.id = id;
this.age = age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
|
Applicant.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
package io.github.picodotdev.blogbitix.drools.domain;
public class LoanApplication {
private String applicantId;
private String explanation;
private boolean approved;
public LoanApplication() {
}
public LoanApplication(String applicantId) {
this.applicantId = applicantId;
}
public LoanApplication(String applicantId, String explanation, boolean approved) {
this.applicantId = applicantId;
this.explanation = explanation;
this.approved = approved;
}
public String getExplanation() {
return explanation;
}
public void setExplanation(String explanation) {
this.explanation = explanation;
}
public boolean isApproved() {
return approved;
}
public void setApproved(boolean approved) {
this.approved = approved;
}
public String getApplicantId() {
return applicantId;
}
public void setApplicantId(String applicantId) {
this.applicantId = applicantId;
}
@Override
public String toString() {
return "LoanApplication [applicantId=" + applicantId + ", explanation=" + explanation + ", approved=" + approved
+ "]";
}
}
|
LoanApplication.java
Los modelos de las peticiones y respuestas.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package io.github.picodotdev.blogbitix.drools.rest;
import io.github.picodotdev.blogbitix.drools.domain.Applicant;
import io.github.picodotdev.blogbitix.drools.domain.LoanApplication;
public class LoanRequest {
private Applicant applicant;
private LoanApplication loanApplication;
public Applicant getApplicant() {
return applicant;
}
public void setApplicant(Applicant applicant) {
this.applicant = applicant;
}
public LoanApplication getLoanApplication() {
return loanApplication;
}
public void setLoanApplication(LoanApplication loanApplication) {
this.loanApplication = loanApplication;
}
}
|
LoanRequest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package io.github.picodotdev.blogbitix.drools.rest;
import io.github.picodotdev.blogbitix.drools.domain.LoanApplication;
public class LoanResponse {
private LoanApplication loanApplication;
public LoanResponse(LoanApplication loanApplication) {
this.loanApplication = loanApplication;
}
public LoanApplication getLoanApplication() {
return loanApplication;
}
public void setLoanApplication(LoanApplication loanApplication) {
this.loanApplication = loanApplication;
}
}
|
LoanResponse.java
Beans del motor de reglas.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package io.github.picodotdev.blogbitix.drools;
import org.kie.api.KieServices;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieRuntimeFactory;
import org.kie.dmn.api.core.DMNRuntime;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Beans {
@Bean
KieContainer kieContainer() {
KieServices kieServices = KieServices.Factory.get();
return kieServices.getKieClasspathContainer();
}
@Bean
DMNRuntime dnmRuntime(KieContainer kieContainer) {
return KieRuntimeFactory.of(kieContainer.getKieBase()).get(DMNRuntime.class);
}
}
|
Beans.java
Ejemplo de invocación de reglas a través de un endpoint REST.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
package io.github.picodotdev.blogbitix.drools.rest;
...
@RestController
@RequestMapping("/loan")
public class LoanController {
private KieContainer kieContainer;
private DMNRuntime dmnRuntime;
public LoanController(KieContainer kieContainer, DMNRuntime dmnRuntime) {
this.kieContainer = kieContainer;
this.dmnRuntime = dmnRuntime;
}
@PostMapping("/rule")
public ResponseEntity<LoanResponse> rule(@RequestBody LoanRequest loanRequest) {
System.out.println("Applicant id: " + loanRequest.getApplicant().getId());
System.out.println("Applicant age: " + loanRequest.getApplicant().getAge());
List<Command> commands = Arrays.asList(
CommandFactory.newInsert(loanRequest.getApplicant(), "applicant"),
CommandFactory.newInsert(loanRequest.getLoanApplication(), "application"),
new SetActiveAgendaGroup("applicationGroup"),
CommandFactory.newFireAllRules());
KieSession kieSession = kieContainer.newKieSession();
ExecutionResults executionResults = kieSession.execute(CommandFactory.newBatchExecution(commands));
LoanApplication application = (LoanApplication) executionResults.getResults().get("application");
System.out.println("Application: " + application);
return ResponseEntity.ok(new LoanResponse(application));
}
@PostMapping("/decision")
public ResponseEntity<LoanResponse> decision(@RequestBody LoanRequest loanRequest) {
System.out.println("Applicant id: " + loanRequest.getApplicant().getId());
System.out.println("Applicant age: " + loanRequest.getApplicant().getAge());
String namespace = "https://kie.org/dmn/_C83DFD16-A42A-46BE-A843-370444580E0F";
String modelName = "loan-application-age-limit";
DMNModel dmnModel = dmnRuntime.getModel(namespace, modelName);
DMNContext dmnContext = dmnRuntime.newContext();
dmnContext.set("Applicant", loanRequest.getApplicant());
dmnContext.set("Application", loanRequest.getLoanApplication());
DMNResult dmnResult = dmnRuntime.evaluateAll(dmnModel, dmnContext);
HashMap<String, Object> result = (HashMap) dmnResult.getDecisionResults().getFirst().getResult();
LoanApplication application = loanRequest.getLoanApplication();
application.setApproved((boolean) result.get("approved"));
application.setExplanation((String) result.get("explanation"));
return ResponseEntity.ok(new LoanResponse(application));
}
}
|
LoanController.java
1
2
3
4
|
curl -X POST -H 'Accept: application/json' -H 'Content-Type: application/json' -d '{"loanApplication":{"applicantId": "1"}, "applicant":{"id": "1", "age": 16}}' http://localhost:8080/loan/rule
curl -X POST -H 'Accept: application/json' -H 'Content-Type: application/json' -d '{"loanApplication":{"applicantId": "1"}, "applicant":{"id": "1", "age": 25}}' http://localhost:8080/loan/rule
curl -X POST -H 'Accept: application/json' -H 'Content-Type: application/json' -d '{"loanApplication":{"applicantId": "1"}, "applicant":{"id": "1", "age": 16}}' http://localhost:8080/loan/decision
curl -X POST -H 'Accept: application/json' -H 'Content-Type: application/json' -d '{"loanApplication":{"applicantId": "1"}, "applicant":{"id": "1", "age": 25}}' http://localhost:8080/loan/decision
|
curl.sh
1
2
3
4
5
|
{"loanApplication":{"applicantId":"1","explanation":"Underage","approved":false}}
{"loanApplication":{"applicantId":"1","explanation":"Old enough","approved":true}}
{"loanApplication":{"applicantId":"1","explanation":"Underage","approved":false}}
{"loanApplication":{"applicantId":"1","explanation":"Old enough","approved":true}}
|
responses.txt
En apache/incubator-kie-examples hay una colección muy completa de ejemplos reglas y Decision Model and Notation que examinar.
El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run