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:

Mermaid diagram

Szczegółowy diagram przepływu

Diagram pokazuje logikę decyzyjną każdego obsługującego:

Mermaid diagram

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

  1. Klient wysyła żądanie do pierwszego obsługującego w łańcuchu.
  2. 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.
  3. 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