Wiosenna WYPRZEDAŻ

Łańcuch zobowiązań

Znany też jako: Chain of command, Chain of Responsibility

Cel

Łańcuch zobowiązań jest behawioralnym wzorcem projektowym, który pozwala przekazywać żądania wzdłuż łańcucha obiektów obsługujących. Otrzymawszy żądanie, każdy z obiektów obsługujących decyduje o przetworzeniu żądania lub przekazaniu go do kolejnego obiektu obsługującego w łańcuchu.

Wzorzec projektowy Łańcuch zobowiązań

Problem

Wyobraź sobie, że pracujesz nad systemem zamawiania online. Chcesz ograniczyć dostęp do systemu, by wyłącznie użytkownicy uwierzytelnieni mogli składać zamówienia. Ponadto użytkownicy z uprawnieniami administracyjnymi powinni mieć pełen dostęp do wszystkich zamówień.

Po obmyśleniu planu, zdajesz sobie sprawę, że takie sprawdzenia powinno się wykonywać sekwencyjnie. Aplikacja może spróbować uwierzytelnić użytkownika otrzymawszy żądanie zawierające poświadczenia użytkownika. Jednak jeśli poświadczenia nie są prawidłowe i uwierzytelnienie nie powiedzie się, nie ma powodu dokonywać dalszych sprawdzeń.

Rozwiązanie problemu z użyciem wzorca Łańcuch zobowiązań

Żądanie musi przejść serię sprawdzeń zanim system zamawiania będzie mógł go obsłużyć.

W kolejnych miesiącach, implementujesz wiele takich sekwencyjnych sprawdzeń.

  • Jeden z twoich współpracowników zauważa, że przekazywanie surowych danych przez system zamawiania nie jest bezpieczne. Dodajesz więc etap walidacyjny, czyszczący dane zawarte w żądaniu.

  • Później ktoś zauważa, że system jest podatny na łamanie haseł metodą brute force. Aby się przed tym uchronić, dodajesz sprawdzenie odrzucające wielokrotne nieskuteczne próby uwierzytelnienia przychodzące z tego samego adresu IP.

  • Ktoś inny zaś zasugerował, że można przyspieszyć działanie systemu, gdyby zwracał on przechowane w pamięci podręcznej wyniki żądań zawierające te same dane. Dodajesz więc kolejne sprawdzenie, pozwalające żądaniu przejść dalej tylko jeśli nie ma już stosownej odpowiedzi zapisanej w pamięci podręcznej.

Z dodaniem każdego kolejnego sprawdzenia, kod puchnie i pojawia się bałagan

Im bardziej kod urósł, tym bardziej stał się zabałaganiony.

Kod sprawdzeń, który już na początku wyglądał pogmatwanie, spuchł jeszcze bardziej wraz z dodawaniem funkcjonalności. Zmiana jednego sprawdzenia czasem wpływała na inne. A co najgorsze, próba ponownego użycia sprawdzeń w zabezpieczeniu innych komponentów systemu spowodowała duplikację części kodu ponieważ niektóre komponenty potrzebowały sprawdzeń, ale inne nie.

System stał się trudny do zrozumienia i kosztowny w utrzymaniu. Po okresie trudzenia się postanawiasz dokonać refaktoryzacji całości.

Rozwiązanie

Jak wiele innych wzorców behawioralnych, Łańcuch Zobowiązań zakłada przekształcenie pewnych obowiązków w samodzielne obiekty zwane obiektami obsługującymi. W naszym przypadku, każde sprawdzenie powinno się wyekstrahować do osobnej klasy posiadającej jedną metodę dokonującą sprawdzenia. Żądanie wraz z towarzyszącymi mu danymi przekazywane jest jako argument tej metody.

Wzorzec sugeruje połączenie tych obiektów obsługujących w łańcuch. Każdy obiekt obsługujący stanowiący ogniwo łańcucha posiada pole przechowujące odniesienie do następnego obiektu w łańcuchu. Poza przetworzeniem żądania, obiekty przekazują je dalej. Żądanie biegnie wzdłuż łańcucha, by wszystkie ogniwa miały okazję je obsłużyć.

