Łańcuch odpowiedzialności w praktyce - implementacja i zastosowania

Diagram klas przedstawiający wzorzec projektowy "łańcuch odpowiedzialności" dla walidatorów.

Autor

Alex Jabłoński

Opublikowano

7 lis 2026

Spis treści

Gdy jedno żądanie może obsłużyć kilka różnych komponentów, szybko pojawia się problem z warunkami, wyjątkami i rosnącą klasą odpowiedzialną za wszystko naraz. Wzorzec chain of responsibility porządkuje taki przepływ, przekazując żądanie przez kolejne obiekty aż do znalezienia właściwego wykonawcy. Pokażę, jak działa ten mechanizm, gdzie sprawdza się w aplikacjach webowych, jak go zaimplementować oraz kiedy lepiej wybrać inne rozwiązanie.

Jedna prosta reguła porządkuje obsługę żądań

  • Żądanie przechodzi przez łańcuch handlerów, a każdy z nich może je obsłużyć albo przekazać dalej.
  • Nadawca nie musi znać konkretnego odbiorcy, dzięki czemu kod jest luźniej powiązany.
  • Wzorzec dobrze pasuje do middleware, walidacji, autoryzacji, filtrów i eskalacji zgłoszeń.
  • Największe ryzyko to niejasna kolejność handlerów, brak obsługi końcowej i trudne śledzenie przepływu.
  • Koszt wykonania wynosi zwykle O(n), ponieważ żądanie może przejść przez wszystkie elementy łańcucha.

Na czym polega łańcuch odpowiedzialności

W tym wzorcu żądanie trafia do pierwszego obiektu, zwanego handlerem. Handler podejmuje decyzję, czy potrafi je obsłużyć. Jeśli nie, przekazuje je do następnego elementu, a proces trwa do momentu znalezienia wykonawcy albo wyczerpania całego łańcucha.

Najważniejsza zasada brzmi: nadawca wysyła żądanie bez znajomości konkretnego odbiorcy. Dzięki temu kod wywołujący nie musi zawierać długiego bloku if lub switch, który rozrasta się przy każdej nowej regule biznesowej.

Typowa implementacja składa się z trzech elementów:

  • interfejsu handlera, który definiuje sposób obsługi żądania,
  • konkretnych handlerów, odpowiadających za poszczególne przypadki,
  • łańcucha, czyli ustalonej kolejności przetwarzania.

Wyobraźmy sobie system zgłoszeń. Proste pytanie może obsłużyć chatbot, problem techniczny trafia do pierwszej linii wsparcia, a sprawa dotycząca płatności do działu finansowego. Każdy poziom sprawdza, czy ma odpowiednie kompetencje, zamiast przekazywać wszystkie zgłoszenia bezpośrednio do jednego, przeciążonego komponentu.

Jak zbudować ten wzorzec w praktyce

Zaczynam od określenia wspólnego kontraktu. Każdy handler powinien przyjmować ten sam typ żądania i zwracać przewidywalny rezultat. Dobrze, gdy potrafi również przekazać sprawę dalej, zamiast samodzielnie znać wszystkie możliwe przypadki.

Minimalny przepływ

  1. Tworzysz obiekty obsługujące różne przypadki.
  2. Ustawiasz ich kolejność.
  3. Przekazujesz żądanie do pierwszego elementu.
  4. Handler obsługuje żądanie albo wywołuje kolejny element.
  5. Ostatni element zwraca odpowiedź awaryjną, jeśli nikt nie przejął sprawy.

W aplikacji webowej podobny schemat często występuje w middleware. Jeden komponent zapisuje żądanie w logach, drugi sprawdza autoryzację, trzeci waliduje dane, a ostatni przekazuje ruch do kontrolera. W tym wariancie kilka handlerów może wykonać swoje zadanie, a nie tylko jeden z nich.

Przykład w JavaScript

class Handler {
  setNext(handler) {
    this.next = handler;
    return handler;
  }

  handle(request) {
    if (this.next) {
      return this.next.handle(request);
    }

    return { handled: false, reason: "Brak obsługi" };
  }
}

