Паттерн «Интерпретатор» в .NET. Расширенное использование
Начало
1. Создание правил из конфигурации
Парсим правила из JSON или БД:
public class RuleEngine
{
public IExpression BuildFromConfig(
RuleDefinition def) => def.Type switch
{
"OrderTotalGreaterThan" =>
new OrderTotalGreaterThan(
def.GetParam<decimal>("threshold")),
"CustomerIsVip" =>
new CustomerIsVip(),
"OrderContainsCategory" =>
new OrderContainsCategory(
def.GetParam<string>("category")),
"AND" =>
new AndExpression(
BuildFromConfig(def.Left!),
BuildFromConfig(def.Right!)),
"OR" =>
new OrExpression(
BuildFromConfig(def.Left!),
BuildFromConfig(def.Right!)),
"NOT" =>
new NotExpression(BuildFromConfig(def.Left!)),
_ => throw new InvalidOperationException(
$"Неизвестный тип: {def.Type}")
};
}
Определение правила в JSON:
{
"type": "AND",
"left": { "type": "CustomerIsVip" },
"right": { "type": "OrderTotalGreaterThan", "params": { "threshold": 200 } }
}Теперь отдел маркетинга может определять правила в UI панели управления. RuleEngine анализирует и оценивает их без изменения кода.
2. Парсинг выражений для вычисляемых полей
Аналогично можно создавать математические выражения:
public interface IMathExpression
{
decimal Evaluate(Dictionary<string, decimal> variables);
}
public class NumberLiteral(decimal value)
: IMathExpression
{
public decimal Evaluate(Dictionary<string, decimal> variables)
=> value;
}
public class Variable(string name)
: IMathExpression
{
public decimal Evaluate(Dictionary<string, decimal> variables)
=> variables[name];
}
public class Multiply(IMathExpression left, IMathExpression right)
: IMathExpression
{
public decimal Evaluate(Dictionary<string, decimal> variables)
=> left.Evaluate(variables) * right.Evaluate(variables);
}
// Выражение: price * quantity * (1 - discount)
var totalExpr = new Multiply(
new Multiply(new Variable("price"), new Variable("quantity")),
new Subtract(new NumberLiteral(1), new Variable("discountRate")));
var vars = new Dictionary<string, decimal>
{
["price"] = 29.99m, ["quantity"] = 3, ["discount"] = 0.10m
};
var total = totalExpr.Evaluate(vars); // 80.973
Когда не использовать
1. Для сложных правил. Если нужны циклы, функции или сложный синтаксис, используйте подходящий генератор парсеров (ANTLR) или существующий скриптовый движок (Roslyn, Lua). Паттерн «Интерпретатор» плохо масштабируется для сложных языков.
2. Важна производительность. Каждый вызов Interpret() проходит по дереву выражений. Для часто используемых путей с миллионами вычислений скомпилированные выражения (Expression<T>.Compile()) на порядки быстрее.
3. Правила редко меняются. Если правила стабильны и меняются раз в год, затраты на создание интерпретатора не оправданы. Просто пишите условия напрямую.
«Интерпретатор» избыточен?
Для 3-4 фиксированных условий — да. Просто используйте if-else. Паттерн особенно эффективен, когда у вас более 20 правил, которые регулярно меняются и позволяют создавать комбинации без перекомпиляции кода.
Альтернативы
Для оценки во время выполнения деревья выражений C# Expression<T> компилируются в IL. Для простой оценки правил может подойти словарь делегатов или паттерн «Стратегия».
Источник: https://thecodeman.net/posts/interpreter-pattern-in-dotnet