A co najlepsze, obiekt obsługujący może zdecydować o nieprzekazaniu żądania dalej i tym samym kończy proces.

W naszym przykładzie systemu zamawiającego, obiekt obsługujący dokonuje przetwarzania żądania i decyduje o przekazaniu go dalej, lub nie. Zakładając, że żądanie zawiera właściwe dane, obiekty obsługujące mogą wykonywać swoje obowiązki, takie jak uwierzytelnianie czy zapis w pamięci podręcznej.

Obiekty obsługujące ułożone są jeden przy drugim, tworząc łańcuch

Obiekty obsługujące są ułożone jeden obok drugiego, tworząc łańcuch.

Istnieje jednak nieco inne podejście (które weszło do kanonu), według którego obiekt obsługujący otrzymawszy żądanie decyduje czy może je obsłużyć i jeśli tak, to nie przekazuje go dalej. Więc albo tylko jeden obiekt obsługuje jedno żądanie, albo żaden. Podejście to jest bardzo powszechne w przypadku stosu zdarzeń w obrębie graficznego interfejsu użytkownika.

Na przykład, gdy użytkownik kliknie przycisk, zdarzenie rozpropaguje się wzdłuż łańcucha elementów UI, zaczynając od przycisku, poprzez jego kontenery (formatki lub panele) i dociera do głównego okna aplikacji. Zdarzenie jest przetwarzane przez pierwszy element w łańcuchu który jest w stanie je obsłużyć. Ten przykład jest też godny uwagi, bo pokazuje jak z każdego drzewa obiektów można wyekstrahować łańcuch.

Z gałęzi drzewa obiektów można uformować łańcuch

Z gałęzi drzewa obiektów można uformować łańcuch.

Istotnym jest, że wszystkie klasy obiektów obsługujących implementują ten sam interfejs. Każdy konkretny obiekt obsługujący powinien wiedzieć tylko o następnym, posiadającym metodę wykonaj. W ten sposób można komponować łańcuchy w trakcie działania programu, stosując różne obiekty obsługujące bez sprzęgania kodu z ich konkretnymi klasami.

Analogia do prawdziwego życia

Rozmowa z pomocą techniczną może być trudna

Połączenie z pomocą techniczną może być przekazane kolejnym jej pracownikom.

Właśnie kupiłeś sobie i zainstalowałeś jakąś część do komputera. Ponieważ jesteś geekiem, na komputerze jest kilka systemów operacyjnych. Uruchamiasz więc jeden po drugim, sprawdzając czy urządzenie jest obsługiwane. Windows wykrywa i włącza urządzenie automatycznie. Jednak twoja ukochana dystrybucja Linuksa odmawia współpracy. W nikłym przebłysku nadziei, dzwonisz na numer pomocy technicznej podany na opakowaniu.

Pierwsze, co słyszysz, to sztucznie brzmiący głos automatu zgłoszeniowego. Sugeruje on dziewięć typowych rozwiązań różnych problemów, ale żaden z nich nie dotyczy twego przypadku. Po jakimś czasie, automat poddaje się i łączy cię z żywym człowiekiem.

Ale żywy pracownik również nie jest w stanie zasugerować nic pożytecznego. Cytuje długie ustępy instrukcji obsługi i nie słucha twoich uwag. Po usłyszeniu dziesiąty raz sugestii “proszę spróbować wyłączyć i włączyć ponownie komputer”, żądasz połączenia z prawdziwym inżynierem.

Ostatecznie łączy cię z jednym z inżynierów, który zapewne od wielu godzin tęskni za rozmową z żywym człowiekiem, siedząc w swojej odosobnionej serwerowni gdzieś w ciemnej piwnicy. Inżynier podaje ci link do odpowiednich sterowników do urządzenia i tłumaczy jak je zainstalować pod Linuksem. Wreszcie — rozwiązanie! Rozłączasz się pełen radości.

Struktura

