BackendBit

К списку паттернов

Chain of Responsibility связывает обработчики в цепочку и передаёт запрос по ней, пока один из обработчиков его не обработает.

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

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

UML диаграмма классов паттерна Цепочка обязанностейПоказывает интерфейс TechSupportHandler с методами handleTicket и setNext, абстрактный класс AbstractTechSupportHandler с полем nextHandler, и три конкретных обработчика: L1TechSupportHandler, L2TechSupportHandler и DevelopersTeam.<<interface>>TechSupportHandler+handleTicket(SupportTicket): void+setNext(TechSupportHandler): TechSupportHandler<<abstract>>AbstractTechSupportHandler+nextHandler: TechSupportHandlerL2TechSupportHandlerL1TechSupportHandlerDevelopersTeamХранит ссылку на следующийобработчик в цепочке

Пример

Представим, что у нас есть приложение, которое моделирует работу технической поддержки.

У нас есть класс тикета. Тикет – это обращение пользователя, у которого есть номер, текст, описывающий проблему, и тип:

package techsupport;

public record SupportTicket(String number, String text, TicketType type) {}
java

У тикета есть три типа:

package techsupport;

public enum TicketType {
    /**
     * Тикет, который впервые попал в систему и ещё не был классифицирован.
     */
    UNCLASSIFIED,

    /**
     * Тикет, который прошёл первичную обработку и нуждается в более глубокой аналитике.
     */
    REQUIRES_ANALYSIS,

    /**
     * Тикет, который нуждается в привлечении технических экспертов (разработчиков).
     */
    REQUIRES_TECHNICAL_EXPERTISE
}
java

Также у нас есть три линии технической поддержки:

  • L1. Сотрудники этой линии обрабатывают запросы, которые впервые поступают в систему.
  • L2. Если сотрудники L1 не смогли решить проблему, тикет передаётся на вторую линию. Сотрудники этой линии отличаются от L1 более глубокой экспертизой по системам компании.
  • Команда разработчиков. Если проблему из тикета не смогли разрешить предыдущие линии поддержки, тикет должен попасть к разработчикам.

Исходя из описания работы технической поддержки мы можем построить цепочку обработки тикета:

L1 → L2 → Команда разработчиков
text

Попробуем смоделировать такую цепочку в коде. Для начала создадим интерфейс обработчика обращений в поддержку:

package techsupport;

public interface TechSupportHandler {
    void handleTicket(SupportTicket ticket);
    TechSupportHandler setNext(TechSupportHandler handler);
}
java

У обработчика два метода:

  • handleTicket(SupportTicket ticket) – в этом методе хэндлер проверяет, может ли он обработать тикет, и при положительном ответе обрабатывает его.
  • setNext(TechSupportHandler handler) – с помощью этого метода мы конструируем цепочку. Метод устанавливает следующий хэндлер, который будет использоваться, если текущий хэндлер не смог обработать тикет. Метод возвращает переданный хэндлер, чтобы можно было выстроить цепочку одним выражением: l1.setNext(l2).setNext(l3).

Для удобства сделаем абстрактный хэндлер. В нём будет реализация метода setNext, чтобы не дублировать код в каждом хэндлере. Также там будет дефолтная реализация handleTicket, которая передаёт тикет следующему хэндлеру (если он есть) или бросает исключение, если цепочка закончилась, а тикет так и не был обработан. Этот метод будет вызываться конкретным хэндлером в случае, если он не может обработать тикет сам:

package techsupport;

public abstract class AbstractTechSupportHandler implements TechSupportHandler {
    private TechSupportHandler nextHandler;

    @Override
    public TechSupportHandler setNext(TechSupportHandler handler) {
        this.nextHandler = handler;
        return handler;
    }

    @Override
    public void handleTicket(SupportTicket ticket) {
        if (nextHandler != null) {
            nextHandler.handleTicket(ticket);
        } else {
            throw new IllegalStateException(
                    "Ticket %s was not handled by any handler in the chain"
                            .formatted(ticket.number())
            );
        }
    }
}
java

Теперь напишем реализации хэндлеров. Для них нам понадобятся вспомогательные классы – репозитории сотрудников, класс сотрудника и бэклог:

package techsupport;

import java.util.Optional;

public interface L1SupportEngineerRepository {
    Optional<SupportEngineer> findAvailable();
}
java
package techsupport;

import java.util.Optional;

public interface L2SupportEngineerRepository {
    Optional<SupportEngineer> findAvailable();
}
java
package techsupport;

public class SupportEngineer {
    public void assignTicket(SupportTicket ticket) {
        // Назначение тикета на сотрудника
    }
}
java
package techsupport;

public class Backlog {
    public void addTicket(SupportTicket ticket) {
        // Добавление тикета в бэклог команды
    }
}
java

Хэндлер первой линии обрабатывает новые, ещё не классифицированные тикеты:

package techsupport;

public final class L1TechSupportHandler extends AbstractTechSupportHandler {
    private final L1SupportEngineerRepository supportEngineerRepository;

    public L1TechSupportHandler(L1SupportEngineerRepository supportEngineerRepository) {
        this.supportEngineerRepository = supportEngineerRepository;
    }

    @Override
    public void handleTicket(SupportTicket ticket) {
        // Если тикет новый и его пока никак не классифицировали
        if (ticket.type() == TicketType.UNCLASSIFIED) {
            // Выбираем свободного сотрудника L1 и назначаем тикет.
            // Если свободных нет – передаём тикет дальше по цепочке
            supportEngineerRepository.findAvailable().ifPresentOrElse(
                    engineer -> engineer.assignTicket(ticket),
                    () -> super.handleTicket(ticket)
            );
        } else {
            // Если тикет не новый – отправляем его дальше по цепочке
            super.handleTicket(ticket);
        }
    }
}
java

