Реализовать в Java простую систему оценки, основанную на правилах, не так уж и сложно. Наверное, синтаксический анализатор выражения - самая сложная вещь. В приведенном ниже примере кода используется несколько шаблонов для достижения желаемой функциональности.
Одноэлементный шаблон используется для хранения каждой доступной операции в карте элементов. Сама операция использует шаблон команды для обеспечения гибкой расширяемости, в то время как соответствующее действие для допустимого выражения действительно использует шаблон диспетчеризации. И, наконец, что не менее важно, для проверки каждого правила используется шаблон интерпретатора.
Выражение, подобное представленному в вашем примере выше, состоит из операций, переменных и значений. Что касается примера вики, все, что может быть объявлено, является Expression
. Таким образом, интерфейс выглядит так:
import java.util.Map;
public interface Expression
{
public boolean interpret(final Map<String, ?> bindings);
}
Хотя пример на вики-странице возвращает int (они реализуют калькулятор), нам нужно только логическое возвращаемое значение здесь, чтобы решить, должно ли выражение запускать действие, если выражение оценивается как true
.
Выражение может, как указано выше, быть операцией типа =
, AND
, NOT
, ... или Variable
или его Value
. Определение 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
содержит фабричный метод для генерации конкретных типов значений для определенного типа Java.
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
, переменной присваивается значение, при этом переменная находится слева от символа =
, а значение - справа.
Кроме того, интерпретация проверяет наличие имени переменной в привязках переменных. Если он недоступен, мы знаем, что этот термин не может быть оценен как истина, поэтому мы можем пропустить процесс оценки. Если он присутствует, мы извлекаем информацию из правой части (= часть значения) и сначала проверяем, равен ли тип класса, и если да, соответствует ли фактическое значение переменной привязке.
Поскольку фактический синтаксический анализ выражений преобразован в операции, фактический синтаксический анализатор довольно тонкий:
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();
}
}
Помимо одноэлементного шаблона enum, здесь нет ничего особенного.
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()
, который обработает действие, которое должно быть выполнено при успешной проверке. Шаблон NULL используется здесь, чтобы избежать работы с нулевыми значениями в случае, если выполнение действий не требуется, поскольку должна выполняться только проверка 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);
на каждое определенное правило.
Я не включаю другие операции, так как публикация здесь уже слишком длинная, но не должно быть слишком сложно реализовать их самостоятельно, если вы того пожелаете. Кроме того, я не включил свою структуру пакета, так как вы, вероятно, будете использовать свою собственную. Более того, я не включил никакой обработки исключений, я оставляю это на усмотрение всех, кто собирается скопировать и вставить код :)
Кто-то может возразить, что разбор, очевидно, должен происходить в парсере, а не в конкретных классах. Я знаю об этом, но, с другой стороны, при добавлении новых операций вам нужно изменить синтаксический анализатор, а также новую операцию вместо того, чтобы касаться только одного отдельного класса.
Вместо использования системы на основе правил сеть Петри или даже BPMN в сочетании с открытым исходным кодом < href = "http://activiti.org/" rel = "noreferrer"> Activiti Engine может выполнить эту задачу. Здесь операции уже определены в языке, вам нужно только определить конкретные операторы как задачи, которые могут выполняться автоматически - и в зависимости от результата задачи (то есть отдельного оператора) он продолжит свой путь через «граф» . Поэтому моделирование обычно выполняется в графическом редакторе или во внешнем интерфейсе, чтобы не иметь дело с XML-природой языка BPMN.
person
Roman Vottner
schedule
02.01.2014