Saltar al contenido

creando un motor de reglas simple en java

Hola, hemos encontrado la respuesta a lo que necesitas, deslízate y la obtendrás aquí.

Solución:

Implementar un sistema de evaluación simple basado en reglas en Java no es tan difícil de lograr. Probablemente, el analizador de la expresión es lo más complicado. El código de ejemplo a continuación utiliza un par de patrones para lograr la funcionalidad deseada.

Se utiliza un patrón singleton para almacenar cada operación disponible en un mapa de miembros. La operación en sí usa un patrón de comando para proporcionar una extensibilidad flexible, mientras que la acción respectiva para una expresión válida hace uso del patrón de distribución. Por último, no menos importante, se utiliza un patrón de intérprete para validar cada regla.

Una expresión como la presentada en su ejemplo anterior consta de operaciones, variables y valores. En referencia a un ejemplo de wiki, todo lo que se puede declarar es un Expression. Por tanto, la interfaz se ve así:

import java.util.Map;

public interface Expression

    public boolean interpret(final Map bindings);

Si bien el ejemplo en la página wiki devuelve un int (implementan una calculadora), solo necesitamos un valor de retorno booleano aquí para decidir si una expresión debe desencadenar una acción si la expresión se evalúa como true.

Una expresión puede, como se indicó anteriormente, ser una operación como =, AND, NOT, … o un Variable o su Value. La definición de un Variable se alista a continuación:

import java.util.Map;

public class Variable implements Expression

    private String name;

    public Variable(String name)
    
        this.name = name;
    

    public String getName()
    
        return this.name;
    

    @Override
    public boolean interpret(Map bindings)
    
        return true;
    

Validar un nombre de variable no tiene mucho sentido, por lo tanto true se devuelve de forma predeterminada. Lo mismo es cierto para un valor de una variable que se mantiene lo más genérico posible al definir un BaseType solamente:

import java.util.Map;