Хэндлер второй линии обрабатывает тикеты, которые нуждаются в более глубокой аналитике:

package techsupport;

public final class L2TechSupportHandler extends AbstractTechSupportHandler {
    private final L2SupportEngineerRepository supportEngineerRepository;

    public L2TechSupportHandler(L2SupportEngineerRepository supportEngineerRepository) {
        this.supportEngineerRepository = supportEngineerRepository;
    }

    @Override
    public void handleTicket(SupportTicket ticket) {
        // Если тикет нуждается в более глубокой аналитике
        if (ticket.type() == TicketType.REQUIRES_ANALYSIS) {
            // Выбираем свободного сотрудника L2 и назначаем тикет.
            // Если свободных нет – передаём тикет дальше по цепочке
            supportEngineerRepository.findAvailable().ifPresentOrElse(
                    engineer -> engineer.assignTicket(ticket),
                    () -> super.handleTicket(ticket)
            );
        } else {
            // Если L2 не может обработать тикет – отправляем дальше по цепочке
            super.handleTicket(ticket);
        }
    }
}
java

Команда разработчиков обрабатывает тикеты, которые требуют привлечения технических экспертов:

package techsupport;

public final class DevelopersTeam extends AbstractTechSupportHandler {
    private final Backlog backlog;

    public DevelopersTeam(Backlog backlog) {
        this.backlog = backlog;
    }

    @Override
    public void handleTicket(SupportTicket ticket) {
        // Если тикет требует привлечения технических экспертов
        if (ticket.type() == TicketType.REQUIRES_TECHNICAL_EXPERTISE) {
            // Добавляем тикет в бэклог команды разработчиков
            backlog.addTicket(ticket);
        } else {
            // Если команда разработки не может обработать тикет – передаём дальше
            super.handleTicket(ticket);
        }
    }
}
java

Имея все хэндлеры на руках, мы можем составить цепочку:

TechSupportHandler l1 = new L1TechSupportHandler(l1Repository);
TechSupportHandler l2 = new L2TechSupportHandler(l2Repository);
TechSupportHandler devTeam = new DevelopersTeam(backlog);

l1.setNext(l2).setNext(devTeam);
java

Клиентскому коду достаточно обратиться к первому звену цепочки, чтобы тикет прошёл по всей цепочке:

l1.handleTicket(ticket);
java

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

Реальные примеры

jakarta.servlet.Filter

Спецификация Jakarta Servlet предоставляет механизм фильтров. Фильтры – это цепочка из объектов, через которую прогоняется HTTP запрос перед тем, как он дойдёт до сервлета.

У класса фильтра должен быть метод doFilter. Первые два параметра – запрос и ответ, а третий – FilterChain, через который фильтр передаёт запрос следующему звену в цепочке:

package real;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

public class AuthenticationFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        var httpRequest = (HttpServletRequest) request;

        if (httpRequest.getHeader("Authorization") == null) {
            // Если токена нет – отклоняем запрос, не передавая его дальше
            var httpResponse = (HttpServletResponse) response;
            httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        // Передаём запрос следующему фильтру в цепочке
        chain.doFilter(request, response);
    }
}
java

Каждый фильтр решает, обработать запрос самостоятельно или передать его дальше – тот же принцип, что и в нашем примере с технической поддержкой. Отличие в том, что следующий хэндлер назначается не через метод setNext, а передаётся параметром FilterChain в метод doFilter. Сама цепочка конфигурируется извне – в web.xml или через аннотации.

java.util.logging.Logger

Стандартная библиотека Java содержит встроенную цепочку обязанностей в системе логгирования. Каждый Logger имеет родительский логгер, и записи о событиях (LogRecord) распространяются вверх по иерархии:

// Родительский логгер – реагирует только на WARNING и выше
Logger appLogger = Logger.getLogger("com.example.app");
appLogger.setUseParentHandlers(false);
Handler appHandler = new ConsoleHandler();
appHandler.setLevel(Level.WARNING);
appLogger.addHandler(appHandler);

// Дочерний логгер – выводит всё начиная с INFO
Logger orderLogger = Logger.getLogger("com.example.app.orders");
Handler orderHandler = new ConsoleHandler();
orderHandler.setLevel(Level.ALL);
orderLogger.addHandler(orderHandler);

// INFO-запись пройдёт по цепочке: orderLogger → appLogger.
// Обработчик orderLogger выведет её (порог ALL),
// обработчик appLogger – нет (порог WARNING)
orderLogger.info("Заказ #42 принят");

// WARNING-запись тоже пройдёт по всей цепочке,
// но теперь оба обработчика выведут запись
orderLogger.warning("Заказ #42: товар заканчивается на складе");
java

Иерархия логгеров образуется через имена: com.example.app.orders – дочерний для com.example.app. При вызове orderLogger.info(...) запись сначала попадает в обработчики orderLogger, а затем поднимается к appLogger. Каждый обработчик (Handler) самостоятельно решает, обрабатывать ли запись, исходя из своего уровня.

HTTP middleware

В Go паттерн Chain of Responsibility реализуется через middleware – функции, которые принимают http.Handler и возвращают http.Handler. Каждая middleware-функция решает, обработать запрос самостоятельно или передать его дальше:

package main

import (
	"net/http"
)

// withAuth – middleware: проверяет авторизацию перед передачей запроса дальше
func withAuth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("Authorization") == "" {
			// Если токена нет – обрабатываем запрос сами, не передавая дальше
			http.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}

		// Передаём запрос следующему обработчику в цепочке
		next.ServeHTTP(w, r)
	})
}

func main() {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("ok"))
	})

	http.Handle("/api", withAuth(handler))
}
go