Chain of Responsibility (Цепочка обязанностей)
Chain of Responsibility связывает обработчики в цепочку и передаёт запрос по ней, пока один из обработчиков его не обработает.
Иногда возникает ситуация, когда запрос могут обработать несколько хэндлеров, и нужный хэндлер выбирается в зависимости от контекста. Под запросом здесь имеется в виду не только HTTP запрос, но и любая логика, которая может быть выражена в виде команды.
Вместо того, чтобы заставлять клиентский код знать обо всех возможных хэндлерах и решать, какой из них вызвать, мы можем связать хэндлеры в цепочку и выбирать нужный хэндлер внутри этой цепочки. В этом случае клиентский код будет зависеть всего лишь от одного хэндлера – начального звена цепочки.
Пример
Представим, что у нас есть приложение, которое моделирует работу технической поддержки.
У нас есть класс тикета. Тикет – это обращение пользователя, у которого есть номер, текст, описывающий проблему, и тип:
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();
}javapackage techsupport;
import java.util.Optional;
public interface L2SupportEngineerRepository {
Optional<SupportEngineer> findAvailable();
}javapackage techsupport;
public class SupportEngineer {
public void assignTicket(SupportTicket ticket) {
// Назначение тикета на сотрудника
}
}javapackage 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