public class BaseType implements Expression

    public T value;
    public Class type;

    public BaseType(T value, Class type)
    
        this.value = value;
        this.type = type;
    

    public T getValue()
    
        return this.value;
    

    public Class getType()
    
        return this.type;
    

    @Override
    public boolean interpret(Map bindings)
    
        return true;
    

    public static BaseType getBaseType(String string)
    
        if (string == null)
            throw new IllegalArgumentException("The provided string must not be null");

        if ("true".equals(string) 

los BaseType La clase contiene un método de fábrica para generar tipos de valores concretos para un tipo específico de Java.

Un Operation ahora es una expresión especial como AND, NOT, =, … La clase base abstracta Operation define un operando izquierdo y derecho ya que el operando puede referirse a más de una expresión. Fe NOT probablemente solo se refiere a su expresión de la mano derecha y niega su resultado de validación, por lo que true convertirse en false y viceversa. Pero AND en el otro lado combina lógicamente una expresión izquierda y derecha, lo que obliga a ambas expresiones a ser verdaderas en la validación.

import java.util.Stack;

public abstract class Operation implements Expression

    protected String symbol;

    protected Expression leftOperand = null;
    protected Expression rightOperand = null;

    public Operation(String symbol)
    
        this.symbol = symbol;
    

    public abstract Operation copy();

    public String getSymbol()
    
        return this.symbol;
    

    public abstract int parse(final String[] tokens, final int pos, final Stack stack);

    protected Integer findNextExpression(String[] tokens, int pos, Stack stack)
    
        Operations operations = Operations.INSTANCE;

        for (int i = pos; i < tokens.length; i++)
        
            Operation op = operations.getOperation(tokens[i]);
            if (op != null)
            
                op = op.copy();
                // we found an operation
                i = op.parse(tokens, i, stack);

                return i;
            
        
        return null;
     

Probablemente dos operaciones salten al ojo. int parse(String[], int, Stack); refactoriza la lógica de analizar la operación concreta a la clase de operación respectiva, ya que probablemente sepa mejor lo que necesita para instanciar una operación válida. Integer findNextExpression(String[], int, stack); se usa para encontrar el lado derecho de la operación mientras se analiza la cadena en una expresión. Puede sonar extraño devolver un int aquí en lugar de una expresión, pero la expresión se inserta en la pila y el valor de retorno aquí solo devuelve la posición del último token utilizado por la expresión creada. Entonces, el valor int se usa para omitir los tokens ya procesados.

los AND la operación se ve así:

import java.util.Map;
import java.util.Stack;

public class And extends Operation
    
    public And()
    
        super("AND");
    

    public And copy()
    
        return new And();
    

    @Override
    public int parse(String[] tokens, int pos, Stack stack)
    
        Expression left = stack.pop();
        int i = findNextExpression(tokens, pos+1, stack);
        Expression right = stack.pop();

        this.leftOperand = left;
        this.rightOperand = right;

        stack.push(this);

        return i;
    

    @Override
    public boolean interpret(Map bindings)
    
        return leftOperand.interpret(bindings) && rightOperand.interpret(bindings);
    

En parse probablemente vea que la expresión ya generada del lado izquierdo se toma de la pila, luego se analiza el lado derecho y se toma nuevamente de la pila para finalmente empujar el nuevo AND operación que contiene ambas, la expresión de la mano izquierda y derecha, de nuevo en la pila.

NOT es similar en ese caso, pero solo establece el lado derecho como se describió anteriormente:

import java.util.Map;
import java.util.Stack;

public class Not extends Operation
    
    public Not()
    
        super("NOT");
    

    public Not copy()
    
        return new Not();
    

    @Override
    public int parse(String[] tokens, int pos, Stack stack)
    
        int i = findNextExpression(tokens, pos+1, stack);
        Expression right = stack.pop();

        this.rightOperand = right;
        stack.push(this);

        return i;
    

    @Override
    public boolean interpret(final Map bindings)
    
        return !this.rightOperand.interpret(bindings);
        

los = El operador se usa para verificar el valor de una variable si realmente es igual a un valor específico en el mapa de enlaces proporcionado como argumento en el interpret método.

import java.util.Map;
import java.util.Stack;

public class Equals extends Operation
      
    public Equals()
    
        super("=");
    

    @Override
    public Equals copy()
    
        return new Equals();
    

    @Override
    public int parse(final String[] tokens, int pos, Stack stack)
    
        if (pos-1 >= 0 && tokens.length >= pos+1)
        
            String var = tokens[pos-1];

            this.leftOperand = new Variable(var);
            this.rightOperand = BaseType.getBaseType(tokens[pos+1]);
            stack.push(this);

            return pos+1;
        
        throw new IllegalArgumentException("Cannot assign value to variable");
    

    @Override
    public boolean interpret(Map bindings)
    
        Variable v = (Variable)this.leftOperand;
        Object obj = bindings.get(v.getName());
        if (obj == null)
            return false;

        BaseType type = (BaseType)this.rightOperand;
        if (type.getType().equals(obj.getClass()))
        
            if (type.getValue().equals(obj))
                return true;
        
        return false;
    

Como puede verse en el parse método se asigna un valor a una variable con la variable en el lado izquierdo de la = símbolo y el valor en el lado derecho.

Además, la interpretación comprueba la disponibilidad del nombre de la variable en los enlaces de variables. Si no está disponible, sabemos que este término no se puede evaluar como verdadero, por lo que podemos omitir el proceso de evaluación. Si está presente, extraemos la información del lado derecho (= parte del valor) y primero verificamos si el tipo de clase es igual y, si es así, si el valor real de la variable coincide con el enlace.

Como el análisis real de las expresiones se refactoriza en las operaciones, el analizador real es bastante delgado:

import java.util.Stack;

public class ExpressionParser

    private static final Operations operations = Operations.INSTANCE;

    public static Expression fromString(String expr)
    
        Stack stack = new Stack<>();

        String[] tokens = expr.split("\s");
        for (int i=0; i < tokens.length-1; i++)
        
            Operation op = operations.getOperation(tokens[i]);
            if ( op != null )
            
                // create a new instance
                op = op.copy();
                i = op.parse(tokens, i, stack);
            
        

        return stack.pop();
    

Aquí el copy El método es probablemente lo más interesante. Como el análisis es bastante genérico, no sabemos de antemano qué operación se está procesando actualmente. Al devolver una operación encontrada entre las registradas resulta en una modificación de este objeto. Si solo tenemos una operación de ese tipo en nuestra expresión, esto no importa; sin embargo, si tenemos varias operaciones (por ejemplo, dos o más operaciones iguales), la operación se reutiliza y, por lo tanto, se actualiza con el nuevo valor. Como esto también cambia las operaciones de ese tipo creadas previamente, necesitamos crear una nueva instancia de la operación: copy() logra esto.

Operations es un contenedor que contiene operaciones registradas previamente y asigna la operación a un símbolo específico:

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public enum Operations

    /** Application of the Singleton pattern using enum **/
    INSTANCE;

    private final Map operations = new HashMap<>();

    public void registerOperation(Operation op, String symbol)
    
        if (!operations.containsKey(symbol))
            operations.put(symbol, op);
    

    public void registerOperation(Operation op)
    
        if (!operations.containsKey(op.getSymbol()))
            operations.put(op.getSymbol(), op);
    

    public Operation getOperation(String symbol)
    
        return this.operations.get(symbol);
    

    public Set getDefinedSymbols()
    
        return this.operations.keySet();
    

Aparte del patrón de enum singleton, no hay nada realmente lujoso aquí.

A Rule ahora contiene una o más expresiones que en la evaluación pueden desencadenar una determinada acción. Por lo tanto, la regla debe contener las expresiones analizadas previamente y la acción que debe activarse en caso de éxito.

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class Rule

    private List expressions;
    private ActionDispatcher dispatcher;

    public static class Builder
    
        private List expressions = new ArrayList<>();
        private ActionDispatcher dispatcher = new NullActionDispatcher();

        public Builder withExpression(Expression expr)
        
            expressions.add(expr);
            return this;
        

        public Builder withDispatcher(ActionDispatcher dispatcher)
        
            this.dispatcher = dispatcher;
            return this;
        

        public Rule build()
        
            return new Rule(expressions, dispatcher);
        
    

    private Rule(List expressions, ActionDispatcher dispatcher)
    
        this.expressions = expressions;
        this.dispatcher = dispatcher;
    

    public boolean eval(Map bindings)
    
        boolean eval = false;
        for (Expression expression : expressions)
        
            eval = expression.interpret(bindings);
            if (eval)
                dispatcher.fire();
        
        return eval;
    

Aquí se usa un patrón de construcción solo para poder agregar múltiples expresiones si se desea para la misma acción. Además, el Rule define un NullActionDispatcher por defecto. Si una expresión se evalúa correctamente, el despachador activará una fire() método, que procesará la acción que debe ejecutarse en una validación exitosa. El patrón nulo se utiliza aquí para evitar tratar con valores nulos en caso de que no se requiera la ejecución de ninguna acción, ya que solo true o false debe realizarse la validación. Por lo tanto, la interfaz también es simple:

public interface ActionDispatcher

    public void fire();

Como realmente no se cual es tu INPATIENT o OUTPATIENT acciones deben ser, el fire() El método solo desencadena un System.out.println(...); invocación de método:

public class InPatientDispatcher implements ActionDispatcher

    @Override
    public void fire()
    
        // send patient to in_patient
        System.out.println("Send patient to IN");
    

Por último, pero no menos importante, un método principal simple para probar el comportamiento del código:

import java.util.HashMap;
import java.util.Map;

public class Main 

    public static void main( String[] args )
    
        // create a singleton container for operations
        Operations operations = Operations.INSTANCE;

        // register new operations with the previously created container
        operations.registerOperation(new And());
        operations.registerOperation(new Equals());
        operations.registerOperation(new Not());

        // defines the triggers when a rule should fire
        Expression ex3 = ExpressionParser.fromString("PATIENT_TYPE = 'A' AND NOT ADMISSION_TYPE = 'O'");
        Expression ex1 = ExpressionParser.fromString("PATIENT_TYPE = 'A' AND ADMISSION_TYPE = 'O'");
        Expression ex2 = ExpressionParser.fromString("PATIENT_TYPE = 'B'");

        // define the possible actions for rules that fire
        ActionDispatcher inPatient = new InPatientDispatcher();
        ActionDispatcher outPatient = new OutPatientDispatcher();

        // create the rules and link them to the accoridng expression and action
        Rule rule1 = new Rule.Builder()
                            .withExpression(ex1)
                            .withDispatcher(outPatient)
                            .build();

        Rule rule2 = new Rule.Builder()
                            .withExpression(ex2)
                            .withExpression(ex3)
                            .withDispatcher(inPatient)
                            .build();

        // add all rules to a single container
        Rules rules = new Rules();
        rules.addRule(rule1);
        rules.addRule(rule2);

        // for test purpose define a variable binding ...
        Map bindings = new HashMap<>();
        bindings.put("PATIENT_TYPE", "'A'");
        bindings.put("ADMISSION_TYPE", "'O'");
        // ... and evaluate the defined rules with the specified bindings
        boolean triggered = rules.eval(bindings);
        System.out.println("Action triggered: "+triggered);
    

Rules aquí es solo una clase de contenedor simple para reglas y propaga el eval(bindings); invocación a cada regla definida.

No incluyo otras operaciones ya que la publicación aquí ya es demasiado larga, pero no debería ser demasiado difícil implementarlas por su cuenta si así lo desea. Además, no incluí la estructura de mi paquete, ya que probablemente usará la suya propia. Además, no incluí ningún manejo de excepciones, se lo dejo a todos los que van a copiar y pegar el código 🙂

Se podría argumentar que el análisis sintáctico debería ocurrir obviamente en el analizador sintáctico en lugar de en las clases concretas. Soy consciente de eso, pero por otro lado, al agregar nuevas operaciones, debe modificar el analizador y la nueva operación en lugar de solo tener que tocar una sola clase.

En lugar de utilizar un sistema basado en reglas, una red petri o incluso un BPMN en combinación con el motor Activiti de código abierto sería posible lograr esta tarea. Aquí las operaciones ya están definidas dentro del lenguaje, solo necesita definir las declaraciones concretas como tareas que se pueden ejecutar automáticamente - y dependiendo del resultado de una tarea (es decir, la declaración única) continuará su camino a través del "gráfico" . Por lo tanto, el modelado generalmente se realiza en un editor gráfico o frontend para evitar lidiar con la naturaleza XML del lenguaje BPMN.

Básicamente ... no lo hagas

Para entender por qué, vea:

  1. http://thedailywtf.com/Articles/The_Customer-Friendly_System.aspx
  2. http://thedailywtf.com/Articles/The-Mythical-Business-Layer.aspx
  3. http://thedailywtf.com/Articles/Soft_Coding.aspx

Sé que parece una gran idea desde lejos, pero El motor de reglas de negocio terminará invariablemente siendo más difícil de mantener, implementar y depurar que el lenguaje de programación en el que fue escrito. - No invente sus propios lenguajes de programación si puede evitarlo.

Personalmente he estado por ese camino en una ex empresa y he visto a dónde va después de un par de años (guiones gigantes indestructibles que se encuentran en una base de datos escrita en un lenguaje que viene directamente de una dimensión paralela donde Dios nos odia y que al final nunca cumplen con el 100% de las expectativas del cliente porque no son tan poderosos como un lenguaje de programación adecuado y al mismo tiempo son demasiado complicados y malvados para que los desarrolladores los manejen (no importa el cliente)).

Sé que hay un cierto tipo de cliente que está enamorado de la idea de que no pagarán horas de programador por "adaptaciones a las reglas de negocio" y que no entiende que al final estarán peor y para atraer a este tipo de cliente, tengo que hacer algo en esta dirección - pero hagas lo que hagas no inventes algo tuyo.

Existe una gran cantidad de lenguajes de scripting decentes que vienen con buenas herramientas (que no requieren compilación, por lo que se pueden cargar dinámicamente, etc.) que se pueden interconectar y llamar de manera hábil desde el código Java y aprovechar las apis de Java implementadas que usted crea. disponible, consulte http://www.slideshare.net/jazzman1980/j-ruby-injavapublic#btnNext por ejemplo, posiblemente también Jython,

y cuando el cliente deja de escribir estos guiones, voluntad quedarse con el feliz deber de mantener su legado fallido - Asegúrate de eso ese el legado es tan indoloro como puede ser.

Sugeriría usar algo como Drools. Crear su propia solución personalizada sería una exageración porque tendría que depurarla y aún así proporcionar una funcionalidad ciertamente menor que la proporcionada por un motor de reglas como Drools. Entiendo que Drools tiene una curva de aprendizaje, pero no lo compararía con la creación de un lenguaje personalizado, o una solución personalizada ...

En mi opinión, para que un usuario escriba reglas, tendría que aprender algo. Si bien supongo que podría proporcionar un lenguaje más simple que el lenguaje de la regla de las babas, nunca captaría todas sus necesidades. El lenguaje de las reglas Drools sería lo suficientemente simple para reglas simples. Además, podría proporcionarle una documentación bien formada. Si planea controlar las reglas creadas por el usuario final y aplicadas en el sistema, entonces quizás sería más prudente crear una interfaz gráfica de usuario que formaría las reglas aplicadas a las babas.

¡Espero haber ayudado!

Si crees que te ha resultado provechoso nuestro artículo, te agradeceríamos que lo compartas con otros juniors así nos ayudas a difundir esta información.

¡Haz clic para puntuar esta entrada!
(Votos: 2 Promedio: 4.5)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *