Jugando con Drools 5.0 – #2 Drools Expert

Continuando con el ejemplo propuesto en el post anterior, vamos a analizar las reglas propuestas, junto con el modelo de dominio (objetos) propuestos para el ejemplo.

Hay que tener en cuenta que este modelo, fue creado a modo de ejemplo, y puede no contemplar algunas situaciones. Asi que acepto sugerencias, si el ejemplo no les parece el mas adecuado, o se les ocurre algún otro modelo de dominio con el cual ejemplificar.

Les recomiendo que si no leyeron el post anterior, lo lean, ya que explica la estructura del proyecto propuesto y aclara un poco las APIs básicas que vamos a utilizar cuando definimos conocimiento en forma de reglas de negocio dentro de Drools 5.0.

Modelo de dominio

En este ejemplo, vamos a utilizar un modelo de dominio bastante simple para crear algunas reglas de negocio simples. La idea de este dominio es simular un escenario donde podemos definir distintos juegos de cartas y dependiendo de la cantidad de jugadores que el juego necesite, crear una ronda cuando se pueda empezar con algún juego. En este caso, vamos a ver como se podría escribir algunas reglas para que cuando se cumplan las condiciones, el juego definido comience.

Dominio en el Ejemplo
Dominio en el Ejemplo

Como podemos ver en la imagen, la clase Juego nos permitiría definir el nombre del juego y algunos datos genéricos que el mismo debería almacenar. Por ejemplo, todo juego tiene una relación con un Mazo de cartas y cuantas cartas por mano se deberán repartir a los jugadores de dicho juego.

Asi también estan definidas las clases Carta y Jugador, que mantienen información genérica sobre estas entidades que se utilizaran en todos los juegos.

Como podran apreciar, estas clases son simples POJOS (Plain Old Java Objects), por lo tanto, técnicamente no hay mucho que aclarar. Estas clases representaran las entidades que nuestro dominio necesita poder representar las interacciones de la realidad. En otras palabras, estas clases representan la información que se manejara en un escenario real donde tengamos Juegos de cartas.

Especificando reglas de negocio

Una vez que conocemos nuestro modelo de dominio, ya estamos listos para empezar a escribir reglas que van a actuar sobre el. Para esto, vamos  a crear un archivo de texto plano en el directorio /src/main/resources/rules llamado juego.drl.

Para los que no conocen DRL es la extensión que se suele utilizar para los archivos que van a contener reglas de negocios expresados en el lenguaje que propone Drools. A continuación vemos este archivo que pueden encontrar dentro del directorio antes mencionado.


package com.wordpress.salaboy.playing.with.drools
import com.wordpress.salaboy.playing.with.drools.model.Juego;
import com.wordpress.salaboy.playing.with.drools.model.Jugador;
import com.wordpress.salaboy.playing.with.drools.model.Dealer;
import com.wordpress.salaboy.playing.with.drools.model.MazoPoker;
import com.wordpress.salaboy.playing.with.drools.model.Mazo;
import com.wordpress.salaboy.playing.with.drools.model.Ronda;
import com.wordpress.salaboy.playing.with.drools.model.Carta;
import java.util.ArrayList;

query "Jugador por nombre" (String qnombre)
  jugador : Jugador( nombre ==  qnombre)
end

rule "Comienza una ronda de Poker cuando hay n jugadores"
  when
    $juego: Juego(nombre == "Poker")
    $jugadores: ArrayList( size == 2 ) from collect (Jugador())
  then
    $juego.setMazo(new MazoPoker());
    Dealer dealer = new Dealer("Poker Dealer");
    insert(dealer);
end

rule "Iniciar Ronda Poker"
  when
    $juego: Juego(nombre == "Poker")
    $dealer: Dealer()
  then
    $dealer.addCartas($juego.getMazo().getMano(5));
    insert(new Ronda());
end

rule "Nueva Ronda"
  when
    Ronda()
    $juego: Juego(nombre == "Poker")
    $jugador: Jugador()
    ArrayList(size == 0)  from collect ( Carta() from $jugador.cartas)
    //eval($jugador.getCartas().size() == 0)
   then
     $jugador.addCartas($juego.getMazo().getMano($juego.getCantidadCartasPorMano()));
