Паттерн «Интерпретатор» в .NET. Начало
Когда бизнес-правила похоронены в цепочках if-else.
Система скидок начиналась с простого: 10% на заказы свыше $100. Одно условие, одно правило. Затем отдел маркетинга захотел: "20% для VIP-клиентов И заказом свыше $200." Затем: "Бесплатную доставку товаров категории "Электроника" ИЛИ за заказ свыше $500." И т.д., и т.п.
Теперь метод предоставления скидок — это 150 строк вложенных условных операторов, к которым никто не хочет прикасаться:
public decimal CalculateDiscount(
Order order, Customer customer)
{
if (customer.IsVip && order.Total > 200)
return order.Total * 0.20m;
else if (order.Total > 100)
return order.Total * 0.10m;
else if (order.Items.Any(i => i.Category == "Electronics") || order.Total > 500)
order.Shipping = 0;
// … ещё 20 условий
return order.Total;
}
Каждое новое правило требует модификации кода, повторного развёртывания и тестирования всей цепочки. Маркетинг не может обновлять правила без участия разработчиков. А логика совершенно непрозрачна — никто не может прочитать её и понять весь набор правил.
Проблема в том, что эти правила представляют собой бизнес-логику, которая часто меняется, но они заблокированы внутри скомпилированного кода. Нужен способ представить правила в виде данных — чего-то, что можно анализировать, компоновать и динамически оценивать.
Паттерн «Интерпретатор»
Паттерн «Интерпретатор» определяет грамматику для языка и предоставляет интерпретатор, который оценивает выражения на этом языке. Каждое правило в грамматике - класс. Сложные правила компонуются из простых.
Определим интерфейс выражений и атомарные выражения:
// Контекст - данные, которые будут оцениваться в выражениях
public class RuleContext(Order order, Customer customer)
{
public Order Order { get; } = order;
public Customer Customer { get; } = customer;
}
// Абстрактное выражение
public interface IExpression
{
bool Interpret(RuleContext ctx);
}
// Окончательные выражения: атомарные условия
public class OrderTotalGreaterThan(decimal threshold)
: IExpression
{
public bool Interpret(RuleContext ctx)
=> context.Order.Total > threshold;
}
public class CustomerIsVip : IExpression
{
public bool Interpret(RuleContext ctx)
=> context.Customer.IsVip;
}
public class OrderContainsCategory(string category)
: IExpression
{
public bool Interpret(RuleContext ctx)
=> ctx.Order.Items.Any(i =>
i.Category.Equals(category,
StringComparison.OrdinalIgnoreCase));
}
// …
Теперь нетерминальные выражения – логические действия:
// И
public class AndExpression(IExpression left, IExpression right)
: IExpression
{
public bool Interpret(RuleContext ctx)
=> left.Interpret(ctx) && right.Interpret(ctx);
}
// ИЛИ
public class OrExpression(IExpression left, IExpression right)
: IExpression
{
public bool Interpret(RuleContext ctx)
=> left.Interpret(ctx) || right.Interpret(ctx);
}
// НЕ
public class NotExpression(IExpression expression)
: IExpression
{
public bool Interpret(RuleContext ctx)
=> !expression.Interpret(ctx);
}
Составляем правила из выражений:
// VIP-клиент и заказ > $200
var vipHighValueRule = new AndExpression(
new CustomerIsVip(),
new OrderTotalGreaterThan(200));
var ctx = new RuleContext(order, customer);
if (vipHighValueRule.Interpret(ctx))
discount = 0.20m;
Правила — это компонуемые структуры данных. Вы можете хранить их, сериализовать и создавать из конфигурации.
Почему это лучше
1. Правила — это данные, а не код. Вы можете хранить определения правил в БД и создавать деревья выражений во время выполнения. Для новых правил не требуется повторное развёртывание.
2. Компонуемость. Операторы И, ИЛИ и НЕ объединяют любые выражения. Сложные правила строятся из простых, протестированных компонентов.
3. Тестируемость. Каждое выражение представляет собой отдельный класс с одним методом. Тестируйте OrderTotalGreaterThan независимо от всего остального.
Окончание следует…
Источник: https://thecodeman.net/posts/interpreter-pattern-in-dotnet