Struktura wzorca Łańcuch zobowiązańStruktura wzorca Łańcuch zobowiązań
  1. Obiekt Obsługujący deklaruje wspólny dla wszystkich obiektów obsługujących interfejs. Zazwyczaj posiada on tylko jedną metodę do obsługi żądań, ale czasem może zawierać też drugą, służącą do wybierania kolejnego obiektu w łańcuchu.

  2. Bazowy Obiekt Obsługujący to opcjonalna klasa, gdzie można umieścić kod przygotowawczy, wspólny dla wszystkich klas obsługujących.

    Na ogół klasa ta definiuje pole służące przechowywaniu odniesienia do kolejnego obiektu obsługującego. Klienci mogą sformować łańcuch przekazując obiekt obsługujący konstruktorowi lub metodzie setter poprzedniego obiektu. Klasa może też implementować domyślną obsługę: przekazać wykonanie kolejnemu obiektowi obsługującemu, sprawdziwszy, czy taki istnieje.

  3. Konkretne Obiekty Obsługujące zawierają faktyczny kod służący obsłudze żądań. Otrzymawszy żądanie, każdy obiekt musi zdecydować, czy je obsłużyć i czy przekazać je dalej.

    Obiekty obsługujące są zazwyczaj samodzielne i niezmienne, akceptują wszystkie konieczne dane jednorazowo za pośrednictwem konstruktora.

  4. Klient może skomponować łańcuch raz, albo robić to dynamicznie, zależnie od logiki aplikacji. Warto pamiętać, że żądanie może być przekazane dowolnemu ogniwu łańcucha — niekoniecznie pierwszemu.

Pseudokod

W poniższym przykładzie, wzorzec Łańcuch Zobowiązań jest odpowiedzialny za wyświetlanie pomocy kontekstowej dotyczącej aktywnych elementów interfejsu użytkownika.

Struktura przykładu użycia Łańcucha zobowiązań

Klasy interfejsu użytkownika (GUI) zbudowano według wzorca Kompozyt. Każdy element jest powiązany ze swoim kontenerem. W dowolnym momencie można stworzyć łańcuch elementów, zaczynający się od samego elementu i prowadzący przez wszystkie elementy kontenera.

Interfejs użytkownika aplikacji zazwyczaj jest ustrukturyzowany w formie drzewa obiektów. Na przykład klasa Dialog, która renderuje główne okno aplikacji byłaby korzeniem drzewa obiektów. Dialog zawiera Panele, które mogą z kolei zawierać inne panele lub proste niskopoziomowe elementy jak Przyciski i PolaTekstowe.

Prosty komponent może pokazywać krótkie kontekstowe podpowiedzi, o ile ma przypisaną mu jakąś treść pomocy. Ale bardziej złożone komponenty definiują swoje sposoby na wyświetlanie pomocy kontekstowej, jak prezentacja stosownego fragmentu instrukcji obsługi lub otwarcie strony internetowej w przeglądarce.

Struktura użycia wzorca Łańcuch zobowiązań

Sposób w jaki żądanie pomocy przechodzi przez kolejne obiekty GUI.

Gdy użytkownik wskaże kursorem jakiś element i wciśnie klawisz F1, aplikacja sprawdza jaki komponent znajduje się pod kursorem i wysyła mu żądanie wyświetlenia pomocy. Żądanie przechodzi przez wszystkie kontenery elementu, aż dotrze do tego, który jest w stanie obsłużyć żądanie.

// Interfejs obiektu obsługującego deklaruje metodę wykonującą
// żądanie.
interface ComponentWithContextualHelp is
    method showHelp()


// Klasa bazowa prostych komponentów.
abstract class Component implements ComponentWithContextualHelp is
    field tooltipText: string

    // Kontener komponentu pełni rolę kolejnego ogniwa łańcucha
    // obiektów obsługujących żądanie.
    protected field container: Container

    // Komponent pokazuje podpowiedź jeśli przypisano mu jakiś
    // tekst pomocy. W przeciwnym razie przekazuje wywołanie
    // kontenerowi, o ile takowy istnieje.
    method showHelp() is
        if (tooltipText != null)
            // Pokaż podpowiedź.
        else
            container.showHelp()


// Kontenery mogą zawierać zarówno proste komponenty, jak i inne
// kontenery podrzędne. Tu ustala się relacje łańcucha. Klasa
// dziedziczy zachowanie showHelp od klasy-rodzica.
abstract class Container extends Component is
    protected field children: array of Component

    method add(child) is
        children.add(child)
        child.container = this


// Prymitywne komponenty mogą posiadać tylko domyślną
// implementację funkcji pomoc...
class Button extends Component is
    // ...

// Ale złożone komponenty mogą nadpisywać domyślną
// implementację. Jeśli nie ma innej treści pomocy, komponent
// może zawsze wywołać implementację z klasy bazowej (patrz
// klasa Component).
class Panel extends Container is
    field modalHelpText: string

    method showHelp() is
        if (modalHelpText != null)
            // Wyświetl modalne okno dialogowe z treścią pomocy.
        else
            super.showHelp()

// ...jak wyżej...
class Dialog extends Container is
    field wikiPageURL: string

    method showHelp() is
        if (wikiPageURL != null)
            // Otwórz stronę z pomocą na wiki.
        else
            super.showHelp()


// Kod klienta.
class Application is
    // Każda aplikacja inaczej konfiguruje łańcuch.
    method createUI() is
        dialog = new Dialog("Budget Reports")
        dialog.wikiPageURL = "http://..."
        panel = new Panel(0, 0, 400, 800)
        panel.modalHelpText = "This panel does..."
        ok = new Button(250, 760, 50, 20, "OK")
        ok.tooltipText = "This is an OK button that..."
        cancel = new Button(320, 760, 50, 20, "Cancel")
        // ...
        panel.add(ok)
        panel.add(cancel)
        dialog.add(panel)

    // Wyobraź sobie co tu się może dziać.
    method onF1KeyPress() is
        component = this.getComponentAtMouseCoords()
        component.showHelp()

Zastosowanie

Stosuj wzorzec Łańcuch zobowiązań gdy twój program ma obsługiwać różne rodzaje żądań na różne sposoby, ale dokładne typy żądań i ich sekwencji nie są wcześniej znane.

Wzorzec pozwala połączyć wiele obiektów obsługujących w jeden łańcuch i otrzymawszy żądanie “odpytać” każde ogniwo czy jest w stanie je obsłużyć. W ten sposób wszystkie obiekty obsługujące mają okazję przetworzyć żądanie.

Stosuj ten wzorzec gdy istotne jest uruchomienie wielu obiektów obsługujących w pewnej kolejności.

Skoro można połączyć obiekty obsługujące w dowolnej kolejności, wszystkie żądania przejdą przez łańcuch w takim porządku, jaki zaplanowano.

Łańcuch zobowiązań pozwala ustawić obiekty obsługujące i ich kolejność w czasie działania programu.

Jeśli eksponujesz w klasie obsługującej metodę setter, ustawiające pole przechowujące odniesienie, będzie można wstawiać, usuwać lub zmieniać kolejność ogniw łańcucha dynamicznie.

Jak zaimplementować

  1. Zadeklaruj interfejs obiektu obsługującego i opisz sygnaturę metody obsługującej żądania.

    Zdecyduj jak klient będzie przekazywał dane żądań do metody. Najbardziej elastycznym sposobem jest konwersja żądania na obiekt i przekazywanie go metodzie obsługującej w charakterze argumentu.

  2. Aby wyeliminować powtarzający się kod przygotowawczy w konkretnych obiektach obsługujących, być może warto utworzyć abstrakcyjną bazową klasę obiektu obsługującego, wywodzącą się z interfejsu obiektu obsługującego.

    Klasa taka powinna zawierać pole przechowujące odniesienie do kolejnego ogniwa łańcucha. Rozważ uczynienie tej klasy niezmienialną. Jednak jeśli planujesz modyfikować łańcuch w czasie działania programu, musisz też zdefiniować setter zmieniający wartość pola z odniesieniem.

    Można także zaimplementować wygodne domyślne zachowanie metody obsługującej, która przekieruje żądanie do kolejnego obiektu o ile takowy istnieje. Konkretne obiekty obsługujące będą w stanie skorzystać z tego zachowania wywołując metodę nadklasy.

  3. Jeden po drugim twórz podklasy obiektów obsługujących i zaimplementuj im metody obsługujące. Każdy obiekt obsługujący powinien podjąć dwie decyzję otrzymawszy żądanie:

    • Czy przetworzyć żądanie.
    • Czy przekazać żądanie dalej wzdłuż łańcucha.
  4. Klient może albo złożyć łańcuch samodzielnie, albo otrzymać wcześniej przygotowany od innego obiektu. W drugim przypadku, trzeba zaimplementować jakieś klasy fabryczne do budowy łańcuchów zgodnie z konfiguracją lub ustawieniami środowiska.

  5. Klient może uruchomić kolejny obiekt w łańcuchu, niekoniecznie pierwszy. Żądanie zostanie przekazane dalej wzdłuż łańcucha, aż jakiś obiekt obsługujący odmówi przekazania go dalej, albo nie będzie już komu je przekazać.

  6. W związku z dynamiczną naturą łańcucha, klient powinien być gotów obsłużyć następujące scenariusze:

    • Łańcuch zawiera tylko jedno ogniwo.
    • Niektóre żądania mogą nie dotrzeć do końca łańcucha.
    • Inne żądania mogą dotrzeć do końca łańcucha i nie zostać obsłużone.

