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();
}
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;
}
}
private T value;
@Override
public T eval() {
return value;
}
public void bind(T value) {
this.value = value;
}
}
Agora vamos definir um Predicado.
public interface Predicate extends Functor<Boolean> {
Boolean eval();
}
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;
}
}
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;
}
}
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());
}
}
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());
}
}
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>();
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();
}
}
private Predicate f;
public Rule(Predicate f) {
this.f = f;
}
public boolean eval() {
return f.eval();
}
}
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);
}
}
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);
}
}
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 dadostag2.bind(30);
value1.bind(10);
value2.bind(30);
System.out.println(r1.eval() ? "regra verdadeira " : "regra falsa");
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);
}
}
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++.