webentwicklung-frage-antwort-db.com.de

erstellen einer einfachen Regel-Engine in Java

Ich untersuche verschiedene Möglichkeiten, um eine einfache Geschäftsregel-Engine in Java zu erstellen. Ich muss dem Client eine einfache Webanwendung präsentieren, mit der er eine Reihe von Regeln konfigurieren kann. Ein Beispiel für eine Regelbasis könnte folgendermaßen aussehen:

Hier ein Beispiel:

 IF (PATIENT_TYPE = "A" AND ADMISSION_TYPE="O")
 SEND TO OUTPATIENT
 ELSE IF PATIENT_TYPE = "B" 
 SEND TO INPATIENT

Die Regel-Engine ist ziemlich einfach, die endgültige Aktion könnte nur eine von zwei Aktionen sein: Senden an stationäre oder ambulante Patienten. Die an einem Ausdruck beteiligten Operatoren könnten =,>,<,!= sein, und logische Operatoren zwischen Ausdrücken sind AND, OR and NOT

Ich möchte eine Webanwendung erstellen, in der Benutzer ein kleines Skript in einer textarea schreiben, und ich würde den Ausdruck auswerten. Auf diese Weise werden Geschäftsregeln in einfachem Englisch erklärt, und der Geschäftsbenutzer kann die Logik vollständig steuern.

Bei meiner Recherche bin ich auf ANTLR gestoßen und habe meine eigene Skriptsprache als mögliche Optionen zur Lösung dieses Problems geschrieben. Ich habe keine Optionen wie die Regelmaschine von Drools untersucht, weil ich das Gefühl habe, dass es hier ein Overkill sein könnte. Haben Sie Erfahrung mit dieser Art von Problemen gemacht? Wenn ja, wie bist du dazu vorgegangen? 

28
Jay

Das Implementieren eines einfachen regelbasierten Bewertungssystems in Java ist nicht so schwer zu erreichen. Wahrscheinlich ist der Parser für den Ausdruck das komplizierteste Zeug. Im folgenden Beispielcode werden einige Muster verwendet, um die gewünschte Funktionalität zu erzielen.

Ein Singleton-Muster wird verwendet, um jede verfügbare Operation in einer Mitgliedskarte zu speichern. Die Operation selbst verwendet ein Befehlsmuster, um eine flexible Erweiterbarkeit bereitzustellen, während die entsprechende Aktion für einen gültigen Ausdruck das Dispatching-Muster verwendet. Last Bust Nicht zuletzt wird zur Validierung jeder Regel ein Interpreter-Pattern verwendet.

Ein Ausdruck wie in Ihrem obigen Beispiel besteht aus Operationen, Variablen und Werten. In Bezug auf ein Wiki-Beispiel ist alles, was deklariert werden kann, ein Expression. Das Interface sieht also so aus:

import Java.util.Map;

public interface Expression
{
    public boolean interpret(final Map<String, ?> bindings);
}

Während das Beispiel auf der Wiki-Seite ein int zurückgibt (sie implementieren einen Taschenrechner), benötigen wir hier nur einen booleschen Rückgabewert, um zu entscheiden, ob ein Ausdruck eine Aktion auslösen soll, wenn der Ausdruck true ergibt.

Ein Ausdruck kann, wie oben angegeben, entweder eine Operation wie =, AND, NOT, ... oder ein Variable oder sein Value sein. Die Definition eines Variable ist nachfolgend aufgeführt:

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;
    }
}

Das Validieren eines Variablennamens macht nicht viel Sinn, daher wird standardmäßig true zurückgegeben. Gleiches gilt für einen Wert einer Variablen, der bei der Definition eines BaseType nur so allgemein wie möglich gehalten wird:

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);
    }
}

Die Klasse BaseType enthält eine Factory-Methode zum Generieren konkreter Werttypen für einen bestimmten Typ Java.

Ein Operation ist jetzt ein spezieller Ausdruck wie AND, NOT, =, ... Die abstrakte Basisklasse Operation definiert einen linken und einen rechten Operanden als Der Operand kann auf mehrere Ausdrücke verweisen. F.e. NOT bezieht sich wahrscheinlich nur auf seinen rechten Ausdruck und negiert sein Validierungsergebnis, also true wird zu false und umgekehrt. Aber AND auf der anderen Seite kombiniert einen linken und einen rechten Ausdruck logisch und zwingt beide Ausdrücke bei der Validierung, wahr zu sein.

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;
     }
}