Zalety i wady

  • Można ustalać porządek obsługi żądania.
  • Zasada pojedynczej odpowiedzialności. Można rozprzęgnąć klasy wywołujące działania klas od klas wykonujących działania.
  • Zasada otwarte/zamknięte. Można wprowadzać do programu nowe obiekty obsługujące bez psucia istniejącego kodu klienta.
  • Niektóre żądania mogą wcale nie zostać obsłużone.

Powiązania z innymi wzorcami

  • Wzorce Łańcuch zobowiązań, Polecenie, Mediator i Obserwator dotyczą różnych sposobów na łączenie nadawców z odbiorcami żądań:

    • Łańcuch zobowiązań przekazuje żądanie sekwencyjnie wzdłuż dynamicznego łańcucha potencjalnych odbiorców, aż któryś z nich je obsłuży.
    • Polecenie pozwala nawiązywać jednokierunkowe połączenia pomiędzy nadawcami i odbiorcami.
    • Mediator eliminuje bezpośrednie połączenia pomiędzy nadawcami a odbiorcami, zmuszając ich do komunikacji za pośrednictwem obiektu mediator.
    • Obserwator pozwala odbiorcom dynamicznie zasubskrybować się i zrezygnować z subskrypcji żądań.
  • Łańcuch zobowiązań często stosuje się w połączeniu z Kompozytem. W takim przypadku, gdy komponent-liść otrzymuje żądanie, może je przekazać poprzez łańcuch nadrzędnych komponentów aż do korzenia drzewa obiektów.

  • Obsługujący w Łańcuchu zobowiązań mogą być zaimplementowani jako Polecenia. Można wówczas wykonać wiele różnych działań reprezentowanych jako żądania na tym samym obiekcie-kontekście.

    Istnieje jednak jeszcze jedno podejście, według którego samo żądanie jest obiektem Polecenie. W takim przypadku możesz wykonać to samo działanie na łańcuchu różnych kontekstów.

  • Łańcuch zobowiązań i Dekorator mają bardzo podobne struktury klas. Oba wzorce bazują na rekursywnej kompozycji w celu przekazania obowiązku wykonania przez ciąg obiektów. Istnieją jednak kluczowe różnice.

    Obsługujący Łańcucha zobowiązań mogą wykonywać działania niezależnie od siebie. Mogą również zatrzymać dalsze przekazywanie żądania na dowolnym etapie. Z drugiej strony, różne Dekoratory mogą rozszerzać obowiązki obiektu zachowując zgodność z interfejsem bazowym. Dodatkowo, dekoratory nie mają możliwości przerwania przepływu żądania.

Przykłady kodu

Łańcuch zobowiązań w języku C# Łańcuch zobowiązań w języku C++ Łańcuch zobowiązań w języku Go Łańcuch zobowiązań w języku Java Łańcuch zobowiązań w języku PHP Łańcuch zobowiązań w języku Python Łańcuch zobowiązań w języku Ruby Łańcuch zobowiązań w języku Rust Łańcuch zobowiązań w języku Swift Łańcuch zobowiązań w języku TypeScript