Wzorzec Chain of Responsibility (łańcuch odpowiedzialności) przekazuje żądanie przez kolejnych obsługujących. Każdy z nich decyduje, czy żądanie przetworzyć, czy podać je dalej. Sięgasz po ten wzorzec, gdy nie wiesz z góry, który obiekt powinien obsłużyć żądanie, albo gdy chcesz dać tę szansę kilku obiektom.
Problem
Musisz wysłać żądanie, ale nie wiesz, który obiekt powinien je obsłużyć. W tradycyjnym kodzie zapisujesz odbiorcę na stałe albo budujesz długi łańcuch if-else, który ustala, kto ma się tym zająć. Oba rozwiązania są sztywne. Dodanie nowego obsługującego wymaga zmiany istniejącego kodu i łamie zasadę Open/Closed.
Chain of Responsibility rozdziela nadawcę od odbiorcy. Żądanie wędruje przez łańcuch obiektów, dopóki któryś go nie obsłuży.
Struktura
Rola uczestników
| Rola | Odpowiedzialność |
|---|---|
| Handler (Obsługujący) | Interfejs z metodą obsługi żądania i opcjonalnym przekazaniem go do następnego obsługującego |
| ConcreteHandler (Konkretny obsługujący) | Implementuje logikę obsługi. Decyduje, czy przetworzyć żądanie, czy przekazać je dalej |
| Client (Klient) | Wysyła żądanie do łańcucha. Nie wie i nie musi wiedzieć, kto je obsłuży |
Prosta wizualizacja łańcucha
Żądanie płynie przez łańcuch od jednego obsługującego do następnego, aż ktoś je przetworzy:
Szczegółowy diagram przepływu
Diagram pokazuje logikę decyzyjną każdego obsługującego:
Klasyczna implementacja
W klasycznym podejściu każdy konkretny obsługujący trzyma referencję do następnego, a klient łączy je w łańcuch.
Kod (C++)
#include <iostream>
#include <memory>
#include <string>
// ── Klasa żądania ────────────────────────────────────────
class Request {
private:
std::string type;
int priority;
public:
Request(const std::string& t, int p) : type(t), priority(p) {}
std::string getType() const { return type; }
int getPriority() const { return priority; }
};
// ── Interfejs Handler ────────────────────────────────────
class Handler {
protected:
std::shared_ptr<Handler> nextHandler;
public:
virtual ~Handler() = default;
void setNext(std::shared_ptr<Handler> handler) {
nextHandler = handler;
}
virtual void handle(const Request& request) = 0;
};
// ── Konkretni obsługujący ────────────────────────────────
class SupportHandler : public Handler {
public:
void handle(const Request& request) override {
if (request.getType() == "technical") {
std::cout << "Zespół wsparcia obsługuje problem techniczny\n";
} else if (nextHandler) {
nextHandler->handle(request);
} else {
std::cout << "Brak obsługującego dla: " << request.getType() << "\n";
}
}
};
class ManagerHandler : public Handler {
public:
void handle(const Request& request) override {
if (request.getType() == "complaint") {
std::cout << "Menedżer obsługuje skargę\n";
} else if (nextHandler) {
nextHandler->handle(request);
} else {
std::cout << "Brak obsługującego dla: " << request.getType() << "\n";
}
}
};
class DirectorHandler : public Handler {
public:
void handle(const Request& request) override {
if (request.getPriority() > 8) {
std::cout << "Dyrektor obsługuje żądanie o wysokim priorytecie\n";
} else if (nextHandler) {
nextHandler->handle(request);
} else {
std::cout << "Brak obsługującego dla: " << request.getType() << "\n";
}
}
};
// ── Klient ──────────────────────────────────────────────
int main() {
auto support = std::make_shared<SupportHandler>();
auto manager = std::make_shared<ManagerHandler>();
auto director = std::make_shared<DirectorHandler>();
// Budujemy łańcuch: support -> manager -> director
support->setNext(manager);
manager->setNext(director);
// Testujemy żądania
Request tech("technical", 5);
Request complaint("complaint", 3);
Request critical("escalation", 9);
std::cout << "=== Obsługiwanie żądań ===\n";
support->handle(tech); // Obsłużone przez SupportHandler
support->handle(complaint); // Przekazane do ManagerHandler
support->handle(critical); // Przekazane do DirectorHandler
return 0;
}
Jak to działa
- Klient wysyła żądanie do pierwszego obsługującego w łańcuchu.
- Każdy obsługujący sprawdza, czy potrafi obsłużyć żądanie:
- Jeśli tak, przetwarza je i może zatrzymać łańcuch.
- Jeśli nie, przekazuje je dalej.
- Ostatni obsługujący obsługuje żądanie albo zgłasza, że nikt nie potrafił tego zrobić.
Budowanie łańcucha
Łańcuch można zbudować na dwa sposoby:
Podejście z jawnym połączeniem
Każdy obsługujący trzyma jawną referencję do następnego, ustawianą przez setNext() albo konstruktor. To podejście z kodu powyżej. Masz pełną kontrolę nad strukturą łańcucha i możesz ją zmieniać w runtime:
// Ręczne budowanie łańcucha
auto support = std::make_shared<SupportHandler>();
auto manager = std::make_shared<ManagerHandler>();
auto director = std::make_shared<DirectorHandler>();
support->setNext(manager);
manager->setNext(director);
// Łańcuch: support → manager → director
Zalety: - Jawna, czytelna struktura - Łatwo zmieniać kolejność obsługujących w runtime - Wspiera wiele niezależnych łańcuchów
Wady:
- Wymaga dodatkowego kodu na wskaźnik nextHandler
- Obsługujący muszą implementować łączenie
Ponowne wykorzystanie istniejących relacji
Zamiast tworzyć jawne łącza, możesz użyć istniejącej hierarchii obiektów, na przykład relacji rodzic-dziecko. W Qt możesz użyć hierarchii widgetów:
// Używanie hierarchii rodzic-dziecko Qt
QWidget* window = new QWidget();
QWidget* panel = new QWidget(window); // panel jest dzieckiem window
QWidget* button = new QPushButton(panel); // button jest dzieckiem panel
// Zdarzenia bąbelkują w górę: button → panel → window
// Bez setNext(): hierarchia jest łańcuchem
bool eventHandler(QEvent* event) {
// Sprawdzenie czy obsługujemy to zdarzenie
if (shouldHandle(event)) {
return true; // Obsłużone
}
// W przeciwnym razie Qt automatycznie przekazuje do rodzica
return QWidget::event(event);
}
Zalety: - Brak dodatkowego kodu do utrzymywania łańcucha - Struktura łańcucha już istnieje, np. hierarchia widgetów albo drzewo - Pasuje do frameworków opartych na hierarchiach
Wady: - Mniej elastyczności. Łańcuch jest związany z hierarchią - Nie każda hierarchia pasuje do semantyki łańcucha
Modele wykonania łańcucha
Łańcuch może być napędzany wewnętrznie albo zewnętrznie:
Łańcuch wewnętrzny (sam się napędza)
Każdy obsługujący sam decyduje, czy przekazać żądanie dalej. Łańcuch napędza się sam:
class Handler {
protected:
std::shared_ptr<Handler> nextHandler;
public:
virtual void handle(const Request& request) = 0;
};
class SupportHandler : public Handler {
public:
void handle(const Request& request) override {
if (canHandle(request)) {
processRequest(request);
// Łańcuch zatrzymuje się tutaj
} else if (nextHandler) {
nextHandler->handle(request); // Łańcuch kontynuuje
}
}
};
Żądanie przechodzi dalej zgodnie z logiką obsługujących. To klasyczne podejście z przykładu powyżej.
Łańcuch zewnętrzny (napędzany z zewnątrz)
Zewnętrzny kod, czyli orkiestracja, przepuszcza żądanie przez obsługujących i decyduje o przepływie:
class Chain {
private:
std::vector<std::shared_ptr<Handler>> handlers;
public:
void addHandler(std::shared_ptr<Handler> h) {
handlers.push_back(h);
}
void process(const Request& request) {
for (auto& handler : handlers) {
if (handler->handle(request)) {
break; // Zatrzymaj się po pierwszym sukcesie obsługującego
}
}
}
};
// Zewnętrzna orkestracja kontroluje przepływ
Chain chain;
chain.addHandler(support);
chain.addHandler(manager);
chain.addHandler(director);
chain.process(request); // Orkestracja napędza łańcuch
Porównanie modeli
| Aspekt | Wewnętrzny | Zewnętrzny |
|---|---|---|
| Przepływ Kontroli | Każdy obsługujący decyduje o następnym kroku | Orkestracja kontroluje przepływ |
| Elastyczność | Trudniej zmienić zachowanie | Łatwo modyfikować logikę łańcucha |
| Sprzężenie | Obsługujący są silnie sprzężeni | Obsługujący są niezależni |
| Widoczność | Ukryty przepływ (trudniej debugować) | Jawny przepływ (łatwiej śledzić) |
Wiele łańcuchów dla różnych zastosowań
Możesz tworzyć łańcuchy z różnymi obsługującymi i różną kolejnością, żeby dopasować zachowanie do sytuacji. Na przykład:
- Obsługa oparta na priorytecie. Osobny łańcuch dla żądań o wysokim priorytecie, osobny dla zwykłych.
- Flagi funkcji. Inny łańcuch dla każdej grupy użytkowników.
- Pipeline przetwarzania. Inny łańcuch dla każdego typu żądania.
Przykład: łańcuchy oparte na priorytecie
// Łańcuch wysokiego priorytetu: dyrektor pierwszy, potem menedżer
Chain highPriority;
highPriority.addHandler(director);
highPriority.addHandler(manager);
highPriority.addHandler(support);
// Łańcuch normalnego priorytetu: wsparcie pierwsze, potem menedżer
Chain normalPriority;
normalPriority.addHandler(support);
normalPriority.addHandler(manager);
normalPriority.addHandler(director);
// Kieruj na podstawie priorytetu
if (request.getPriority() > 7) {
highPriority.process(request);
} else {
normalPriority.process(request);
}
Takie podejście pozwala: - Zmieniać kolejność obsługujących w danym scenariuszu - Tworzyć osobne łańcuchy dla różnych przypadków użycia - Trzymać niezależne łańcuchy i testować je osobno
Warianty
Wzorzec łańcucha loggerów
Częsty wariant, w którym obsługujący przetwarzają żądanie i przekazują je dalej, zamiast się wykluczać:
class Logger {
protected:
std::shared_ptr<Logger> next;
int level; // INFO, WARNING, ERROR
public:
virtual ~Logger() = default;
void setNext(std::shared_ptr<Logger> n) { next = n; }
void log(int logLevel, const std::string& message) {
if (logLevel >= level) {
writeLog(message);
}
if (next) {
next->log(logLevel, message);
}
}
virtual void writeLog(const std::string& message) = 0;
};
class ConsoleLogger : public Logger {
public:
ConsoleLogger() { level = 1; }
void writeLog(const std::string& message) override {
std::cout << "[KONSOLA] " << message << "\n";
}
};
class FileLogger : public Logger {
public:
FileLogger() { level = 2; }
void writeLog(const std::string& message) override {
std::cout << "[PLIK] " << message << "\n";
}
};
Kiedy używać Chain of Responsibility
- Wielu obsługujących. Kilka obiektów może obsłużyć żądanie, a nie wiesz z góry który.
- Dynamiczny łańcuch. Zbiór obsługujących albo ich kolejność może się zmienić w runtime.
- Separacja zagadnień. Rozdzielenie nadawcy od odbiorcy zmniejsza liczbę zależności.
- Pipeline przetwarzania. Obsługujący przetwarzają żądanie po kolei, na przykład logowanie, walidacja i wykonanie.
Przykłady z praktyki
- Obsługa zdarzeń w frameworkach GUI, gdzie kliknięcie myszą bąbelkuje w górę hierarchii komponentów.
- Middleware HTTP w frameworkach webowych, gdzie żądanie przechodzi przez warstwy uwierzytelniania, logowania i kompresji.
- Obieg zatwierdzania żądań, na przykład pracownik → menedżer → dyrektor → szef.
- Obsługa wyjątków w językach, w których wyjątek można przechwycić na kilku poziomach.
- Biblioteki logowania z wieloma appenderami: konsola, plik, baza danych.
Zalety i wady
| Aspekt | Zaleta | Wada |
|---|---|---|
| Rozdzielenie | Nadawca nie musi znać obsługujących | Kolejność ma znaczenie, a debugowanie bywa trudne |
| Elastyczność | Łatwo dodawać i usuwać obsługujących w runtime | Żądanie może nie być obsłużone |
| Pojedyncza odpowiedzialność | Każdy obsługujący robi jedną rzecz | Potrzeba więcej klas dla prostych żądań |
| Open/Closed | Nowych obsługujących dodajesz bez zmian w istniejących | Obsługujący są sprzężeni przez nextHandler |
Podsumowanie
- Chain of Responsibility przekazuje żądanie przez kolejnych obsługujących, dopóki któryś go nie przetworzy.
- Każdy obsługujący albo obsługuje i zatrzymuje łańcuch, albo przekazuje żądanie dalej.
- Sprawdza się przy rozdzielaniu nadawcy od odbiorcy i dynamicznych łańcuchach, ale utrudnia debugowanie, bo przepływ jest niejawny.
- Częsty w middleware, systemach zdarzeń i obiegach zatwierdzania.
Dalsze czytanie
- Design Patterns: Elements of Reusable Object-Oriented Software (GoF)
- Command Pattern, często używany razem z Chain of Responsibility
- Observer Pattern, alternatywa do powiadamiania wielu obiektów