Zwei Operationen springen wahrscheinlich ins Auge. int parse(String[], int, Stack<Expression>); refaktoriert die Logik zum Parsen der konkreten Operation auf die jeweilige Operationsklasse, da sie wahrscheinlich am besten weiß, was sie benötigt, um eine gültige Operation zu instanziieren. Integer findNextExpression(String[], int, stack); wird verwendet, um die rechte Seite der Operation zu finden, während der String in einen Ausdruck zerlegt wird. Es mag seltsam klingen, hier statt eines Ausdrucks ein int zurückzugeben, aber der Ausdruck wird auf den Stack verschoben, und der Rückgabewert gibt hier nur die Position des letzten Tokens zurück, das vom erstellten Ausdruck verwendet wurde. Der int-Wert wird also verwendet, um bereits verarbeitete Token zu überspringen.

Die AND -Operation sieht folgendermaßen aus:

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);
    }
}

In parse sehen Sie wahrscheinlich, dass der bereits generierte Ausdruck von der linken Seite aus dem Stapel entnommen wird, dann wird die rechte Seite analysiert und erneut aus dem Stapel entnommen, um schließlich die neue AND -Operation mit beiden zu verschieben , der linke und rechte Ausdruck, zurück auf den Stapel.

NOT ist in diesem Fall ähnlich, legt jedoch nur die rechte Seite wie oben beschrieben fest:

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);
    }    
}

Der Operator = wird verwendet, um den Wert einer Variablen zu überprüfen, wenn er tatsächlich einem bestimmten Wert in der Bindungszuordnung entspricht, die als Argument in der Methode interpret angegeben ist.

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;
    }
}

Wie aus der Methode parse ersichtlich, wird einer Variablen ein Wert zugewiesen, wobei sich die Variable auf der linken Seite des Symbols = und der Wert auf der rechten Seite befindet.

Darüber hinaus prüft die Interpretation die Verfügbarkeit des Variablennamens in den Variablenbindungen. Wenn es nicht verfügbar ist, wissen wir, dass dieser Begriff nicht als wahr bewertet werden kann, sodass wir den Bewertungsprozess überspringen können. Wenn es vorhanden ist, extrahieren wir die Informationen von der rechten Seite (= Werteteil) und prüfen zuerst, ob der Klassentyp gleich ist und wenn ja, ob der tatsächliche Variablenwert mit der Bindung übereinstimmt.

Da das eigentliche Parsen der Ausdrücke in die Operationen umgestaltet wird, ist der eigentliche Parser ziemlich schmal:

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();
    }
}

Hier ist die copy -Methode wahrscheinlich die interessanteste Sache. Da das Parsen eher generisch ist, wissen wir nicht im Voraus, welche Operation gerade verarbeitet wird. Wenn eine gefundene Operation unter den registrierten zurückgegeben wird, führt dies zu einer Änderung dieses Objekts. Wenn wir nur eine Operation dieser Art in unserem Ausdruck haben, spielt dies keine Rolle - wenn wir jedoch mehrere Operationen haben (z. B. zwei oder mehr Gleichheitsoperationen), wird die Operation wiederverwendet und daher mit dem neuen Wert aktualisiert. Da dies auch zuvor erstellte Operationen dieser Art ändert, müssen wir eine neue Instanz der Operation erstellen - dies wird durch copy() erreicht.

Operations ist ein Container, der zuvor registrierte Operationen enthält und die Operation einem angegebenen Symbol zuordnet:

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();
    }
}

Außer dem Enum-Singleton-Muster ist hier nichts wirklich Besonderes.

Ein Rule enthält nun einen oder mehrere Ausdrücke, die bei der Auswertung eine bestimmte Aktion auslösen können. Die Regel muss daher die zuvor analysierten Ausdrücke und die Aktion enthalten, die im Erfolgsfall ausgelöst werden soll.

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;
    }
}