class AuthHandler extends Handler {
  handle(request) {
    if (!request.user) {
      return { handled: true, status: 401 };
    }

    return super.handle(request);
  }
}

class RoleHandler extends Handler {
  handle(request) {
    if (request.requiredRole && !request.user.roles.includes(request.requiredRole)) {
      return { handled: true, status: 403 };
    }

    return super.handle(request);
  }
}

class ControllerHandler extends Handler {
  handle(request) {
    return { handled: true, status: 200, data: "Gotowe" };
  }
}

const auth = new AuthHandler();
const role = new RoleHandler();
const controller = new ControllerHandler();

auth.setNext(role).setNext(controller);

const response = auth.handle({
  user: { roles: ["editor"] },
  requiredRole: "editor"
});

W tym przykładzie pierwszy handler sprawdza obecność użytkownika, drugi jego uprawnienia, a trzeci wykonuje właściwą operację. Kolejność ma znaczenie: autoryzacja powinna nastąpić przed dostępem do logiki biznesowej, a walidacja danych przed zapisem.

Nie zawsze handler powinien kończyć obsługę. W potoku middleware często wykonuje własną operację, a później wywołuje następny element. Trzeba jednak jasno ustalić, czy dany komponent może zatrzymać przepływ, czy zawsze ma przekazywać żądanie dalej. Brak takiej reguły prowadzi do trudnych do wykrycia błędów.

Gdzie ten wzorzec naprawdę się sprawdza

Najbardziej naturalnym środowiskiem dla łańcucha jest aplikacja, w której żądanie przechodzi przez kilka niezależnych kontroli. W programowaniu webowym zamiast budować jedną ogromną funkcję, można podzielić przepływ na małe kroki, które da się testować i wymieniać osobno.

Zastosowanie Jak działa łańcuch Co daje
Middleware HTTP Logowanie, autoryzacja, limity i obsługa żądania wykonują się w ustalonej kolejności. Łatwe dodawanie i usuwanie etapów.
Walidacja formularza Kolejne handlery sprawdzają format, wymagane pola i reguły biznesowe. Każda reguła ma własną odpowiedzialność.
Eskalacja zgłoszeń Sprawa trafia do coraz bardziej wyspecjalizowanego poziomu wsparcia. Proste przypadki nie obciążają ekspertów.
Filtrowanie zdarzeń Każdy filtr może przepuścić, zmodyfikować albo zatrzymać zdarzenie. Elastyczne reagowanie na różne typy danych.
Obsługa wyjątków Kolejne komponenty próbują obsłużyć błąd o określonym typie. Logika błędów nie jest skupiona w jednym miejscu.

Dobrym przykładem jest walidacja zamówienia. Pierwszy etap sprawdza, czy użytkownik przesłał wymagane pola, drugi weryfikuje dostępność produktów, trzeci kontroluje limit płatności, a czwarty przekazuje poprawne zamówienie do realizacji. Każdy krok można zmienić bez przebudowy całego procesu.

Ten mechanizm sprawdza się także przy regułach o zmiennej kolejności. Na przykład w panelu administracyjnym najpierw można sprawdzić sesję, później uprawnienia, a dopiero potem ograniczenia wynikające z konkretnej organizacji. Gdy polityka dostępu się zmieni, wymieniam lub przestawiam odpowiedni handler.

Co zyskujesz, a za co płacisz

Największą zaletą jest luźne powiązanie nadawcy z odbiorcą. Kod, który rozpoczyna obsługę, nie musi znać szczegółów wszystkich możliwych przypadków. Dodanie nowego handlera zwykle nie wymaga modyfikowania istniejących klas.

Drugą korzyścią jest możliwość konfigurowania przepływu. Ten sam zestaw handlerów można ułożyć inaczej dla panelu administratora, API publicznego i zadania uruchamianego w tle. Dobrze zaprojektowany łańcuch sprzyja też testom jednostkowym, bo każdy element można sprawdzić niezależnie.

