creando un motor de reglas simple en java
motor de reglas java (14)
Estoy explorando diferentes maneras de crear un motor de reglas de negocio simple en Java. Necesito presentarle al cliente una aplicación web simple que le permita configurar un montón de reglas. Una muestra de la base de reglas podría verse así:
Aquí hay un ejemplo:
IF (PATIENT_TYPE = "A" AND ADMISSION_TYPE="O")
SEND TO OUTPATIENT
ELSE IF PATIENT_TYPE = "B"
SEND TO INPATIENT
El motor de reglas es bastante simple, la acción final podría ser solo una de dos acciones, el envío a pacientes hospitalizados o ambulatorios. Los operadores involucrados en una expresión podrían ser =,>,<,!=
Y los operadores lógicos entre expresiones son AND, OR and NOT
.
Quiero crear una aplicación web en la que el usuario escriba un pequeño script en un área de textarea
, y evaluaría la expresión. De esta manera, las reglas de negocios se explican en inglés simple y el usuario de negocios tiene un control completo sobre la lógica.
A partir de la investigación que hice hasta el momento, encontré ANTLR
y escribí mi propio lenguaje de scripting como posibles opciones para resolver este problema. No he explorado opciones como el motor de reglas de Drools, porque tengo la sensación de que podría ser una exageración aquí. ¿Has tenido alguna experiencia en resolver este tipo de problemas? Si es así, ¿cómo lo hiciste?
Básicamente ... no lo hagas
Para entender por qué ver:
- http://thedailywtf.com/Articles/The_Customer-Friendly_System.aspx
- thedailywtf.com/Articles/The-Mythical-Business-Layer.aspx
- thedailywtf.com/Articles/Soft_Coding.aspx
Sé que parece una gran idea desde lejos, pero el motor de reglas de negocios siempre será más difícil de mantener, implementar y depurar luego el lenguaje de programación en el que estaba escrito ; no inventes tus propios lenguajes de programación si puedes ayudar. eso.
Personalmente, he pasado por ese camino en una ex empresa y he visto a dónde ha ido después de un par de años ( guiones gigantes no recuperables que se encuentran en una base de datos escrita en un lenguaje que viene directamente de una dimensión paralela donde Dios nos odia que en el final nunca cumple 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 programación para las "adaptaciones de reglas de negocios" y que poco entienden que al final estarán peor y que para atraer a este tipo de clientes Tienes que hacer algo en esta dirección, pero hagas lo que hagas, no inventes algo propio .
Hay una gran cantidad de lenguajes de scripts decentes que vienen con buenas herramientas (que no requieren compilación, por lo que se pueden cargar dinámicamente, etc.) que pueden ser interconectadas y llamadas desde código Java y aprovechar las ventajas de las API de Java implementadas. disponible, consulte http://www.slideshare.net/jazzman1980/j-ruby-injavapublic#btnNext por ejemplo, posiblemente también Jython,
y cuando el cliente deje de escribir estos guiones, quedará con el feliz deber de mantener su legado fallido ; asegúrese de que ese legado sea tan indoloro como sea posible.
Como el código de análisis con Java solo es un suicidio de implementación, es posible que desee escribir un compilador simple con Jflex y CUP , que son la versión de Java de GNU FLEX
y YACC
. De esta manera, puede generar tokens simples con Jflex
(un token es una palabra clave como IF
, ELSE
, etc.) mientras que CUP consumirá esos token para ejecutar algún código.
Converse bien con sus usuarios y pregúnteles por qué esto debe ser configurable y qué cambios en la configuración esperan que aparezcan. Averigüe qué cambios próximos son ciertos, probables, remotamente posibles, escandalosamente improbables. Y qué tan rápido necesitarían ser implementados. Para cada cambio, ¿sería aceptable escribir una pequeña actualización o no?
Con esta cantidad de flexibilidad necesaria en mente, evalúe la opción de incluir su propia solución en comparación con la de incorporar un motor completo. "Pruebe" su solución simple frente a los escenarios de cambio venideros anotando brevemente cómo se implementaría cada cambio. Está bien si algunos escenarios improbables tienen un gran costo. Sin embargo, si los escenarios probables también son costosos, es mejor que elijas una solución más genérica.
En cuanto a las opciones a considerar, me gustan tanto los drools como la sugerencia de escribir uno propio. Una tercera opción: al implementar un paquete de registro financiero con actualizaciones legales anuales, hemos tenido bastante éxito implementando las reglas en el código pero dejando sus configuraciones configurables en las tablas SQL. Entonces, en tu caso, eso podría significar una tabla como esta:
patient_type | admission_type | inpatient_or_outpatient
-------------------------------------------------------
''A'' | ''O'' | ''Outpatient''
''B'' | NULL | ''Inpatient''
(Nuestras tablas tienden a tener columnas de validez de fecha a fecha que le permiten al usuario realizar cambios)
Si termina escribiendo un DSL, eche un vistazo a http://martinfowler.com/books/dsl.html que ofrece descripciones detalladas de los distintos enfoques. Como advertencia: en su sección de preguntas y respuestas, Martin Fowler escribe:
Entonces, ¿este es el gancho: la gente de negocios escribe las reglas por sí misma?
En general no lo creo. Es mucho trabajo crear un entorno que permita a las personas de negocios escribir sus propias reglas. Debe crear una herramienta de edición cómoda, herramientas de depuración, herramientas de prueba, etc. Usted obtiene la mayor parte del beneficio de los DSL que enfrentan las empresas al hacer lo suficiente para permitir que las personas de negocios puedan leer las reglas. Luego pueden revisarlos para verificar su precisión, hablar sobre ellos con los desarrolladores y redactar cambios para que los desarrolladores los implementen correctamente. Hacer que los DSL sean legibles para el negocio es mucho menos esfuerzo que el que se puede escribir en el negocio, pero produce la mayoría de los beneficios. Hay momentos en los que vale la pena hacer el esfuerzo de hacer que los DSL puedan escribirse en el negocio, pero es un objetivo más avanzado.
De acuerdo con la experiencia pasada, la solución basada en reglas de "texto simple" es MUY mala idea, deja mucho margen para el error, además, tan pronto como tenga que agregar reglas simples o complejas, se convertirá en una pesadilla para codificar / depurar / mantener / modificar ...
Lo que hice (y funciona excepcionalmente bien) es crear clases estrictas / concretas que extienden una regla abstracta (1 para cada tipo de regla). Cada implementación sabe qué información necesita y cómo procesarla para obtener el resultado deseado.
En el lado web / front-end, creará un componente (para cada implementación de regla) que coincida estrictamente con esa regla. Luego, puede dar al usuario la opción de qué regla le gustaría usar y actualizar la interfaz en consecuencia (por recarga de página / javascript).
Cuando la regla se agrega / modifica, se itera en todas las implementaciones de la regla para obtener la implementación correspondiente y hacer que la implementación analice los datos sin procesar (id recomienda usar json) desde el front-end, luego ejecute esa regla.
public abstract class AbstractRule{
public boolean canHandle(JSONObject rawRuleData){
return StringUtils.equals(getClass().getSimpleName(), rawRuleData.getString("ruleClassName"));
}
public abstract void parseRawRuleDataIntoThis(JSONObject rawRuleData); //throw some validation exception
public abstract RuleResult execute();
}
public class InOutPatientRule extends AbstractRule{
private String patientType;
private String admissionType;
public void parseRawRuleDataIntoThis(JSONObject rawRuleData){
this.patientType = rawRuleData.getString("patientType");
this.admissionType= rawRuleData.getString("admissionType");
}
public RuleResultInOutPatientType execute(){
if(StringUtils.equals("A",this.patientType) && StringUtils.equals("O",this.admissionType)){
return //OUTPATIENT
}
return //INPATIENT
}
}
En lugar de crear su propio motor de reglas, es posible que desee considerar el motor Open Source N-CUBE, un motor de reglas Java de código abierto que utiliza Groovy como el lenguaje específico del dominio (DSL).
Es un motor de reglas secuenciales en oposición a un motor de reglas no secuencial como un motor de reglas basado en RETE. El beneficio de un motor de reglas secuenciales es que es mucho más fácil depurar las reglas. Tratar de descifrar inferencias de conjuntos de reglas realmente grandes puede ser muy difícil, pero con un motor de reglas secuencial como N-CUBE, rastrear las reglas es muy similar a seguir la ''lógica de código'' secuencial.
N-CUBE tiene soporte incorporado para las tablas de decisión y los árboles de decisión. Las tablas y árboles de decisión dentro de N-CUBE permiten que los datos o el código se ejecuten dentro de las celdas, muy parecido a un Excel multidimensional. El lenguaje ''macro'' (DSL) es Groovy. Al escribir código dentro de una celda, no necesita definir una declaración de paquete, importaciones, un nombre de clase o función; todo esto se agrega para usted, lo que hace que los fragmentos de código DSL sean fáciles de leer / escribir.
Este motor de reglas está disponible en GitHub en https://github.com/jdereg/n-cube .
En lugar de textArea, proporcione es un cuadro de opción para el estado fijo (PATIENT_TYPE) y los operadores fijos () y terminará con él. De todos modos controlas cómo se ve la aplicación web.
Esto es lo que yo haría. Creo un conjunto de variables de expresiones regulares, en función de la coincidencia, codifico la lógica empresarial. Si el conjunto de reglas se vuelve más complejo que esto, me gustaría ir para la implementación CommandLineParser
de apache commons en el servidor.
Pero puede usar GUI / HTML y un conjunto de menús desplegables y subgrupos. De esa manera usted puede hacer consultas de base de datos con claridad.
Implementar un simple sistema de evaluación 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 de singleton para almacenar cada operación disponible en un mapa miembro. La operación en sí utiliza un patrón de comando para proporcionar una extensibilidad flexible, mientras que la acción respectiva para una expresión válida sí hace uso del patrón de envío. Por último, no menos importante, se utiliza un patrón de intérprete para validar cada regla.
Una expresión como la que se presenta en el ejemplo anterior consta de operaciones, variables y valores. En referencia a un wiki-example todo lo que se puede declarar es una Expression
. La interfaz por lo tanto se ve así:
import java.util.Map;
public interface Expression
{
public boolean interpret(final Map<String, ?> 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 una Variable
o su Value
. La definición de una Variable
se enumera 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<String, ?> bindings)
{
return true;
}
}
La validación de un nombre de variable no tiene mucho sentido, por lo tanto, true
se devuelve de forma predeterminada. Lo mismo se aplica a un valor de una variable que se mantiene tan genérica como sea posible al definir solo un tipo de base:
import java.util.Map;
public class BaseType<T> implements Expression
{
public T value;
public Class<T> type;
public BaseType(T value, Class<T> type)
{
this.value = value;
this.type = type;
}
public T getValue()
{
return this.value;
}
public Class<T> getType()
{
return this.type;
}
@Override
public boolean interpret(Map<String, ?> 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) || "false".equals(string))
return new BaseType<>(Boolean.getBoolean(string), Boolean.class);
else if (string.startsWith("''"))
return new BaseType<>(string, String.class);
else if (string.contains("."))
return new BaseType<>(Float.parseFloat(string), Float.class);
else
return new BaseType<>(Integer.parseInt(string), Integer.class);
}
}
La clase BaseType
contiene un método de fábrica para generar tipos de valor concretos para un tipo de Java específico.
Una Operation
ahora es una expresión especial como AND
, NOT
, =
, ... La Operation
abstracta de la clase base 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 el true
convierte en false
y viceversa. Pero AND
por otro lado, combina lógicamente la expresión izquierda y derecha, forzando que ambas expresiones sean 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<Expression> stack);
protected Integer findNextExpression(String[] tokens, int pos, Stack<Expression> 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;
}
}
Dos operaciones probablemente saltan al ojo. int parse(String[], int, Stack<Expression>);
refacta 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 utiliza 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 coloca en la pila y el valor de retorno aquí solo devuelve la posición del último token utilizado por la expresión creada. Por lo tanto, el valor int se utiliza para omitir tokens ya procesados.
La operación AND
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<Expression> 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<String, ?> bindings)
{
return leftOperand.interpret(bindings) && rightOperand.interpret(bindings);
}
}
En el parse
, probablemente vea que la expresión ya generada desde el lado izquierdo se toma de la pila, luego el lado derecho se analiza y nuevamente se toma de la pila para finalmente empujar la nueva operación AND
que contiene ambas expresiones, la izquierda y la 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<Expression> 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<String, ?> bindings)
{
return !this.rightOperand.interpret(bindings);
}
}
El operador =
se usa para verificar el valor de una variable si en realidad es igual a un valor específico en el mapa de enlaces proporcionado como argumento en el método de interpret
.
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<Expression> 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<String, ?> 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 método de parse
, se asigna un valor a una variable con la variable en el lado izquierdo del símbolo =
y el valor en el lado derecho.
Además, la interpretación verifica 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.
A medida que el análisis real de las expresiones se refacta 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<Expression> 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 método de copy
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, se produce una modificación de este objeto. Si solo tenemos una operación de ese tipo en nuestra expresión, esto no importa. Si, sin embargo, 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 creadas previamente de ese tipo, necesitamos crear una nueva instancia de la operación - copy()
logra esto.
Operations
es un contenedor que contiene operaciones previamente registradas 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<String, Operation> 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<String> getDefinedSymbols()
{
return this.operations.keySet();
}
}
Aparte del patrón de enlet singleton nada realmente lujoso aquí.
Una 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<Expression> expressions;
private ActionDispatcher dispatcher;
public static class Builder
{
private List<Expression> 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<Expression> expressions, ActionDispatcher dispatcher)
{
this.expressions = expressions;
this.dispatcher = dispatcher;
}
public boolean eval(Map<String, ?> 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 expresiones múltiples si se desea para la misma acción. Además, la Rule
define un NullActionDispatcher
de forma predeterminada. Si una expresión se evalúa con éxito, el despachador activará un método fire()
, que procesará la acción que debe ejecutarse en una validación exitosa. El patrón nulo se usa aquí para evitar tratar con valores nulos en caso de que no se requiera la ejecución de una acción, ya que solo se debe realizar una validación true
o false
. La interfaz por lo tanto es simple también:
public interface ActionDispatcher
{
public void fire();
}
Como realmente no sé cuáles deberían ser sus acciones de OUTPATIENT
o OUTPATIENT
, el método fire()
solo activa 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<String, String> 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í son solo una clase contenedora simple para reglas y propagan los 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 utilizarás la tuya propia. Además, no incluí ningún manejo de excepciones, se lo dejo a todos los que copiarán y pegarán el código :)
Uno podría argumentar que el análisis obviamente debería ocurrir en el analizador en lugar de 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 usar un sistema basado en reglas, una red petri o incluso un BPMN en combinación con el motor de fuente abierta Activiti Engine sería posible para lograr esta tarea. En este caso, las operaciones ya están definidas dentro del lenguaje, solo necesita definir las declaraciones concretas como tareas que pueden ejecutarse automáticamente, y dependiendo del resultado de una tarea (es decir, la declaración única), se procederá a través del "gráfico" . Por lo tanto, el modelado generalmente se realiza en un editor gráfico o frontend para evitar tratar con la naturaleza XML del lenguaje BPMN.
Si está buscando algo más ligero que drools pero con una funcionalidad similar, puede consultar http://smartparam.org/ project. Permite almacenar parámetros tanto en archivos de propiedades como en bases de datos.
Te estás preparando para el fracaso por dos razones principales:
- Analizar el texto libre del usuario es DIFÍCIL.
- Escribir parsers en Java es algo engorroso
La solución 1. lo empujará al dominio borroso de la PNL, para lo cual puede usar una herramienta como OpenNLP o algo de ese ecosistema. Debido a la gran cantidad de formas sutilmente diferentes en que el usuario puede escribir las cosas, encontrará que su pensamiento se inclina hacia una gramática más formal. Hacer este trabajo lo terminará en una solución de tipo DSL, o tendrá que diseñar su propio lenguaje de programación.
He tenido resultados razonables utilizando los combinadores de analizador de Scala para analizar tanto el lenguaje natural como las gramáticas más formales. Los problemas son los mismos, pero el código que tienes que escribir para resolverlos es más legible.
En pocas palabras, incluso si está pensando en un lenguaje de reglas muy simple, encontrará que subestima la cantidad de escenarios que tiene que probar. NeilA tiene derecho a recomendarle que reduzca la complejidad creando una IU adecuada para cada tipo de regla. No trates de ser demasiado genérico, o explotará en tu cara.
Un simple motor de reglas se puede construir sobre cierres, es decir, en Groovy:
def sendToOutPatient = { ... };
def sendToInPatient = { ... };
def patientRule = { PATIENT_TYPE ->
{''A'': sendToOutPatient,
''B'': sendToInPatient}.get(PATIENT_TYPE)
}
static main(){
(patientRule(''A''))()
}
Podría definir sus reglas como cierres, reutilizarlos / reasignarlos o incluso construir un DSL sobre ellos.
Y Groovy se puede incrustar fácilmente en Java, por ejemplo:
GroovyShell shell = new GroovyShell(binding);
binding.setVariable("foo", "World");
System.out.println(shell.evaluate("println ''Hello ${foo}!'';));
Yo sugeriría usar algo como Drools . Crear tu propia solución personalizada sería una exageración porque tendrías 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 crear 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 reglas de drools , nunca capturaría todas sus necesidades. El lenguaje de reglas de Drools sería lo suficientemente simple para reglas simples. Además, puede proporcionarle una documentación bien formada. Si planea controlar las reglas creadas por el usuario final y aplicadas en el sistema, tal vez sería más inteligente crear una interfaz gráfica de usuario que formaría las reglas aplicadas en drools.
Espero haberte ayudado!
Implementar un motor de reglas no es trivial. Un sistema basado en reglas significativas tiene un motor de inferencia que soporta tanto el encadenamiento hacia adelante como el encadenamiento hacia atrás, así como las estrategias de búsqueda en primer lugar y primero en profundidad. Easy Rules no tiene nada de esto, solo ejecuta todas las reglas una y solo una vez. Drools admite el encadenamiento hacia adelante y hacia atrás, y afaik también es compatible con la profundidad primero y el ancho primero. Se explica here .
Desde mi experiencia, Drools es el único motor de reglas significativo para Java. Tiene sus limitaciones. Debo decir que he usado Drools hace más de 5 años.