Creando un sistema basado en reglas en Java

Hace tiempo cuando intentaba aprender Scala, compré el libro Exercises for Programmers y últimamente he dedicado algo de tiempo a hacer uno de los ejercicios, que consiste en un sistema basado en reglas muy simple.

Los sistemas basados en reglas son un subconjunto de los sistemas expertos, que a su vez se engloban en el área de la inteligencia artificial. En el contexto de este artículo, simplificaremos los conceptos, quedándonos con un simple árbol de decisión, que nos permitirá llegar a una conclusión basándonos en las respuestas del usuario.

Podemos encontrar ejemplos de estos sistemas en asistentes paso-a-paso, en las centralitas telefónicas (“Para consultar el saldo, pulse 1”) y en los asistentes digitales como Alexa, Siri, Cortana o Google. Cuando escribimos un sistema de este tipo, nuestra lógica tendrá el siguiente aspecto:

sistema pregunta a.
si usuario responde si:
    sistema pregunta b.
    si usuario responde = "si"
        sistema responde c <- Respuesta final
si no
    sistema pregunta d
...

Esta información puede vivir en nuestro código fuente en formato de if, else anidados o puede formar parte de los metadatos de nuestro sistema. En el artículo de hoy diseñaremos uno de estos sistemas utilizando Java y la siempre útil consola de comandos.

Además de escribir nuestro sistema de reglas, en este artículo mencionaremos por encima los siguientes temas:

  • Tests unitarios con JUnit y AssertJ
  • Generación de Setters y builders con Lombok
  • Manejo de Ficheros yaml con snakeyaml
  • Inyección de dependencias e inversión de control
  • Creación de paquetes JAR con Gradle incluyendo dependencias

Los tests

Una práctica que intento seguir a la hora de hacer este tipo de ejercicios, es empezar por las pruebas al más puro estilo TDD, definiendo de manera inicial el estado al que queremos llegar.

Podemos emplear JUnit y AssertJ para ejecutar los tests, y agregar las referencias a Gradle es tan sencillo como utilizar las siguientes líneas:

testCompile group: ‘junit’, name: ‘junit’, version: ‘4.12’
testCompile(“org.assertj:assertj-core:3.11.1”)

Con ello podemos escribir nuestro primer test:

@Test
public void question() {
    assertThat(expertSystem.getMessage()).isEqualTo("Are you able to see any wifi network?");
}

Una vez pedido el estado inicial, hemos de comprobar que el motor pasa del primer estado al siguiente, como hemos en el siguiente test:

@Test
public void firstAnswerYes() {
    assertThat(expertSystem.getMessage()).isEqualTo("Are you able to see any wifi network?");
    expertSystem.answer(true);
    assertThat(expertSystem.getMessage()).isEqualTo("Is the network ID visible?");
    assertThat(expertSystem.isDone()).isFalse();

Finalmente, podemos comprobar que hemos llegado a un estado final con el siguiente test:

@Test
public void secondAnswerYesYesDone() {
    assertThat(expertSystem.getMessage()).isEqualTo(Are you able to see any wifi network?");
    expertSystem.answer(true);
    assertThat(expertSystem.getMessage()).isEqualTo("Is the network ID visible?");
    expertSystem.answer(true);
    assertThat(expertSystem.getMessage()).isEqualTo("Contact your network provider");
    assertThat(expertSystem.isDone()).isTrue();
}

Con estos tests, definimos un sistema de reglas que:

  • Recibe una respuesta binaria, que puede ser sí o no.
  • Proporciona el estado actual así como la información de si el estado actual es final o no.

El motor de reglas

Para este ejemplo, el motor de reglas no es más que un árbol binario con una clase muy sencilla que contiene dos hijos, “sí” y “no”, para cada rama:

@Getter
@Builder
class Stage {

    private String status;
    private Stage yes;
    private Stage no;

    boolean isEnd(){
        return yes == null && no == null;
    }
}

Para crear tanto la construcción de los objetos como los getters como el patrón builder podemos recurrir a Lombok, del que ya hemos hablado en otros artículos de este blog, y que podemos agregar a nuestro modelo de Gradle con las siguientes líneas:

compileOnly ‘org.projectlombok:lombok:1.18.8’
annotationProcessor ‘org.projectlombok:lombok:1.18.8’

A la hora de pasar de una etapa a otra, el motor de reglas simplemente decide qué hijo tiene que buscar:

private Stage current; //Aquí inicializaremos la etapa inicial

public String getMessage() {
    return current.getStatus();
}

public void answer(boolean answer) {
    if (answer){
        current = current.getYes();
    } else {
        current = current.getNo();
    }
}

public boolean isDone() {
    return current.isEnd();
}

Una comprobación que no forma parte del código es qué pasa si la etapa ya es final. Por otra parte se podría transformar bloque IF en un operador ternario, dando como resultado algo como current = answer ?? current.getYes() : current.getNo();

El almacenamiento

En el apartado anterior no hemos mencionado cómo inicializar las reglas. Podemos empezar por crear los objetos como parte del constructor, dando como resultado algo así:

Stage root = Stage.builder()
        .status(“Are you able to see any wifi network?”)
        .yes(Stage.builder()
                .status(“Is the network ID visible?”)
                .yes(Stage.builder()
                        .status(“Contact your network provider”)
                        .build())
                .build())
        .no(Stage.builder()
                .status(“Wireless network might be off. Reboot computer”)
                .build())
        .build();

Sin embargo, esta aproximación genera un fuerte acoplamiento entre el código y los datos, así que es una buena práctica sacar las reglas del código a un formato diferente, optando en este caso por YAML.

Al transformar nuestro árbol de Java a YAML tenemos como resultado la siguiente estructura, y este fichero lo podemos almacenar en la carpeta resources de nuestro proyecto:

text: "Are you able to see any wifi network?"
yes:
  text: "Is the network ID visible?"
  yes:
    text: "Contact your network provider"
  no:
    text: "Reboot the wireless router"
no:
  text: "Wireless network might be off. Reboot computer"

Para leer el fichero YAML podemos utilizar SnakeYaml, que podemos importar en Gradle con la siguiente línea:

compile group: ‘org.yaml’, name: ‘snakeyaml’, version: ‘1.8’

Finalmente, podemos cargar el fichero YAML en memoria de la siguiente manera.

class YamlLoader implements FileLoader<Stage> {

    public Stage loadFromFile() {
        Yaml yaml = new Yaml();
        InputStream inputStream = this.getClass()
                .getClassLoader()
                .getResourceAsStream("options.yaml");
        Map<Object, Object> obj = (Map<Object, Object>) yaml.load(inputStream);

        return getStage(obj);
    }

    private Stage getStage(Map<Object, Object> obj) {
        if (obj == null) {
            return null;
        }

        return Stage.builder()
                .status(obj.get("text").toString())
                .yes(getStage((Map<Object, Object>) obj.get(true)))
                .no(getStage((Map<Object, Object>) obj.get(false)))
                .build();
    }
}

Para no tener un acoplamiento entre nuestra clase ExpertSystemy YamlLoader, la segunda implementa una interfaz genérica llamada FileLoader que simplemente define un método, lo que nos da la posibilidad de agregar otros gestores de ficheros en el futuro como xml o JSON.

public interface FileLoader <T> {
    T loadFromFile();
}

Finalmente la conexión entre nuestra clase ExpertSystemy el gestor de ficheros se realiza en el constructor:

public ExpertSystem(FileLoader<Stage> fileLoader) {
    current = fileLoader.loadFromFile();
}

Este último paso no deja de ser inyección de dependencias e inversión de control. De esta manera, nuestra clase ExpertSystem es completamente independiente del formato en el que almacenemos nuestros datos, y podemos probarla de manera aislada, mientras mantenemos nuestra lógica de carga de YAML independiente de la lógica del motor de reglas.

La interacción

Para que nuestro código sea utilizable necesitamos, además de un algoritmo que funcione, una manera de interactuar con nuestro sistema. Para eso, volvemos a los mecanismos basados en System.outy System.in para escribir y leer por la consola.

public class UI {

    public static void main(String[] args) {
        ExpertSystem system = new ExpertSystem(new YamlLoader());

        Scanner scanner = new Scanner(in);

        while (!system.isDone()) {
            out.printf("%s %s ", system.getMessage(), Option.getOptions());
            system.answer(Option.parse(scanner.nextLine()));
        }

        scanner.close();
        out.println(system.getMessage());
    }
}

Finalmente, podemos extraer la gestión de la entrada a una clase específica llamada Option:

class Option {

    private static final String truthy = "yes";
    private static final String falsy = "no";

    static String getOptions() {
        return String.format("[%s/%s]:", truthy, falsy);
    }

    static boolean parse(String input) {
        if (input.toLowerCase().equals(truthy)) {
            return true;
        } else if (input.toLowerCase().equals(falsy)) {
            return false;
        }

        throw new IllegalArgumentException();
    }
}

Esta clase analizará la entrada que recibimos de la consola y generará un valor verdadero o falso, que luego podemos pasar posteriormente a nuestro motor.

Creando nuestro paquete jar

El último paso de nuestro sistema es empaquetarlo para pode distribuirlo. En nuestro caso lo que queremos es un fichero jar que se pueda ejecutar de manera independiente.

Para ello, hemos de modificar nuestro fichero Gradle para agregar ciertas directivas que, por una parte, establecen la clase principal de nuestro proyecto, y por otra parte, fuerzan a que se combinen las diferentes dependencias que tiene nuestra aplicación. Este paso se muestra a continuación:

jar {
    manifest {
        attributes "Main-Class": "net.rlbisbe.expert.UI"
    }

    from {
        configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

Finalmente, solamente tenemos que ejecutar java -jar NuestroPaquete.jar, y podremos ver nuestro sistema en funcionamiento:

Recapitulando

A lo largo de este artículo, hemos visto cómo crear un simple sistema basado en reglas, cómo cargar dichas reglas de un fichero YAML, mientras repasábamos conceptos como inversión de control e inyección de dependencias, pruebas unitarias, y aprendíamos a manejar dependencias en Gradle.

No pierdas la oportunidad de echarle un ojo a Exercises for Programmers que tiene este y otros problemas para aprender nuevos lenguajes, probar maneras diferentes de hacer las cosas o practicar conocimientos.

Puedes encontrar el código fuente de este ejemplo en Github – Expert-System

Kata UpperCounter con Software Craftsmanship Madrid

El pasado martes 5 de agosto tuve la oportunidad de acudir a mi primer meetup de Software Craftsmanship Madrid, en el que se celebaba un coding dojo facilitado por Carlos Ble @carlosble. El objetivo de la sesión era hacer uso de un patrón diseñado por Robert «Uncle Bob» Martin llamado «Transformation Priority Premise» que nos lleva a una programación más genérica y funcional.

Tras sentarnos por parejas y escoger el entorno y el lenguaje de programación (En mi caso, C# con Visual Studio 2013 y XUnit como motor de pruebas) se desveló el objetivo de la kata:

Dada una cadena, devolver, en forma de array, las posiciones de dicha cadena cuyos caracteres sean la letra mayúscula

Ejemplos:
– A: {0}
– bA: {1}
– aBcdE: {1, 4}

Primera iteración, sin restricciones

Para esta primera iteración no teníamos ninguna limitación más allá de intentar resolver la kata. El resultado es el que se muestra en el vídeo:

El código completo, tanto del test como de la implementación se puede ver a continuación:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Sample
{
public class UppercaseSearcher
{
internal int[] Search(string source)
{
var result = new List<int>();
if (source.Length > 0)
{
for (int i = 0; i < source.Length; i++)
{
var current = source[i];
if (char.IsUpper(source, i))
{
result.Add(i);
}
}
}
return result.ToArray();
}
private bool IsUpperCase(char current)
{
return current.ToString().ToUpper()
== current.ToString();
}
}
}

view raw

Source.cs

hosted with ❤ by GitHub


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using Xunit.Extensions;
namespace Sample
{
public class Test
{
[Fact]
public void ShouldReturnEmptyArrayIfEmptyString()
{
//Arrange
var source = "";
var searcher = new UppercaseSearcher();
//Act
var result = searcher.Search(source);
//Assert
Assert.Empty(result);
}
[Fact]
public void ShouldReturnValidUppercaseLocation()
{
//Arrange
var source = "A";
var searcher = new UppercaseSearcher();
//Act
var result = searcher.Search(source);
//Assert
Assert.Equal(1, result.Length);
Assert.Equal(0, result[0]);
}
[Fact]
public void ShouldReturnValidUppercaseInSecondPlace()
{
//Arrange
var source = "bA";
var searcher = new UppercaseSearcher();
var expected = new int[] { 1 };
//Act
var result = searcher.Search(source);
//Assert
Assert.Equal(expected, result);
}
[Theory]
[InlineData("A", new int[] { 0 })]
[InlineData("bA", new int[] { 1 })]
[InlineData("bbAab", new int[] { 2 })]
[InlineData("babC", new int[] { 3 })]
//Multiple uppercases
[InlineData("bCbC", new int[] {1,3})]
//No uppercase
[InlineData("qwerty", new int[] { })]
public void ShouldBeValidInDifferentSituations(string source, int[] expected)
{
//Arrange
var searcher = new UppercaseSearcher();
//Act
var result = searcher.Search(source);
//Assert
Assert.Equal(expected, result);
}
}
}

view raw

Test.cs

hosted with ❤ by GitHub

El enfoque es iterativo, utilizando un bucle para recorrer los caracteres de la cadena y la comprobación es bastante artesanal (a mejorar en la segunda iteración) y además, como teníamos tiempo, pudimos probar las Theories de XUnit, que nos permiten utilizar, en el mismo test, diferentes conjuntos de entrada y esperar diferentes resultados.

Segunda iteración, con restricciones

En esta segunda iteración teníamos una limitación importante: No podíamos asignar variables, ni modificar valores existentes. Esto nos deja sin la posibilidad de usar bucles (ya que vamos actualizando la posición del iterador) y forzando nuestro código a que sea más funcional. El resultado es el que se muestra:

El código completo es el siguiente, con el que intentamos seguir a rajatabla la indicación de no asignar o modificar los valores de las variables. En este caso el tiempo no permitió pasar de unos pocos test, pero se aprecia una diferencia notable por una parte en el test y por otra en el código de implementación:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Sample.WithLimits
{
class UppercaseSearcher
{
internal static int[] Search(string source)
{
return Search(source, 0);
}
internal static int[] Search(string source, int index)
{
if (IsOutOfBounds(source, index))
{
return new int[0];
}
if (char.IsUpper(source, index))
{
return new int[] { index }
.Concat(Search(source, index + 1))
.ToArray();
}
else
{
return Search(source, index + 1)
.ToArray();
}
}
private static bool IsOutOfBounds(string source, int index)
{
return source.Length == 0 || index >= source.Length;
}
}
}

view raw

Searcher.cs

hosted with ❤ by GitHub


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace Sample.WithLimits
{
public class TestClass
{
[Fact]
public void ShouldReturnEmptyArray()
{
//Arrange, act, assert
Assert.Equal(new int[0], UppercaseSearcher.Search(""));
}
[Fact]
public void ShouldReturn0IfOneLetterIsUppercase()
{
//Arrange, act, assert
Assert.Equal(new int[] {0}, UppercaseSearcher.Search("A"));
}
[Fact]
public void ShouldReturn1IfSecondLetterIsUppercase()
{
//Arrange, act, assert
Assert.Equal(new int[] { 1 }, UppercaseSearcher.Search("bA"));
}
}
}

view raw

Test.cs

hosted with ❤ by GitHub

Como nota adicional, gracias a @DanielRoz0 que me ha estado ayudando con la edición del vídeo, se pudo simplificar la comparación de una letra con su correspondiente mayúscula mediante el uso de la función char.IsUpper(source, index).

Información y enlaces:

Excepciones con Test Unitarios en C#

Una kata, en el contexto de desarrollo de software, es un ejercicio de programación que nos permite, en un entorno controlado, probar nuevas técnicas y mejorar la calidad de nuestro código. Una de las maneras más interesantes de realizarlas es a través de la plataforma Solveet, que, en colaboración con la web 12meses12katas, proponen un ejercicio mensual.

En el ejercicio del pasado mes, descubrí que nunca había probado un método que devolviera una excepción, así que recurrí a lo primero que sabía, un bloque try catch, aunque el resultado deja bastante que desear:

[TestMethod]
public void TestMethodThatShouldReturnAnException()
{
  try
  {
    CodeMachine machine = CodeMachineBuilder.Generate("KKKKK");
    Assert.Fail();
  }
  catch (IncorrectCharactersException){}
}

Un bloque catch vacío es una de las cosas menos recomendadas desarrollando software, ya que se pueden capturar excepciones que no deseamos. Por suerte, Microsoft ha puesto a nuestra disposición el siguiente artículo: A Unit Testing Walkthrough with Visual Studio Team Test, que, aunque algo antiguo, nos propone otra manera de controlar las excepciones en un test unitario:

[TestMethod]
[ExpectedException(typeof(IncorrectCharactersException))]
public void
TestMethodThatShouldReturnAnException
()
{
 CodeMachine machine = CodeMachineBuilder.Generate("KKKK");
}

Con un atributo llamado ExpectedException y el tipo de excepción que esperamos, podemos mantener el código limpio y elegante, así, podemos seguir con nuestras pruebas.

Mi primer Katayuno

Kata (Wikipedia): palabra japonesa que describe lo que en un inicio se consideró una serie, forma o secuencia de movimientos establecidos que se pueden practicar normalmente solo […].

Las artes marciales nos han enseñado que para ser buenos en algo, lo tenemos que repetir muchas, muchas veces. Este concepto se ha llevado a la programación, donde una kata es un pequeño ejercicio de código que plantea un problema que hemos de resolver de manera incremental, empleando TDD (desarrollo dirigido por tests), como si de la secuencia de movimientos de una kata se tratara.

El objetivo de estos ejercicios es mejorar nuestra capacidad de resolución de problemas y familiarizarnos con la manera de desarrollar a partir de pruebas. Estas katas se pueden hacer de manera individual, aunque si nos juntamos varios puede dar como resultado algo muy, muy interesante.

Esto es lo que ocurrió el pasado sábado en el grupo de AgileCyl, que organizaron lo que se conoce como Katayuno, reunir a un grupo de desarrolladores, programar por parejas una kata, y luego comentar la experiencia. En esta edición usamos Ruby, Python, Objective-C, Java, Javascript, C# y Groovy como lenguajes de programación, si no me dejo ninguno.

El funcionamiento fue el siguiente, hubo 3 iteraciones de 40 minutos con una pausa entre la segunda y la tercera para desayunar:

  • Primera iteración: Se desarrolla la kata por parejas, se introduce el concepto de TDD a aquellos que no estuvieran familiarizados.
  • Segunda iteración: Se cambian las parejas, se borra el código fuente (manteniendo el código de los test) y se re-escribe la kata. Es muy interesante porque el resultado puede llegar a ser bastante diferente a la primera iteración.
  • Tercera iteración: Se cambian las parejas (otra vez), se borra todo el código fuente y se re-escribe la kata en otro lenguaje de programación. Una vez más, es una tercera aproximación al mismo problema, aprovechando las características de cada lenguaje.

Al final de cada iteración tenìamos unos minutos en los que comentábamos qué nos había parecido la experiencia, sobre todo a aquellos que se enfrentaban, por primera vez, a un ejercicio de TDD.

La experiencia ha sido muy enriquecedora, he podido desarrollar con tres personas diferentes y tener tres puntos de vista del mismo problema, usando dos lenguajes de programación diferentes, he podido discutir sobre las ventajas o deventajas de usar según qué tipo de estructuras para problemas específicos. Espero poder repetir en el siguiente.