end

Como podemos ver, en el archivo juego.drl, se definen actualmente 3 reglas y una consulta (query). Cada regla delimitada por las palabras reservadas del lenguaje “rule” y “end“.

Una Regla

Cada una de estas reglas tiene un nombre asociado y dos secciones internas que definen la condición de la regla y la consecuencia que se ejecutara cuando la condición sea cierta. Como podemos apreciar las palabras reservadas “when” y “then” son utilizadas para demarcar estas secciones dentro de cada regla.

Luego de la palabra reservada “when“, expresaremos la condición que debera cumplirse para que esta regla se ejecute. Esta condición se expresa en un lenguaje especial donde las restricciones de la condición se expresan utilizando tipos de objetos y filtrando por propiedades. Este lenguaje es llamado DRL (Drools Rule Language) y es muy sencillo de aprender.

Analicemos juntos una de estas reglas:

rule "Comienza una ronda de Poker cuando hay n jugadores"
  when
    $juego: Juego(nombre == "Poker")
    $jugadores: ArrayList( size == 2 ) from collect (Jugador())
  then
    $juego.setMazo(new MazoPoker());
    Dealer dealer = new Dealer("Poker Dealer");
    insert(dealer);
end

La Condición

Como podemos ver, la condición de esta regla especifica dos restricciones que deben cumplirse para que la consecuencia se ejecute. Analizaremos estas dos restricciones y vamos a aprovechar para hacer algunos comentarios de la sintaxis y la semántica con la que se escriben estas restricciones.

 when
    $juego: Juego(nombre == "Poker")
    $jugadores: ArrayList( size == 2 ) from collect (Jugador())

Es importante notar, que hay dos restricciones expresadas aqui, ambas unidas por un operador AND implícito. La dos restricciones que tenemos en esta condición pueden ser expresadas de la siguiente manera en lenguaje natural:

SI…

  • Hay un Juego que se llama Poker
  • Y hay 2 jugadores

Entonces… Consecuencia

Por lo tanto si estas restricciones se cumplen, la consecuencia de la regla se ejecutara. La primer restricción “Hay un Juego que se llama Poker” es expresada con la siguiente linea en Drools:

Juego(nombre == "Poker")

Donde lo unico que estamos expresando aqui es que en la memoria de trabajo que Drools utiliza, tendra que haber una instancia del objecto Juego, por eso se utiliza el tipo de la clase (con la J mayúscula) y luego se hace una restriccion de valor de propiedad, en este caso, que el valor de la propiedad nombre sea igual a Poker. En Drools cada instancia de objeto que es insertada en la memoria de trabajo es tratada con el nombre de Hecho. Esto hace referencia a que cada objecto que insertemos, va a generar en Drools un hecho cierto del mundo real sobre el cual queremos hacer inferencias y deducciones.

La siguiente restricción un poco mas compleja, sin embargo, lo unico que hace es contar que hayan dos jugadores para comenzar el juego. Basicamente para esto, realiza una búsqueda en la memoria de trabajo sobre todas las instancias del objeto Jugador que hayamos insertado y cuando sean dos evalúa como verdadera.

Esta bueno notar, que como con Drools escribimos código declarativo y no imperativo, no podríamos escribir esta segunda parte de la restricción de la siguiente manera:

 when
    $juego: Juego(nombre == "Poker")
    Jugador()
    Jugador()

Debido a que aquí estaríamos expresando que por cada par de Jugadores se debería empezar un Juego. Esto causaría que si en la memoria de trabajo tuviéramos 2 instancias de objetos Jugador, la consecuencia se ejecutaría dos veces, ya que el par [Jugador 1, Jugador 2] es distinto al par [Jugador 2, Jugador 1].

Como ultima aclaración sobre las restricciones en las condiciones, vale la pena mencionar los bindeos/enlaces a variables que se estan realizando en ambas restricciones con: $juego y $jugadores. Estas variables, luego pueden ser usadas dentro de restricciones siguientes o en la consecuencia de la regla, como veremos a continuación.

La consecuencia