Korzyść Praktyczne znaczenie
Mniejsza liczba zależności Handler nie musi znać całej logiki procesu.
Łatwe rozszerzanie Nową regułę można dodać jako osobny komponent.
Elastyczna kolejność Przepływ można dopasować do konkretnego scenariusza.
Lepsze testowanie Każdy handler ma mały, wyraźny zakres odpowiedzialności.
Ryzyko ukrytej logiki Bez dokumentacji trudno od razu ustalić, gdzie żądanie zostanie obsłużone.
Koszt liniowy Przy n handlerach żądanie może wymagać do n sprawdzeń.

Minusem jest przede wszystkim mniejsza przewidywalność przepływu. Jeśli łańcuch ma kilkanaście elementów, a każdy może zmienić dane lub przerwać operację, diagnozowanie problemu staje się trudniejsze niż w prostym wywołaniu funkcji.

Dlatego nie tworzę łańcucha tylko dlatego, że kilka warunków wygląda nieestetycznie. Jeżeli mamy dwa proste przypadki i stabilną logikę, zwykły if może być czytelniejszy. Wzorzec zaczyna się opłacać wtedy, gdy reguły często się zmieniają, rosną albo występują w kilku różnych przepływach.

Jak odróżnić go od podobnych rozwiązań

Najczęstsze nieporozumienie polega na utożsamianiu każdego potoku przetwarzania z tym wzorcem. Podobny wygląd nie oznacza identycznego celu. Różnica tkwi w tym, kto podejmuje decyzję i czy następny element jest opcjonalny.

Rozwiązanie Główna idea Kiedy wybrać
Łańcuch odpowiedzialności Handler obsługuje żądanie albo przekazuje je dalej. Gdy nie chcesz wiązać nadawcy z konkretnym odbiorcą.
Pipeline Każdy etap zwykle wykonuje swoją część i przekazuje wynik dalej. Gdy wszystkie kroki mają uczestniczyć w przetwarzaniu.
Strategy Wybierasz jedną alternatywną implementację algorytmu. Gdy potrzebujesz jednej metody spośród kilku wariantów.
Decorator Owijasz obiekt dodatkowymi zachowaniami. Gdy chcesz rozszerzać funkcję bez zmiany jej interfejsu.
Observer Powiadamiasz wiele niezależnych odbiorców o zdarzeniu. Gdy każdy zainteresowany może zareagować na ten sam fakt.

Przykładowo autoryzacja i kontrola limitu mogą tworzyć łańcuch, bo każdy etap może zatrzymać żądanie. Z kolei zestaw transformacji tekstu, w którym wszystkie operacje mają wykonać się po kolei, bliżej przypomina pipeline. To rozróżnienie pomaga uniknąć niewłaściwej abstrakcji.

W praktyce frameworki często mieszają te idee. Middleware może działać jak łańcuch odpowiedzialności, ale jednocześnie tworzyć pipeline, w którym każdy element wykonuje kod przed i po wywołaniu następnego. Nie przywiązuję się więc do nazwy tak mocno, jak do konkretnej semantyki przepływu.

Jak uniknąć typowych błędów

Nie zostawiaj żądania bez końcowej decyzji

Łańcuch powinien mieć jasno określone zakończenie. Jeśli żaden handler nie obsłuży żądania, system musi zwrócić błąd, wartość domyślną albo przekazać sprawę do kontrolera awaryjnego. Ciche pominięcie żądania jest szczególnie niebezpieczne w autoryzacji i walidacji.

Dokumentuj kolejność

Dwa poprawne handlery mogą stworzyć błędny system, jeśli zostaną ustawione w złej kolejności. Walidacja danych przed sprawdzeniem uprawnień może ujawnić informacje, do których użytkownik nie powinien mieć dostępu. Kolejność jest częścią konfiguracji biznesowej, a nie przypadkowym szczegółem technicznym.

Nie mieszaj zbyt wielu odpowiedzialności

Handler powinien wykonywać jedną spójną czynność. Komponent, który jednocześnie loguje, pobiera dane z bazy, sprawdza role i wysyła e-mail, przestaje być łatwy do testowania. Rozdzielenie tych zadań zwykle daje więcej korzyści niż skrócenie liczby klas.