Hier wird ein Gebäudemuster verwendet, um bei Bedarf mehrere Ausdrücke für dieselbe Aktion hinzufügen zu können. Darüber hinaus definiert das Rule standardmäßig ein NullActionDispatcher. Wenn ein Ausdruck erfolgreich ausgewertet wird, löst der Dispatcher eine fire() -Methode aus, die die Aktion verarbeitet, die bei erfolgreicher Validierung ausgeführt werden soll. Das Nullmuster wird hier verwendet, um den Umgang mit Nullwerten zu vermeiden, falls keine Aktionsausführung erforderlich ist, da nur eine true- oder false -Validierung durchgeführt werden sollte. Die Oberfläche ist daher auch einfach:

public interface ActionDispatcher
{
    public void fire();
}

Da ich nicht genau weiß, wie Ihre Aktionen INPATIENT oder OUTPATIENT lauten sollen, löst die Methode fire() nur einen Methodenaufruf System.out.println(...); aus:

public class InPatientDispatcher implements ActionDispatcher
{
    @Override
    public void fire()
    {
        // send patient to in_patient
        System.out.println("Send patient to IN");
    }
}

Last but not least eine einfache Hauptmethode zum Testen des Verhaltens des Codes:

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 ist hier nur eine einfache Containerklasse für Regeln und verteilt den Aufruf eval(bindings); auf jede definierte Regel.

Ich schließe andere Operationen nicht ein, da der Beitrag hier bereits viel zu lang ist, aber es sollte nicht zu schwierig sein, sie auf eigene Faust umzusetzen, wenn Sie dies wünschen. Außerdem habe ich meine Paketstruktur nicht angegeben, da Sie wahrscheinlich Ihre eigene verwenden werden. Außerdem habe ich keine Ausnahmebehandlung hinzugefügt, das überlasse ich jedem, der den Code kopieren und einfügen wird :)

Man könnte argumentieren, dass das Parsen offensichtlich im Parser statt in den konkreten Klassen stattfinden sollte. Ich bin mir dessen bewusst, aber wenn Sie neue Operationen hinzufügen, müssen Sie sowohl den Parser als auch die neue Operation ändern, anstatt nur eine einzelne Klasse berühren zu müssen.

Anstatt ein regelbasiertes System zu verwenden, wäre ein Petri-Netz oder sogar ein BPMN in Kombination mit dem Open Source Activiti Engine möglich, um diese Aufgabe zu lösen. Hier sind die Operationen bereits in der Sprache definiert, Sie müssen nur die konkreten Anweisungen als Aufgaben definieren, die automatisch ausgeführt werden können - und je nach Ergebnis einer Aufgabe (dh der einzelnen Anweisung) wird sie ihren Weg durch den "Graphen" finden. . Die Modellierung erfolgt daher in der Regel in einem grafischen Editor oder Frontend, um den XML-Charakter der BPMN-Sprache nicht zu beeinträchtigen.

108
Roman Vottner

Grundsätzlich ... Tu es nicht

Um zu verstehen, warum sehen:

  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

Ich weiß, es sieht aus der Ferne nach einer großartigen Idee aus, aber die Geschäftsregeln-Engine wird immer schwieriger zu warten, bereitzustellen und zu debuggen sein als die Programmiersprache, in der sie geschrieben wurde - Machen Sie sich keine Gedanken Eigene Programmiersprachen, wenn Sie helfen können.

Ich persönlich war in einer Ex-Firma auf diesem Weg und habe gesehen, wohin es nach ein paar Jahren riesiger, nicht ausbaufähiger Skripte geht, die in einer Datenbank gespeichert sind, die in einer Sprache geschrieben ist, die direkt aus einer parallelen Dimension stammt, in der Gott lebte hasst es, dass am Ende nie 100% der Kundenerwartungen erfüllt werden, weil sie nicht so mächtig sind wie eine richtige Programmiersprache und gleichzeitig viel zu verworren und böse, als dass Entwickler damit umgehen könnten (ohne Rücksicht auf den Kunden) ).

Ich weiß, dass es eine bestimmte Art von Kunden gibt, die in die Idee verliebt sind, dass sie keine Programmierstunden für "Geschäftsregelanpassungen" zahlen und wenig verstehen, dass es ihnen am Ende schlechter gehen wird und Sie diese Art von Kunden anziehen werden muss etwas in diese Richtung machen - aber was auch immer du tust, erfinde nicht etwas von dir selbst.

