Перейти к содержанию

Chain of Responsibility (Цепочка обязанностей)

Категория: паттерн поведения.

Проблема

Запрос может быть обработан одним из нескольких обработчиков, но заранее неизвестно, каким именно - это зависит от свойств самого запроса (уровень серьёзности заявки в поддержку, тип исключения, права доступа). Если весь выбор обработчика зашить в один метод с большим if/switch, этот метод придётся менять при добавлении каждого нового вида обработки, а сам список обработчиков будет сложно переиспользовать или переупорядочить.

Решение

  1. Обработчики выстраиваются в цепочку: каждый хранит ссылку на следующий обработчик.
  2. Получив запрос, обработчик решает: может ли он сам его обработать. Если да - обрабатывает (и обычно на этом цепочка останавливается). Если нет - передаёт запрос дальше по цепочке.
  3. Отправитель запроса обращается только к первому обработчику в цепочке и не знает, сколько их всего и кто из них в итоге обработает запрос.
  4. Цепочку можно собирать и переупорядочивать динамически, не меняя код самих обработчиков.

Структура

  • Handler - интерфейс/базовый класс со ссылкой на следующий обработчик и методом обработки запроса.
  • ConcreteHandler - решает, может ли обработать запрос; если нет - передаёт его следующему звену.
  • Client - формирует цепочку и отправляет запрос первому обработчику.

Когда применять

  • Больше одного объекта может обработать запрос, и обработчик должен определяться динамически по свойствам запроса, а не жёстко назначаться заранее.
  • Нужно отправить запрос одному из нескольких обработчиков, явно не указывая получателя.
  • Набор обработчиков и их порядок должны настраиваться независимо от кода, который инициирует запрос (например, конфигурируемый порядок middleware, валидаторов, обработчиков исключений).

Плюсы

  • Уменьшает связанность между отправителем запроса и конкретными обработчиками - отправитель знает только про первое звено цепочки.
  • Позволяет добавлять, убирать и переупорядочивать обработчики без изменения существующего кода (открытость/закрытость).
  • Каждый обработчик отвечает за одно узкое условие - удобно тестировать по отдельности.

Минусы

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

Отличие от Decorator

Структурно оба паттерна похожи - каждое звено хранит ссылку на следующее и решает, что делать с вызовом. Но назначение разное: Chain of Responsibility передаёт запрос дальше только если текущий обработчик не может (или не должен) обработать его сам, и как правило только один обработчик в итоге реально что-то делает. Decorator всегда вызывает следующий объект в цепочке и лишь добавляет к его результату своё поведение - выполняются все звенья цепочки, а не одно из них.

Пример в .NET Framework / BCL

  • ASP.NET Core middleware pipeline - каждый middleware решает, обработать ли запрос самому (вернуть ответ) или передать его дальше по конвейеру через next().
  • Обработка исключений через вложенные try/catch разных уровней приложения - концептуально похожая идея передачи "необработанного" случая на следующий уровень.
  • Валидация в виде цепочки правил, где каждое правило проверяет свой аспект и передаёт объект дальше, если само не нашло нарушений.

Пример реализации на C#

ChainOfResponsibility.cs
using System;

namespace DesignPatterns.Behavioral.ChainOfResponsibility
{
    public sealed class SupportTicket
    {
        public string Description { get; }
        public int SeverityLevel { get; } // 1 - низкий, 3 - критический

        public SupportTicket(string description, int severityLevel)
        {
            Description = description;
            SeverityLevel = severityLevel;
        }
    }

    // Общий интерфейс обработчика - и сам обрабатывает запрос, и хранит ссылку
    // на следующего обработчика в цепочке.
    public abstract class SupportHandler
    {
        private SupportHandler? _next;

        // Возвращаем handler, чтобы можно было строить цепочку одной цепочкой вызовов
        public SupportHandler SetNext(SupportHandler next)
        {
            _next = next;
            return next;
        }

        public void Handle(SupportTicket ticket)
        {
            if (CanHandle(ticket))
            {
                Process(ticket);
                return;
            }

            if (_next is not null)
            {
                _next.Handle(ticket);
            }
            else
            {
                Console.WriteLine($"Тикет «{ticket.Description}» никто не смог обработать");
            }
        }

        protected abstract bool CanHandle(SupportTicket ticket);
        protected abstract void Process(SupportTicket ticket);
    }

    public sealed class FirstLineSupport : SupportHandler
    {
        protected override bool CanHandle(SupportTicket ticket) => ticket.SeverityLevel == 1;

        protected override void Process(SupportTicket ticket) =>
            Console.WriteLine($"[Первая линия] Обработан: {ticket.Description}");
    }

    public sealed class SecondLineSupport : SupportHandler
    {
        protected override bool CanHandle(SupportTicket ticket) => ticket.SeverityLevel == 2;

        protected override void Process(SupportTicket ticket) =>
            Console.WriteLine($"[Вторая линия] Обработан: {ticket.Description}");
    }

    public sealed class EngineeringTeam : SupportHandler
    {
        protected override bool CanHandle(SupportTicket ticket) => ticket.SeverityLevel >= 3;

        protected override void Process(SupportTicket ticket) =>
            Console.WriteLine($"[Инженеры] Обработан критический тикет: {ticket.Description}");
    }

    public static class Demo
    {
        public static void Run()
        {
            var firstLine = new FirstLineSupport();
            var secondLine = new SecondLineSupport();
            var engineering = new EngineeringTeam();

            // Строим цепочку: первая линия -> вторая линия -> инженеры
            firstLine.SetNext(secondLine).SetNext(engineering);

            firstLine.Handle(new SupportTicket("Не приходит письмо", severityLevel: 1));
            firstLine.Handle(new SupportTicket("Ошибка в отчёте", severityLevel: 2));
            firstLine.Handle(new SupportTicket("Сервис недоступен", severityLevel: 3));
        }
    }
}

Открыть ChainOfResponsibility.cs отдельно Скачать ChainOfResponsibility.cs