Pasando a la consecuencia, solamente hay que aclarar que podemos expresar cualquier código Java que queramos que se ejecute cuando las restricciones de la condición son satisfechas.

then
    $juego.setMazo(new MazoPoker());
    Dealer dealer = new Dealer("Poker Dealer");
    insert(dealer);

Como podemos ver, hacemos uso de dos facilidades que son agregadas a la condición. La primer facilidad usada aquí es la la variable que relacionamos en la condición ($juego) y luego la función/método insert que nos deja insertar un nuevo hecho dentro de la memoria de trabajo desde la consecuencia de una regla.

Interacción y conclusión

Para ir terminando con el post, ya que se hizo muy extenso, lo falta aclarar como interactuamos con la sesión que habíamos creado en el post anterior. Si descargan el proyecto y revisan la clase App.java, veran que luego de configurar y obtener la session interactuamos con ella llamando los métodos insert y fireAllRules.

StatefulKnowledgeSession ksession = kbase.newStatefulKnowledgeSession();
 KnowledgeRuntimeLogger logger = KnowledgeRuntimeLoggerFactory.newConsoleLogger(ksession);
ksession.insert(new Juego("Poker",2));
 ksession.insert(new Jugador("Jugador 1"));
 ksession.insert(new Jugador("Jugador 2"));
 ksession.fireAllRules();

Espero que a grandes rasgos se entienda el post, y como podemos crear reglas que representen nuestro conocimiento para poder interactuar con el. Aparte de querer compartir los lineamientos básicos para utilizar Drools, la idea del post también es generar dudas, para poder establecer un canal de discusión. Asi que espero sus comentarios, criticas y preguntas si las dudas comienzan a surgir.

Saludos

PD: les dejo de vuelta el link del ejemplo para que puedan descargarlo sin tener que ir al post anterior: www.jbug.com.ar/external/playingWithDrools-ExpertIntroduction.zip