Es gibt eine Fülle an anständigen Skriptsprachen, die mit guten Tools (die keine Kompilierung erfordern, also dynamisch hochgeladen werden können usw.) ausgestattet sind, die problemlos über Java Code aufgerufen werden können und Ihre Vorteile bieten implementierte Java APIs, die Sie zur Verfügung stellen, siehe http://www.slideshare.net/jazzman1980/j-Ruby-injavapublic#btnNext zum Beispiel Jython möglicherweise auch,

nd wenn der Client aufgibt, diese Skripte zu schreiben, bleibt Ihnen die glückliche Pflicht, sein gescheitertes Erbe zu bewahren - stellen Sie sicher, dass das Vermächtnis ist so schmerzlos wie es nur geht.

19
bbozo

Ich würde vorschlagen, etwas wie Drools zu verwenden. Das Erstellen einer eigenen benutzerdefinierten Lösung wäre ein Overkill, weil Sie das Debugging durchführen müssten und dennoch Funktionalität bieten, die sicherlich weniger ist als die einer Regelmaschine wie Drools. Ich verstehe, dass Drools eine Lernkurve hat, aber ich würde es nicht mit der Erstellung einer eigenen Sprache oder einer benutzerdefinierten Lösung vergleichen ...

Meiner Meinung nach müsste er/sie etwas lernen, damit ein Benutzer Regeln schreiben kann. Ich denke, Sie könnten für eine Sprache sorgen, die einfacher ist als die drools-Regelsprache , aber Sie würden niemals alle ihre Bedürfnisse erfassen. Die Regelsprache der Drools wäre für einfache Regeln einfach genug. Außerdem können Sie ihm/ihr eine gut formulierte Dokumentation zur Verfügung stellen. Wenn Sie vorhaben, die vom Endbenutzer erstellten und auf das System angewendeten Regeln zu steuern, ist es vielleicht klüger, eine GUI zu erstellen, die die Regeln für die Abläufe darstellt.

Hoffe ich habe geholfen!

10

Wenn Sie auf der Suche nach etwas Leichterem sind, aber mit ähnlicher Funktionalität, können Sie das http://smartparam.org/ -Projekt überprüfen. Es ermöglicht das Speichern von Parametern in Eigenschaftendateien sowie in der Datenbank.

5
Jakub Kubrynski

Sie haben sich aus zwei Gründen für das Scheitern entschieden:

  1. Das Analysieren von Freitext des Benutzers ist hart.
  2. Das Schreiben von Parsern in Java ist etwas umständlich

Lösung von 1. wird entweder Sie in die Fuzzy-Domäne von NLP bringen, für die Sie ein Tool wie OpenNLP oder etwas von diesem Ökosystem verwenden können. Aufgrund der vielen subtil unterschiedlichen Möglichkeiten, mit denen der Benutzer die Dinge aufschreiben kann, werden Sie feststellen, dass Sie zu einer eher formellen Grammatik neigen. Wenn Sie diese Arbeit ausführen, erhalten Sie eine DSL-Lösung, oder Sie müssen Ihre eigene Programmiersprache entwerfen.

Ich habe vernünftige Ergebnisse mit Scala-Parser-Kombinatoren erzielt, um sowohl natürliche Sprache als auch formalisierte Grammatiken zu analysieren. Die Probleme sind die gleichen, aber der Code, den Sie schreiben müssen, um sie zu lösen, ist lesbarer.

Unterm Strich, selbst wenn Sie an eine sehr einfache Regelsprache denken, werden Sie feststellen, dass Sie die Anzahl der Szenarien unterschätzen, die Sie testen müssen. NeilA empfiehlt Ihnen, die Komplexität zu reduzieren, indem Sie für jeden Regeltyp eine geeignete Benutzeroberfläche erstellen. Versuchen Sie nicht, zu generisch zu sein, sonst wird es in Ihrem Gesicht explodieren.

5
iwein

Aus Erfahrung ist die auf Regeln basierende "Klartext" -Lösung eine sehr schlechte Idee, sie lässt zu viel Raum für Fehler, und sobald Sie mehrere einfache oder komplexe Regeln hinzufügen müssen, wird es zu einem Alptraum für Code/debuggen/pflegen/ändern ... 

