sábado, 20 de novembro de 2010

Como implementar um simples avaliador de regras em Java


Como implementar um conjuto de condições definidas pelo usuário de forma simples que possa ser armazenado em um banco de dados em Java ?

Ex.: if(tag1.equals(value1) && tag2.equals(value2)) { doThis(); }


se tornar algo do tipo:

        Var<Integer> tag1 = new Var<Integer>();
        Var<Integer> tag2 = new Var<Integer>();
       
        Var<Integer> value1 = new Var<Integer>();
        Var<Integer> value2 = new Var<Integer>();
       
        // tag1 == value && tag2 == value2
        Rule r1 = new Rule(
            and(
                eq(tag1, value1),
                eq(tag2, value2)
            )
        );
       
        tag1.bind(10);
        tag2.bind(30);
       
        value1.bind(10);
        value2.bind(30);
       
        System.out.println(r1.eval());

Vamos definir que esse conjunto de condições é uma regra (rule) que precisa ser avaliada com certas variáveis. Para simplificar, nossas regras só podem ter os operadores Equals e NotEquals, e os conectores And e Or. Após você entender como é implementado, você pode estender para os seus próprios operadores e conectores.

Para definirmos nossas regras, precisamos construir um objeto que represente uma função, ou seja, um Functor. Um Functor é semelhante a um callback ou uma interface com somente um método, comumente usando no padrão Command, que podemos passá-los como argumentos para outros objetos executarem a função, a diferença é podemos montar um Functor dinamicamente, semelhante a linguagens funcionais.

interface Functor<T> {
    T eval();
}

Definido o Functor, precisamos definir onde ficaram nossas variáveis e/ou parametros usados na regra:

public class Var<T> implements Functor<T> {
   
    private T value;
   
    @Override
    public T eval() {
        return value;
    }
   
    public void bind(T value) {
        this.value = value;
    }
}

Um Var é um caso particular de um Functor onde sempre retornará o valor de uma variável.

Agora vamos definir um Predicado.

public interface Predicate extends Functor<Boolean> {
    Boolean eval();
}

Um Predicado é outro caso de um Functor que sempre retorna um valor true ou false. Para o nosso caso de analisador de regras, é que queremos.

Agora vamos começar a construir nossos conectores:

// And
public class AndOp implements Predicate {
   
    private Predicate[] ps;

    public AndOp(Predicate... ps) {
        this.ps = ps;
    }

    @Override
    public Boolean eval() {
        for(Predicate p : ps) {
            if(!p.eval()) return false;
        }
        return true;
    }
}

// Or
public class OrOp implements Predicate {
   
    private Predicate[] ps;

    public OrOp(Predicate... ps) {
        this.ps = ps;
    }

    @Override
    public Boolean eval() {
        for (Predicate p : ps) {
            if (p.eval())
                return true;
        }
        return false;
    }
}

Veja que estamos tentando otimizar a avaliação do conector And para que caso ele encontre algum resultado falso, ele já retorne com o valor false. Do mesmo modo, fazemos para o conector Or, que se encontrarmos um resultado verdadeiro, ele já retorne com o valor true, sem a necessidade de avaliarmos o resto dos resultados.

Agora vamos definir nossos operadores:

public class EqualsOp<T> implements Predicate {
   
    private Var<T> v1;
    private Var<T> v2;

    public EqualsOp(Var<T> v1, Var<T> v2) {
        this.v1 = v1;
        this.v2 = v2;
    }

    @Override
    public Boolean eval() {
        return v1.eval().equals(v2.eval());
    }
   
}

public class NotEqualsOp<T> implements Predicate {
   
    private Var<T> v1;
    private Var<T> v2;

    public NotEqualsOp(Var<T> v1, Var<T> v2) {
        this.v1 = v1;
        this.v2 = v2;
    }

    @Override
    public Boolean eval() {
        return !v1.eval().equals(v2.eval());
    }
}

Veja que os operadores recebem variáveis, enquanto os conectores recebem outros predicados para serem avaliados sob uma certa condição.

Assim, podemos escrever baseado somente nos construtores, a seguinte regra:

Var<Integer> tag1 = new Var<Integer>();
Var<Integer> tag2 = new Var<Integer>();
       
Var<Integer> value1 = new Var<Integer>();
Var<Integer> value2 = new Var<Integer>();

new AndOp(new EqualsOp(tag1, value1), new EqualsOp(tag2, value2));

Para criar a abstração de regra, criamos a classe:

public class Rule {
   
    private Predicate f;
   
    public Rule(Predicate f) {
        this.f = f;
    }
   
    public boolean eval() {
        return f.eval();
    }
       
}

Para simplificar a construção da regra, podemos tentar usar uma sintax mais simples, criando um Factory com métodos státicos:

public class RuleFactory {
   
    public static <T> Predicate eq(Var<T> v1, Var<T> v2) {
        return new EqualsOp<T>(v1, v2);
    }
   
    public static <T> Predicate ne(Var<T> v1, Var<T> v2) {
        return new NotEqualsOp<T>(v1, v2);
    }
   
    public static Predicate and(Predicate op1, Predicate op2) {
        return new AndOp(op1, op2);
    }
   
    public static Predicate or(Predicate op1, Predicate op2) {
        return new OrOp(op1, op2);
    }
   
}

Assim, podemos importar estaticamente os métodos e usar na forma abaixo:

       Rule r1 = new Rule(
            and(
                eq(tag1, value1),
                eq(tag2, value2)
            )
        );

Agora é só fazermos o bind das variáveis e efetuar avaliação da regra:

tag1.bind(10);
tag2.bind(30);
       
value1.bind(10);
value2.bind(30);
       
System.out.println(r1.eval() ? "regra verdadeira " : "regra falsa");
Armazenar em um banco de dados

Para armazenar a regra em um banco de dados, utilizei o xstream para serializar meu Functor em um formato xml.

public class RuleXMLConverter {
   
    public static String toXML(Rule r) {
        XStream xstream = new XStream();
        return xstream.toXML(r);
    }
   
    public static Rule fromXML(String x) {
        XStream xstream = new XStream();
        return (Rule)xstream.fromXML(x);
    }

}

Alternativas

Antes de começar a fazer um novo Functor, dê uma olhada nos frameworks abaixo:

LambdaJ - manipulate collections in a pseudo-functional and statically typed way

apache-commos-functor - Function Objects for Java

jga - Generic Algorithms for Java

Caso esteja precisando desenvolver algo em C++ que não já exista no Boost ou no STL (C++ Standart Library) há um ótimo post aqui sobre a implementação de Functors usando sobrecarga de operadores em C++.