Wzorzec projektowy Łańcuch zobowiązań

Łańcuch odpowiedzialności, ang. Chain of Responsibility

Cel

  • Usunięcie powiązania pomiędzy nadawcą i odbiorcą żądania
  • Umożliwienie wielu obiektom obsługi żądania
  • Komunikat przekazywany jest pomiędzy obiektami należącymi do pewnego zbioru zgodnie z precyzyjnie wyznaczoną trasą i kolejnością.
  • Odpowiedzialność za przetworzenie komunikatu spada na obiekt, który jest do tego zadania najlepiej przygotowany.

Struktura

Obiekty Handler tworzą listę jednokierunkową (łańcuch), wzdłuż której są przekazywane żądania. Struktura tego wzorca jest bardzo prosta: obiekty typu Handler są powiązane ze sobą w postaci jednokierunkowej kolejki (albo łańcucha). Nadchodzące od klienta żądanie jest przekazywane wzdłuż tego łańcucha, gdzie każdy obiekt typu Handler ma szansę na ich obsłużenie. Co ważne, obiekty typu Handler są od siebie niezależne, tzn. nie wiedzą o sobie nic (poza abstrakcyjnym wskazaniem na obiekt następnika).

Uczestnicy

  • Handler - definiuje interfejs do obsługi żądań
  • Concrete Handler - obsługuje jeden rodzaj żądania, pozostałe przekazuje do następnika w łańcuchu, posiada referencję typu Handler do następnika
  • Client - inicjuje przetwarzanie, przekazując żądanie do pierwszego obiektu Handler w łańcuchu

Handler definiuje interfejs obsługi żądań. Zwykle jest to jedna metoda, która realizuje prosty algorytm: jeżeli dany obiekt ConcreteHandler jest w stanie obsłużyć żądanie, to obsługuje je; wprzeciwnym wypadku (bądź w sytuacji, gdy wiele obiektów typu Handler może obsłużyć jedno żądanie) - przekazuje je do swojego następnika w łańcuchu. Charakterystyczna dla wzorca jest dowolna konfigurowalność łańcucha: żaden jego element nie musi posiadać wiedzy o rodzaju żądań obsługiwanych przez kolejne elementy, dlatego zmiany w jego strukturze nie mają wpływu na zachowanie. Zadaniem klienta przy takiej strukturze jest przekazanie żądania pierwszemu elementowi łańcucha, który następnie dalej obsługuje żądanie.

Konsekwencje

  • Ograniczone powiązania:
    • Klient i każdy obiekt Handler nie wiedzą, który z pozostałych obiektów Handler obsługuje dany typ żądania
    • nadawca i odbiorca żądania nie mają o sobie żadnej wiedzy
  • Możliwość elastycznego przydziału odpowiedzialności do obiektów Handler
  • Ułatwione testowanie
  • Brak gwarancji obsłużenia żądania

Zalety

Zaletą tego wzorca jest znaczne ograniczenie powiązań pomiędzy klientem i każdym z obiektów Handler. Klient, przekazując żądanie, nie wie, który z obiektów Handler będzie je w rzeczywistości obsługiwał. Poszczególne ogniwa łańcucha są zorganizowane w postaci prostej kolejki jednokierunkowej, a ich wiedza o sobie nawzajem ogranicza się do abstrakcyjnego typu ogniwa. Nie znają swoich zadań ani klas, jakie implementują. Taka struktura pozwala elastycznie przydzielać odpowiedzialność do poszczególnych ogniw: każdy z nich zajmuje się obsługą żądań jednego typu, a rozszerzenie łańcucha o kolejne elementy nie wpływa na sposób przetwarzania przez niego żądań. To z kolei przyczynia się do łatwiejszego testowania każdego ogniwa łańcucha z osobna: wystarczy zweryfikować, czy poprawnie obsługuje on żądania jednego typu.

Wady

Wadą takiej konstrukcji łańcucha jest brak gwarancji obsługi żądania: kolejne ogniwa mogą zrezygnować z zajęcia się nim. Co więcej, informacja o tym fakcie nie jest przekazywana klientowi. W tym celu stosuje się rozmaite rozwiązania pośrednie: umieszczając informację o obsłudze wewnątrz żądania (wówczas brak takiej informacji oznacza jego nieobsłużenie) lub zmieniając nieco strukturę przetwarzania. Ponadto, błąd w implementacji filtra może skutkować nieprzekazaniem sterowania do następnika i przerwaniem łańcucha. Aby zminimalizować to ryzyko, w niektórych implementacjach klasa bazowa Filter posiada zaimplementowany na stałe mechanizm przekazywania sterowania do następnika, a programiście udostępniona jest tylko metoda dokonująca faktycznej obsługi żądania. Występują dwie odmiany wzorca: jedna, w której w przypadku znalezienia procedury obsługi żądanie nie jest dalej przesyłane, i druga, gdzie przetwarzanie jest kontynuowane niezależnie od tego, czy znaleziono właściwą procedurę obsługi. Drugi wariant, gdzie zdarzają się sytuacje wywołania wielu procedur obsługi, jest szczególnie trudny do diagnozowania. W przypadku wzorca Łańcuch odpowiedzialności potwierdza się reguła, że rozbudowane możliwości programów komputerowych osiąga się kosztem złożoności i wydajności.

Przykłady

Przykład 1

Obiekt Inbox wywołuje pierwszy obiekt Filter w łańcuchu. Kolejne filtry przekazują sobie sterowanie Prostym przykładem tego wzorca jest np. mechanizm filtrów obecnych w większości klientów poczty elektronicznej. Wiadomość przychodząca do foldera Inbox jest przesyłana przez łańcuch zdefiniowanych przez użytkownika filtrów: każdy z nich może dokonać pewnej akcji na wiadomości, polegającej na przeniesieniu jej do innego foldera, zmianie jej priorytetu czy usunięciu jej. Każdy filtr podejmuje decyzję (poprzez wywołanie metody isEligible()), czy konkretna wiadomość powinna być przez niego obsłużona, i przekazuje sterowanie dalej.

Przykład 2

Obiekt Inbox wywołuje kolejno obiekty Filter. Nie występuje bezpośrenie przekazywanie sterowania z jednego filtra do drugiego. Z uwagi na wymienione wcześniej niedogodności, przede wszystkim możliwość przerwania łańcucha sterowania, możliwa jest także inna struktura przetwarzania, która nie posiada już topologii łańcucha. W tym rozwiązaniu pojawia się nowa rola: zarządcy, który posiada referencje do wszystkich filtrów. Zarządca (w tym przypadku jest nim także obiekt Inbox) wywołuje po kolei wszystkie filtry, które obsługują daną wiadomość lub nie. Jednak dzięki temu, że filtry nie przekazują sobie bezpośrednio sterowania, nie ma możliwości przerwania łańcucha, a ponadto informacja o nieobsłużeniu żądania może być w łatwy sposób przedstawiona klientowi przez zarządcę.