Ответ 1
Реализация простой системы оценки на основе правил в Java не так уж и трудна. Вероятно, парсер для выражения - это самый сложный материал. В приведенном ниже примере кода используется несколько шаблонов для достижения желаемой функциональности.
Одноэлементный шаблон используется для хранения каждой доступной операции в карте участника. Сама операция использует шаблон команды для обеспечения гибкой расширяемости, в то время как соответствующее действие для действительного выражения использует шаблон диспетчеризации. Последний спад не в последнюю очередь, шаблон интерпретатора используется для проверки каждого правила.
Выражение, представленное в приведенном выше примере, состоит из операций, переменных и значений. В отношении wiki-example все, что можно объявить, это Expression
. Поэтому интерфейс выглядит следующим образом:
import java.util.Map;
public interface Expression
{
public boolean interpret(final Map<String, ?> bindings);
}
В то время как пример на wiki-странице возвращает int (они реализуют калькулятор), нам нужно только логическое значение возврата здесь, чтобы решить, должно ли выражение запускать действие, если выражение оценивается как true
.
Выражение может, как указано выше, быть либо операцией типа =
, AND
, NOT
,... или Variable
, либо ее Value
. Определение a Variable
приводится ниже:
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;
}
}
Проверка имени переменной не имеет такого смысла, поэтому true
возвращается по умолчанию. То же самое справедливо для значения переменной, которая поддерживается как можно более общая при определении только BaseType
:
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);
}
}
Класс BaseType
содержит метод factory для генерации конкретных типов значений для определенного типа Java.
An Operation
теперь является специальным выражением типа AND
, NOT
, =
,... Абстрактный базовый класс Operation
определяет левый и правый операнды, так как операнд может ссылаться на большее, чем одно выражение. F.E. NOT
, вероятно, относится только к его правому выражению и отрицает его результат проверки, поэтому true
превращается в false
и наоборот. Но AND
на другой стороне сочетает левое и правое выражение логически, заставляя оба выражения быть истинными при проверке.
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;
}
}
Возможно, две операции прыгают в глаза. int parse(String[], int, Stack<Expression>);
реорганизует логику разбора конкретной операции соответствующему классу операций, так как он, вероятно, лучше знает, что ему нужно для создания действительной операции. Integer findNextExpression(String[], int, stack);
используется для поиска правой части операции при разборе строки в выражении. Может показаться странным возвращать int вместо выражения, но выражение помещается в стек, а возвращаемое значение здесь возвращает позицию последнего токена, используемого созданным выражением. Таким образом, значение int используется для пропуска уже обработанных токенов.
Операция AND
выглядит примерно так:
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);
}
}
В parse
вы, вероятно, увидите, что уже сгенерированное выражение с левой стороны взято из стека, затем правая сторона анализируется и снова берется из стека, чтобы окончательно нажать новую операцию AND
, содержащую обе, левое и правое выражение, обратно в стек.
NOT
аналогичен в этом случае, но только устанавливает правую сторону, как описано ранее:
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);
}
}
Оператор =
используется для проверки значения переменной, если он фактически равен определенному значению в карте привязок, предоставленной в качестве аргумента в методе 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;
}
}
Как видно из метода parse
, значение присваивается переменной с переменной, находящейся в левой части символа =
, и значением с правой стороны.
Кроме того, интерпретация проверяет наличие имени переменной в привязках переменных. Если он недоступен, мы знаем, что этот термин не может оцениваться как истинный, поэтому мы можем пропустить процесс оценки. Если он присутствует, мы извлекаем информацию с правой стороны (= Value part) и сначала проверяем, является ли тип класса равным, и если да, если фактическое значение переменной соответствует привязке.
Поскольку фактический синтаксический разбор выражений реорганизуется в операции, фактический синтаксический анализатор довольно тонкий:
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();
}
}
Здесь метод copy
, вероятно, самый интересный. Поскольку разбор довольно общий, мы не знаем заранее, какая операция обрабатывается в настоящее время. При возврате найденной операции среди зарегистрированных приводит к модификации этого объекта. Если у нас есть только одна операция такого рода в нашем выражении, это не имеет значения - если мы, однако, имеем несколько операций (например, две или более равных-операции), операция повторно используется и, следовательно, обновляется с новым значением. Поскольку это также изменяет ранее созданные операции такого типа, нам нужно создать новый экземпляр операции - copy()
достигает этого.
Operations
- это контейнер, который содержит ранее зарегистрированные операции и сопоставляет операцию с указанным символом:
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();
}
}
Рядом с рисунком одноэлементного перечисления ничего действительно не интересно.
A Rule
теперь содержит одно или несколько выражений, которые при оценке могут вызвать определенное действие. Поэтому правилу необходимо удержать ранее разобранные выражения и действие, которое должно быть инициировано в случае успеха.
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;
}
}
Здесь шаблон здания используется, чтобы иметь возможность добавлять несколько выражений, если это необходимо для того же действия. Кроме того, Rule
по умолчанию определяет NullActionDispatcher
. Если выражение успешно оценивается, диспетчер запускает метод fire()
, который будет обрабатывать действие, которое должно быть выполнено при успешной проверке. Нулевой шаблон используется здесь, чтобы избежать обработки нулевых значений в случае, если не требуется выполнение действия, поскольку должны выполняться только проверки true
или false
. Интерфейс также прост:
public interface ActionDispatcher
{
public void fire();
}
Как я действительно не знаю, какими должны быть ваши действия INPATIENT
или OUTPATIENT
, метод fire()
запускает вызов метода System.out.println(...);
:
public class InPatientDispatcher implements ActionDispatcher
{
@Override
public void fire()
{
// send patient to in_patient
System.out.println("Send patient to IN");
}
}
И последнее, но не менее важное: простой основной метод проверки поведения кода:
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
здесь просто класс контейнера для правил и распространяется вызов eval(bindings);
для каждого определенного правила.
Я не включаю другие операции, так как сообщение здесь уже давно, но вам не нужно слишком сложно реализовать их самостоятельно, если вы этого хотите. Кроме того, я не включил свою структуру пакета, поскольку вы, вероятно, будете использовать свою собственную. Furhtermore, я не включал обработку исключений, я оставляю это всем, кто собирается скопировать и вставить код:)
Можно утверждать, что синтаксический анализ, очевидно, должен происходить в парсере вместо конкретных классов. Я знаю об этом, но, с другой стороны, при добавлении новых операций вам необходимо изменить синтаксический анализатор, а также новую операцию, вместо того, чтобы касаться только одного класса.
Вместо использования системы, основанной на правилах, сеть petri или даже BPMN в сочетании с открытым исходным кодом Activiti Engine можно было бы достичь этой задачи. Здесь операции уже определены внутри языка, вам нужно только определить конкретные операторы как задачи, которые могут выполняться автоматически - и в зависимости от результата задачи (т.е. Одного оператора) она будет проходить через" график", Поэтому моделирование обычно выполняется в графическом редакторе или интерфейсе, чтобы избежать использования XML-характера языка BPMN.