Was ich getan habe (und es funktioniert außergewöhnlich gut), ist das Erstellen strenger/konkreter Klassen, die eine abstrakte Regel erweitern (1 für jeden Regeltyp). Jede Implementierung weiß, welche Informationen sie benötigt und wie sie verarbeitet werden müssen, um das gewünschte Ergebnis zu erzielen.

Auf der Web-/Front-End-Seite erstellen Sie (für jede Regelimplementierung) eine Komponente, die genau dieser Regel entspricht. Sie können dem Benutzer dann die Option geben, welche Regel er verwenden möchte, und die Schnittstelle entsprechend aktualisieren (per Seite reload/javascript).

Wenn die Regel hinzugefügt/geändert wird, werden alle Regelimplementierungen iteriert, um die entsprechende Implementierung zu erhalten, und die Implementierung der Rohdaten (id recommend using json) vom Frontend aus durchführen zu lassen. Führen Sie dann diese Regel aus.

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
  }
}
5
NeilA

Anstatt eine eigene Regel-Engine zu erstellen, sollten Sie die Open-Source-N-CUBE-Engine betrachten, eine Open Source-Java-Regelengine, die Groovy als Domain Specific Language (DSL) verwendet. 

Es ist eine sequenzielle Regelengine, im Gegensatz zu einer nicht sequentiellen Regelengine wie einer RETE-basierten Regelengine. Der Vorteil einer sequenziellen Regelengine besteht darin, dass das Debuggen der Regeln sehr einfach ist. Der Versuch, Folgerungen aus wirklich großen Regelsätzen zu entschlüsseln, kann sehr schwierig sein, aber bei einer sequentiellen Regel-Engine wie N-CUBE ist das Verfolgen der Regeln der sequenziellen "Codelogik" sehr ähnlich.

N-CUBE unterstützt sowohl Entscheidungstabellen als auch Entscheidungsbäume. Mit den Entscheidungstabellen und -bäumen in N-CUBE können Daten oder Code in den Zellen ausgeführt werden, ähnlich wie bei einem mehrdimensionalen Excel. Die "Makrosprache" (DSL) ist Groovy. Wenn Sie Code innerhalb einer Zelle schreiben, müssen Sie keine Paketanweisungen, Importe, Klassennamen oder Funktionen definieren. All dies wird für Sie hinzugefügt, sodass die DSL-Codeausschnitte leicht zu lesen/schreiben sind.

Diese Regel-Engine ist auf GitHub unter https://github.com/jdereg/n-cube verfügbar.

3

Das würde ich tun. Ich erstelle einen Satz von Regex-Variablen, abhängig von der Übereinstimmung, ich codiere die Geschäftslogik. Wenn der Regelsatz komplexer wird, würde ich die Implementierung von Apache Commons CommandLineParser auf dem Server wählen. 

Sie können jedoch GUI/HTML und eine Reihe von Dropdowns und Unter-Dropdowns verwenden. Auf diese Weise können Sie Datenbankabfragen übersichtlich machen.

2
Siva Tumma

Anstelle von textArea ist Bereitstellen als Auswahlfeld für festen Zustand (PATIENT_TYPE) und feste Operatoren () vorgesehen, und Sie werden damit fertig sein.

2
Sathya

Eine einfache Regel-Engine kann auf Schließungen aufgebaut werden, d. H. In Groovy: 

def sendToOutPatient = { ... };

def sendToInPatient = { ... };

def patientRule = { PATIENT_TYPE ->
    {'A': sendToOutPatient,
     'B': sendToInPatient}.get(PATIENT_TYPE)
}

static main(){
    (patientRule('A'))()
}

Sie können Ihre Regeln als Schließungen definieren, wiederverwenden/neu zuweisen oder sogar eine DSL darüber bauen.

Und Groovy kann einfach in Java eingebettet werden. Beispiel:

GroovyShell Shell = new GroovyShell(binding);
binding.setVariable("foo", "World");
System.out.println(Shell.evaluate("println 'Hello ${foo}!';));
2
Andrey Chaschev

Sprechen Sie gut mit Ihren Benutzern und fragen Sie sie, warum dies konfigurierbar sein muss und welche Änderungen in der Konfiguration sie erwarten werden. Finden Sie heraus, welche anstehenden Änderungen sicher, wahrscheinlich aus der Ferne möglich und unwahrscheinlich unwahrscheinlich sind. Und wie schnell sie umgesetzt werden müssten. Wäre das Schreiben einer kleinen Update-Version für jede Änderung akzeptabel oder nicht?

Berücksichtigen Sie in Anbetracht dieser erforderlichen Flexibilität die Option, Ihre eigene Lösung gegen die Integration eines vollständigen Motors auszurollen. Testen Sie Ihre einfache Lösung mit den anstehenden Änderungsszenarien, indem Sie kurz aufschreiben, wie die einzelnen Änderungen implementiert werden sollen. Es ist in Ordnung, wenn einige unwahrscheinliche Szenarien große Kosten verursachen. Wenn auch die wahrscheinlichen Szenarien teuer sind, sollten Sie sich für eine allgemeinere Lösung entscheiden. 

Was die zu berücksichtigenden Optionen angeht, so mag ich beide, und den Vorschlag, eigene zu schreiben. Eine dritte Option: Bei der Implementierung eines Finanzregistrierungspakets mit jährlichen gesetzlichen Aktualisierungen hatten wir recht gute Erfolge bei der Implementierung der Regeln im Code, wobei die Einstellungen in SQL-Tabellen konfigurierbar waren. In Ihrem Fall könnte dies eine Tabelle bedeuten:

patient_type | admission_type | inpatient_or_outpatient
-------------------------------------------------------
'A'          | 'O'            | 'Outpatient'
'B'          | NULL           | 'Inpatient'

(Unsere Tabellen enthalten in der Regel Datumsangaben und Gültigkeitsspalten, in denen der Benutzer Änderungen vornehmen kann.)

Wenn Sie am Ende ein DSL schreiben, werfen Sie einen Blick auf http://martinfowler.com/books/dsl.html , das ausführliche Beschreibungen der verschiedenen Ansätze bietet Fragen und Antworten Martin Fowler schreibt:

Also ist dies der Haken - Geschäftsleute schreiben die Regeln selbst?

Im Allgemeinen denke ich nicht so. Es ist viel Arbeit, eine Umgebung zu schaffen So können Geschäftsleute ihre eigenen Regeln schreiben. Du musst machen ein komfortables Bearbeitungstool, Debugging-Tools, Test-Tools und so weiter . Sie können den größten Nutzen aus dem Geschäft mit DSLs ziehen, indem Sie genug tun, um Erlauben Sie Geschäftsleuten, die Regeln lesen zu können. Sie können dann Überprüfen Sie sie auf Richtigkeit, besprechen Sie sie mit den Entwicklern und Änderungsentwürfe für Entwickler zur korrekten Implementierung. DSLs werden als Geschäftslesbar ist weit weniger Aufwand als geschäftlich beschreibbar, aber bringt die meisten Vorteile. Es gibt Zeiten, in denen es sich lohnt, es zu machen der Aufwand, um die DSLs für das Business beschreibbar zu machen, aber es ist mehr fortgeschrittenes Ziel.

1
flup

Da das Parsen von Code nur mit Java ein Selbstmord der Implementierung ist, möchten Sie möglicherweise einen einfachen Compiler schreiben, der Jflex und CUP verwendet. Dies sind die Java-Versionen von GNU FLEX und YACC. Auf diese Weise können Sie einfache Token mit Jflex generieren (ein Token ist ein Schlüsselwort wie IF, ELSE usw.), während CUP diese Token verwendet, um Code auszuführen.

1
HAL9000

Es gibt eine Regel-Engine für Clojure namens Clara , die von Java aus verwendet werden kann sowie Clojure [Java] Script. Ich denke, es wäre ziemlich einfach, daraus etwas Nutzbares zu schaffen.

1
claj

Die Implementierung einer Regelengine ist nicht trivial. Ein sinnvolles regelbasiertes System verfügt über eine Inferenz-Engine, die sowohl Vorwärts- als auch Rückwärtsverkettung sowie Breitensuchstrategien unterstützt. Easy Rules hat nichts davon, führt alle Regeln nur einmal und nur einmal aus. Drools unterstützt Vorwärts- und Rückwärtsverkettung, und afaik unterstützt auch zuerst Tiefe und Breite. Es wird erklärt hier .

Aus meiner Erfahrung ist Drools die einzig sinnvolle Rule Engine für Java. Es hat seine Grenzen. Ich muss sagen, ich habe vor 5 Jahren Drools verwendet. 

0
Christine