понедельник, 2 декабря 2024 г.

Надоели многочисленные if , условные операции и switch? Тогда мы идём к вам!

Приветствую, уважаемый читатель моего блога! Как-то давно меня не было в уличных гонках не выпускал новые статьи. На то были разные причины личного характера, но мы сегодня не об этом. 

Уверен, что каждый уважающий себя разработчик за свою карьеру писал выражения ветвления, которые не ограничиваются 1-2 условиями, а содержат многочисленные условия, проверки каких-либо выражений. Зачастую такие участки кода разрастаются ещё больше, обрастают дополнительной бизнес-логикой. Чем могут причинить не меньшую боль, чем известное растение-сорняк (фото ниже):


Существует множество паттернов проектирования, которые позволят минимизировать количество таких выражений. В зависимости от ситуации, можно использовать как поведенческие паттерны "Стратегия", "Состояние", да даже структурный "Декоратор" иногда может подойти. Но сегодня хотел бы рассмотреть с вами ещё один поведенческий вариант - "Цепочка обязанностей".

Начнём с определения. Паттерн "Цепочка обязанностей" позволяет создавать цепочку объектов для обработки запроса или выполнения задачи. Как и многие другие поведенческие паттерны, "Цепочка обязанностей" основывается на том, чтобы сформировать отдельные поведения в объекты-обработчики. Таким образом паттерн подразумевает связывание объектов-обработчиков в одну цепь.

Назначение: данный паттерн позволяет избежать привязки отправителя запроса к его получателю с целью обработки запроса несколькими объектами. Т.е. происходит связывание объектов-получателей цепочку и передача запроса вдоль этой цепочки. Отправитель запроса знает только о первом обработчике, но не знает о конкретном обработчике, который в итоге и обработает запрос.

Схема паттерна выглядит следующим образом (прилагается ниже):


где Client - отправляет запрос объекту Handler, Handler - определяет интерфейс для обработки запроса. Также может определять ссылку на следующий обработчик запроса, а ConcreteHandler1 и ConcreteHandler2 - конкретные обработчики, которые реализуют функционал обработки запроса.

Рассмотрим реализацию паттерна в .NET на практическом примере.

Допустим, у нас есть ряд набор скидок для приложения магазина. В зависимости от покупателя мы можем указать различные проценты скидок к его заказам:

  • VIP - 20%,
  • постоянный клиент - 10%,
  • новый клиент - 5%.
  • иначе не применять скидку.
Конечно же мы изначально могли бы написать такой код (и скорее всего написали бы) с помощью цепочки операторов if (можно и switch expression):

public decimal CalculateTotalOrderSummary(Customer customer, decimal price)
{
  if (customer.IsVIP)
    return price * 0.8m;
  
  if (customer.IsRegular)
    return price * 0.9m;

  if (customer.IsNew)
    return price * 0.95m;

  return price;
}

Но ведь такой подход может стать громоздким, когда количество правил разрастётся и правила усложняются. Предлагаю внедрить новый паттерн с целью рефакторинга участка кода.

1. Создадим абстрактный класс обработчика DiscountHandler, который определяет общий интерфейс для всех обработчиков скидок:

public abstract class DiscountHandler
{
  protected DiscountHandler _next;

  public DiscountHandler SetNextHandler(DiscountHandler next)
  {
    _next = next;
    return next;
  }

  public abstract decimal CalculateTotalOrderSummary(Customer customer, decimal price);
}

2. Теперь пора реализовать обработчики для каждого типа скидки, т.е. производные классы от DiscountHandler. Каждый обработчик будет отвечать за определённое правило и решать, применять ли скидку или передавать запрос следующему обработчику:

public sealed class VIPDiscountHandler : DiscountHandler
{
  public override decimal CalculateTotalOrderSummary(Customer customer, decimal price)
  {
    if (customer.IsVIP)
      return price * 0.8m;

    return _next?.CalculateDiscount(customer, price) ?? price;
  }
}

По аналогии можно реализовать и остальные, но оставлю это за рамками статьи.

3. Создадим цепочку связанных обработчиков:

var discountHandler = new VIPDiscountHandler();

discountHandler
.SetNextHandler(new RegularDiscountHandler()) .SetNextHandler(new NewCustomerDiscountHandler()) .SetNextHandler(new NoDiscountHandler());

И вот теперь можно вызвать цепочку, обратившись к методу CalculateTotalOrderSummary первого обработчика:

decimal totalOrder = discountHandler
 .CalculateTotalOrderSummary(customer, price);

Самое время разобраться в преимуществах и недостатках нового подхода.

Плюсы паттерна, в чём-то совпадают с принципами SOLID:

1. Гибкость
Позволяет динамически изменять или расширять цепочку (добавлять или удалять обработчики), не затрагивая другие части кода.

2. Слабая связанность
Каждому обработчику нужно знать только о своем непосредственном преемнике, что минимизирует зависимости.

3. Единая ответственность
Отделяет классы, которые вызывают операции, от классов, которые выполняют операции.

Минусы подхода:

1. Запрос может остаться необработанным
Если ни один из обработчиков в цепочке не может обработать запрос, он может остаться необработанным, что приведет к неожиданному поведению. Важно иметь обработчик по умолчанию или способ обработки таких сценариев.

2. Потенциальное влияние на производительность
Если цепочка становится очень длинной, это может привести к накладным расходам производительности из-за обхода нескольких обработчиков. И стоит учитывать, что добавляются объекты для последующей сборки мусора (но это применимо к большинству паттернов).

При применении этого паттерна в реальных сценариях важно соблюдать баланс между количеством обработчиков и соображениями производительности. Поэтому если нужен максимальный хайлод, где важна каждая миллисекунда и существуют жёсткие лимиты по таймаутам исполнения запросов, то стоит подумать о применении разобранного выше паттерна.

Источники:
- Тепляков С. "Паттерны проектирования на платформе .NET." — СПб.: Питер, 2015. Глава 7.
https://refactoring.guru/ru/design-patterns/chain-of-responsibility
https://thecodeman.net/posts/chain-responsibility-pattern