39 thoughts on “Jugando con Drools 5.0 – #2 Drools Expert”

  1. Buenas te escribo porque tengo una duda con el ejemplo,quisiera saber si podrias explicar como entender el log que sale por consola porque entiendo las reglas y el API pero no logro comprender como se ejecuta para que deje ese Log

    Saludos y gracias

    Like

  2. Gracias por tus comentarios. La idea de no explicarlo, era ver cuanta gente llegaba a ese punto. Tratare de publicar la respuesta a tu pregunta en un post asi queda claro el funcionamiento. Ya que requiere varias aclaraciones que si quedan en un comentario pueden no ser tan claras.
    Si tienes alguna otra gran duda que te surga, por favor compartela asi también la incluyo en el siguiente post, que espero que salga maniana!
    Saludos!

    Like

  3. Si no he entendido mal, para trabajar con los datos del “dominio” estos han de ser insertados en la sesión. Por lo que si dispongo de una gran cantidad de datos almacenados en bd, los debería extraer todos y cargarlos en la sesión? No existe la posibilidad de hacer que la sesión acceda directamente a la bd?.

    Saludos.

    Like

  4. Exactamente como tu dices. No hay manera de levantar todos datos de la base de datos directamente a la memoria. Mas que nada porque en la base de datos los datos estan almacenados de manera relacional y Drools hace inferencias sobre hechos/facts representados con Objetos. Por lo tanto tendrías que encargarte de realizar esa transformación.(También Hibernate/JPA puede hacerlo por ti, no es tarea difícil) También es bueno aclarar que no tienes que tomar esto como una limitación de la herramienta, sino que deberías plantearte exactamente que quieres hacer con ella. Debido a que una confusión conceptual puede llevarte a hacer cosas para cuales la herramienta no esta pensada o tiene un enfoque distinto para afrontar un problema.
    Saludos

    Like

  5. Tendria una duda para generar una variable generica en un dslr.
    Por ejemplo seria crear una variable y comparla con otra del mismo tipo

    Like

      1. Antetodo gracias por su respuesta
        seria en dslr, comparar dos objetos de un mismo tipo.

        imaginese el objeto casa(int x)

        seria comparar si casaA igual a casaB

        como haria yo esa declaracion en el dsl, la de la declaracion de objetos.

        Like

  6. DSL se usa solamente para hacer mapeos a un lenguaje especifico de un dominio. Si estas queriendo comparar objetos, seguramente lo estas haciendo en DRL.
    Para hacer eso por lo general se suele algo como lo siguiente:

    $a: Casa()
    $b: Casa(this == a)

    O si tiene la propiedad que le pasas al constructor (int x), puedes hacer:

    Casa($xa: x)
    Casa(x = $xa)

    Obviamente, esta comparacion te realizara el producto cartesiano entre las casa que tienes en tu working memory.
    Si tienes 2 casas, esta regla matcheara dos veces, si tienes una sola casa, esta regla matcheara 1 veces, ya que la misma casa se evalua para los dos patterns.
    En el caso de tener 4 casas, esta regla matcheara 16 veces.
    Espero haber aclarado tu duda, saludos.

    Like

  7. Claro, pero no se como mapear eso en el dslr.
    ¿como seria?

    Ej:
    when

    Hay una casa a
    hay otra casa b
    si casa a < casa b

    then
    msgError

    Muchas gracias por tu ayuda,
    si ha asi no me comprendes te mandare el ejemplo real
    Un saludo

    Like

    1. No tengo el IDE aca pero deberia ser algo asi:
      [condition]Hay una casa = $a: Casa($xa: x)
      [condition]Hay otra casa = $b: Casa(this != $a, $xb: x)
      [condition] Casa a < Casa b = eval($xa < $xb)

      En la ultima linea puede escoger usar un eval y comparar ahi adentro o crear una funcion si es mas complicada la logica.
      En el eval dependiendo del tipo de dato que compares es si tienes que castear o no.
      Saludos

      Like

      1. Muchas gracias, lo de la funcion dentro del eval lo conozco, la he usado y probare esa solucion haber si hay suerte

        Un saludo

        Like

  8. Buenos dias, primero de todo Feliz Año Nuevo….
    Te queria hacer una preguntita, segun he leido en este post, las reglas suelen llevar un operador AND implícito, existe la posibilidad de un OR???Me explico, mi problema es que tengo un valor que he de comparar con una suma de valores, no necesariamente deben existir todos pero si debe ser la suma de los que existan, en este caso, distinta de un valor predeterminado…..

    Espero haberme explicado bien. Muchas gracias de antemano. Un saludo

    Like

  9. Gracias por tu comentario, claro que puedes usar OR. Ejemplo: Persona() OR Mascota().
    Si tienes una condición mas compleja puedes usar parentesis como si fuera una formula.

    Like

    1. Me podrias poner un ejemplito???Lo estoy probando y me fallar, te copio una parte de mi codigo:

      rule “Ejemplo”
      when
      Nombre “cafe”
      (Nombre “leche” OR
      Nombre “colacao”)
      valor producto “cafe” distinto valor producto “leche” mas valor producto “colacao”
      then
      Mensaje de warn “Aviso – El precio del cafe no coincide con la suma de los precios de la leche mas el colacao”;
      end

      Esta regla lo que debe hacer es comprobar que la suma del coste de la leche y el colacao es distinta del precio del cafe, pudiendo exisistir o no la leche o el colacao…como a veces no me entero ni yo mismo te voy a poner un ejemplo :p, el cafe vale 1,20€, pues bien la alerta debe saltar cuando la suma de la leche y el colacao sea distinta, con la salvedad de que puede que uno de los dos no exista….

      Muchas gracias por todo

      Like

      1. Por la sintaxis que estas usando, supongo que estas usando DSL, en ese caso, no creo que puedas agregar properties asi nomas, sino que tienes que ir a editar la regla en DRL o el mappeo de DSL.
        En DRL lo siguiente seria
        when
        $c: Producto(nombre == “cafe”)
        $l: Producto(nombre == “leche”)
        $cola: Producto (nombre == “colacao”)
        eval($c.getPrecio() = ($l.getPrecio() + $cola.getPrecio()))
        then

        Espero haber sido de ayuda. Cualquier cosa espero un nuevo comentario tuyo.

        Like

  10. Si quieres comparar con OR dentro de un Pattern para fields, por ejemplo Producto(nombre == “cafe” || == “colacao”) debes hacerlo con “||”, en cambio si quieres usar el operador entre patterns deberias usar “OR” Producto(nombre == “a”) OR Producto(nombre == “b”), es depende tu caso lo que quieres comparar. Con esto me refiero a que evaluar fields, las evaluaciones van a ocurrir por todos los objetos Producto que tengas, en cambio comparando patterns la comparacion va a ocurrir entre Objetos distintos.

    Like

  11. Hola,

    Tengo unas dudas,
    estoy insertando en la memoria de trabajo dos tipos de objetos, Clientes y Movimientos.

    La cuestión es que según ciertas condiciones de los nuevos movimientos que voy insertando, en la parte Then tendría que modificar ciertos atributos de algunos de los Clientes que tengo en la memoria de trabajo, concretamente, a partir del idCliente del nuevo movimiento tendría que modificar ciertos atributos de ese idCliente de la clase Cliente, y esto no se como hacerlo.

    También podrías explicar la utilización de: Modify?

    Espero que haya quedado clara mi pregunta, sinó me lo haces saber.

    Gracias por los posts, me estan sirviendo de mucha ayuda.

    Like

  12. Hola Mauricio, estoy super entretenido leyendo tu blog y aprendiendo sobre drools. Me encontré con una duda y quiero comentártela a ver si me ayudas a responderla.

    Estoy escribiendo un DSL que tiene una definición como esta

    [when]El cliente tiene un sueldo fijo entre {v1} y {v2}=$p: PersonaVO(sueldoLiquido>{v1} && <{v2})

    y tengo un archivo con la regla que define lo sgte:

    rule "evaluar Credito para Cliente"
    when
    El cliente tiene un sueldo fijo entre 1000000 y 1500000
    then

    El tema es que me gustaría saber si es posible formatear los montos para que en vez que diga lo anterior sea algo como esto:

    El cliente tiene un sueldo fijo entre $1.000.000 y $1.500.000

    A ver si me puedes ayudar
    saludos

    Like

    1. Mauricio como estas? es una buena pregunta en realidad. Yo si tendria que dar solucion puntual a ese tema lo haria a nivel de presentacion de usuario. Ya que el mecanismo de DSL solo hace un reemplazo de texto, por lo cual es un tema puramente de presentacion. Supongo que tu field: sueldoLiquido guarda 10000000 y no $1.000.000. Lo cual me dice que si estas pensando en un editor tipo guvnor, para crear reglas usando DSL, incluiria un campo que permita ingresar distintos tipos de unidades.
      Saludos

      Like

  13. Hola, escribo porque estoy haciendo un proyecto con Drools y Liferay, y quiero hacer reglas con el operador OR pero no funciona.
    Tengo una clase ‘user’ y otra que es ‘address’, y un usuario tiene una o más ‘address’. Lo que hago es en el Initialize rule hago un insertLogical de todas las direcciones del usuario y luego hago una regla así:

    rule “Prueba”
    when
    mayorEdad : ( User ( age > 18)) OR
    userAddress : Address(country.name in (“France”, “Germany”, “Spain”));
    then
    #usuario correcto
    retract(userAddress);
    end

    Da este error:
    Caused by: com.liferay.portal.kernel.bi.rules.RulesEngineException: Unable to resolve ObjectType ‘OR’ : [Rule name=’Prueba’]

    Rule Compilation error : [Rule name=’Prueba’]
    com/liferay/drools/dependencies/Rule_Prueba_99674dc051fe4e72ac5c3b2bd6624d68.java (13:3068) : userAddress cannot be resolved

    Muchas gracias.

    Like

    1. Hola Sandra, el problema puede estar relacionado con Los parentesis alrededor de User que no son necesarios, puedes probar sacandolos? Que version de drools estas usando? Recuerdo que en una version el OR cambio a “or” en minusculas y sin las comillas, puedes probar eso tambien.

      Saludos!

      Like

      1. Muchas gracias por tu respuesta, pero no he conseguido solucionar el problema.
        He probado lo de los parentesis:
        when
        mayorEdad: User(age > 18 ) or
        userAddress : Address(country.name in (“France”, “Germany”, “Spain”));

        y también la ‘or’ en mayusculas y minúsculas pero no hay manera.
        Con la OR da este error:

        Caused by: com.liferay.portal.kernel.bi.rules.RulesEngineException: [109,3]: [ERR 102] Line 109:3 mismatched input ‘userAddress’ in rule “Prueba”
        [0,0]: Parser returned a null Package

        Y en con or:

        Caused by: com.liferay.portal.kernel.bi.rules.RulesEngineException: Rule Compilation error : [Rule name=’Prueba’]
        com/liferay/drools/dependencies/Rule_Prueba_741b752c204844b29a577b710c558628.java (11:2876) : userAddress cannot be resolved

        La versión de Drools que estoy utilizando es la 5.5.0.

        He probado las dos condiciones sin la ‘or’ y funcionan correctamente. Y en el ‘then’ del ‘Initialize rule’ tengo este codigo:

        List userAddresses = AddressLocalServiceUtil.getAddresses(
        user.getCompanyId(), Contact.class.getName(), user.getContactId());

        for (Address userAddress : userAddresses) {
        insertLogical(userAddress);
        }

        Muchas gracias de nuevo!

        Like

      2. Hola de nuevo, perdona las molestias. He conseguido arreglar el error quitando el retract(userAddress) de la regla ‘Prueba’, pero no entiendo ahora como funcionará la regla ya que segun tengo entendido si hago un insertLogical luego tengo que hacer retract para ir sacando los elementos, además ahora en el ‘then’ no puedo hacer un print ni de ‘mayorEdad’ ni de ‘userAddress’ para comprobar los resultados que da.

        Muchas gracias

        Like

    1. Ok, el problema entonces esta en la comprensión del Logical Insert:

      Primero tienes que resolver que tus reglas compilen. Puedes compartir tus reglas usando pastebin.com?

      Yo trataria lo siguiente:

      rule “Prueba”
      when
      $mayorEdad: User( age > 18) or
      $userAddress: Address( country.name in (“France”, “Germany”, “Spain”) )

      then
      retract($userAddress);
      end

      Si userAddress fue insertado logicamente, va a ser retractado automaticamente solo cuando la condición de la regla que lo inserto sea falsa.

      Nota que los $ se suelen usar como buena practica para demarcar cuales son las variables que tu regla esta introduciendo. Y los ; no son necesarios y pueden causar problemas en el LHS (en el bloque when).

      Like

    1. Ok el error es claro, te recomendaria dividir tu regla en dos, algo asi:

      rule “Prueba 1”
      when
      $mailusuario: User(emailAddress contains “@gmail.com”)
      $userAddress: Address()
      then
      retract($userAddress);
      end

      rule “Prueba 2”
      when
      User()
      $userAddress: Address(country.name in (“Germany”, “Spain”))
      then
      retract($userAddress);
      end

      Ya que en tu ejemplo si usas OR no puedes crear la variable $userAddress siempre, ya que podria ser la parte que no se evalua, porque User(emailAddress contains “@gmail.com”) ya es verdadero y no es necesario evaluar Address.

      Esta separacion deberia funcionar sin problemas y logicamente representa lo mismo.

      Like

  14. Esa seria la manera recomendada de hacerlo, pero tienes que analizar que quieres lograr. Esa regla que inicializa e hace los insertLogicals no tiene mucho sentido. No tengo muy en claro que estas tratando de lograr, pero tienes que tener cuidado y por sobre todo la solución tiene que ser lógica.

    Like

    1. Lo que quiero hacer en el Initialize rule es conseguir todos los address de un usuario q le paso como parametro, para que luego al hacer: ‘userAddress: Address(country.name in (…))’ mire todas las direcciones.
      Gracias.

      Like

      1. Eso se entiende, pero porque usas insertLogical en vez de insert? Otra pregunta que se me ocurre, es podes tener mas de un usuario al mismo tiempo?

        Like

      2. La verdad que lo del insertLogical era pq descargue el codigo Drools de un ejemplo de Liferay y estaba asi, lo de los usuarios pq le paso el usuario q esta logueado,

        Like

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.