Przeczytaj również: SOLID w aplikacjach webowych - Czy naprawdę go potrzebujesz?

Dodaj obserwowalność

Przy większym łańcuchu przydają się logi z nazwą handlera, identyfikatorem żądania i informacją o podjętej decyzji. Bez tego trudno ustalić, czy żądanie zostało zatrzymane przez autoryzację, walidację czy limit. W systemach produkcyjnych ślad przepływu jest równie ważny jak sama logika.

  • Ustal, czy handler może zatrzymać przetwarzanie.
  • Określ zachowanie, gdy nikt nie przejmie żądania.
  • Testuj poprawną i błędną kolejność elementów.
  • Sprawdź, czy handler nie modyfikuje danych w sposób nieudokumentowany.
  • Mierz czas wykonania, jeśli łańcuch obsługuje duży ruch.

Najlepszy test dla tej architektury

Przed wdrożeniem zadaję sobie jedno pytanie: czy potrafię dodać nową regułę bez edytowania kodu, który rozpoczyna obsługę żądania? Jeśli odpowiedź brzmi tak, wzorzec prawdopodobnie dobrze pasuje do problemu.

Jeśli natomiast każdy przypadek musi przejść przez wszystkie elementy, a kolejność jest stała i obowiązkowa, prosty pipeline może być bardziej czytelny. Łańcuch odpowiedzialności daje najwięcej wtedy, gdy odpowiedzialność może przejąć różny komponent, a sam nadawca nie powinien wiedzieć, który z nich to zrobi.

FAQ - Najczęstsze pytania

Żądanie trafia do pierwszego handlera, który może je obsłużyć albo przekazać dalej. Proces kończy się po znalezieniu wykonawcy lub po przejściu przez cały łańcuch, który powinien mieć zdefiniowaną odpowiedź awaryjną.

Należy zdefiniować wspólny kontrakt handlerów, utworzyć konkretne komponenty, ustawić ich kolejność i przekazać żądanie do pierwszego elementu. Przykład z artykułu łączy AuthHandler, RoleHandler i ControllerHandler, a brak obsługi kończy odpowiedzią z informacją o nieobsłużonym żądaniu.

Pasuje do middleware, walidacji formularzy, autoryzacji, filtrowania zdarzeń, eskalacji zgłoszeń i obsługi wyjątków. Jest szczególnie użyteczny, gdy reguły często się zmieniają, występują w kilku przepływach albo różne komponenty mogą przejąć odpowiedzialność za żądanie.

W łańcuchu handler może obsłużyć żądanie albo przekazać je dalej, więc następny element jest opcjonalny. Pipeline zwykle uruchamia wszystkie etapy po kolei, natomiast Strategy wybiera jedną alternatywną implementację algorytmu.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi:

middleware walidacja autoryzacja pipeline łańcuch odpowiedzialności

Udostępnij artykuł

Alex Jabłoński

Alex Jabłoński

Nazywam się Alex Jabłoński i od 14 lat zajmuję się programowaniem webowym. Moja przygoda z tą dziedziną zaczęła się od prostych projektów, które stopniowo przerodziły się w pasję do tworzenia złożonych aplikacji internetowych. Interesuje mnie nie tylko kodowanie, ale także dzielenie się wiedzą, co skłoniło mnie do pisania artykułów, które pomagają innym zrozumieć zasady programowania. W swoich tekstach koncentruję się na podstawach, ale także na bardziej zaawansowanych zagadnieniach, starając się w przystępny sposób wyjaśnić trudne tematy. Przy pisaniu zwracam szczególną uwagę na aktualność informacji oraz ich zrozumiałość. Regularnie śledzę nowinki w branży, porównuję różne podejścia i staram się uprościć skomplikowane koncepcje, aby były dostępne dla każdego, niezależnie od poziomu zaawansowania. Moim celem jest dostarczenie wartościowych treści, które pomogą czytelnikom w ich drodze do kariery w programowaniu webowym.

Napisz komentarz