Nasza strona używa cookies. Dowiedz się więcej o celu ich używania i zmianie ustawień w przeglądarce. Korzystając ze strony, wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki. Rozumiem

Wzorzec projektowy: Łańcuch zobowiązań

Dowiedz się, jakie problemy pomoże Ci rozwiązać użycie łańcucha zobowiązań.
Wzorzec projektowy: Łańcuch zobowiązań

Łańcuch odpowiedzialności (nazywany też łańcuchem zobowiązań) jest czynnościowym wzorcem projektowym, który pozwala na oddzielenie nadawcy żądania od obiektu, który je zrealizuje. Osiąga to poprzez utworzenie łańcucha z obiektów, potencjalnie mogących obsłużyć żądanie. Przyjrzyjmy się zastosowaniom, wadom i zaletom tego wzorca.


Jaki problem rozwiązuje?

W momencie, gdy pojawia się więcej, niż jeden kandydat do obsłużenia żądania, musimy określić logikę, która posłuży wybraniu ostatecznego odbiorcy. Najbardziej naiwnym sposobem nakreślenia tego typu logiki jest przełożenie jej na kilka instrukcji warunkowych. W tym wypadku za każdym razem trzeba zmieniać logikę wraz z pojawieniem się nowego kandydata i utrudnia to dynamiczne zmienianie odbiorców w czasie działania programu.

Stąd potrzeba oddzielenia klienta (czyli obiektu wysyłającego wiadomość) od odbiorcy, a tym samym wprowadzenia luźnych zależności między nimi. Można tego dokonać na kilka sposobów, a to, który wybrać, zależy mocno od konkretnego przypadku.

Sztandarowym przykładem użycia Chain of Responsibility jest obsługa funkcji pomocy w programie okienkowym. Każdy widget może mieć swój własny sposób na obsługiwanie zapytania o pomoc albo też w ogóle nie zapewniać takiej funkcji. W tym wypadku to jego rodzic powinien spróbować obsłużyć żądanie lub przekazać je dalej, jeżeli nie potrafi tego zrobić. To właśnie tworzy łańcuch i znacznie upraszcza połączenia między obiektem, który wysyła żądanie, a odbiorcami.


Jak działa?

By wprowadzić w życie łańcuch odpowiedzialności, należy zaimplementować mniej więcej taką hierarchię klas:



Zachowanie może być bardziej czytelne, gdy przedstawimy je na diagramie sekwencji:



Handler może być tu interfejsem lub klasą bazową, którą rozszerzają konkretne implementacje (np. ConcreteHandler1 itd.). Klasa bazowa może być przydatna o tyle, że można tam zawrzeć podstawowe działanie łańcucha odpowiedzialności, czyli wywołanie metody handle na kolejnym elemencie w kolejce.

Przed wywołaniem łańcucha należy ustawić odpowiednio successor. Można zrobić to wprost lub skorzystać z już istniejących powiązań. Dlatego też często używa się wzorca Chain of Responsibility ze wzorcem Composite. Kompozyt organizuje obiekty w drzewiaste struktury, dzięki czemu automatycznie tworzy się kształt łańcucha odpowiedzialności i wystarczy dodać samą obsługę żądania.

W ConcreteHandler znajduje się logika odpowiadająca za obsłużenie żądania. To od niej zależy, czy łańcuch zakończy obsługę na tym etapie, czy przekaże request dalej do następcy (successor).

To wszystko tworzy elegancką i prostą strukturę kodu, która pozwala na łatwe rozszerzanie. Żądanie obsługi można wywoływać w którymkolwiek fragmencie łańcucha, co również pozwala na większą elastyczność.


Na co zwrócić uwagę i potencjalne modyfikacje


Domyślne zachowanie

Jedną z ważniejszych rzeczy, na którą należy zwrócić uwagę, jest to, że nie ma gwarancji, że żądanie zostanie obsłużone. By tak było, trzeba to zaimplementować. Jedno z rozwiązań podaje tu Michał Borawski z Kruk S.A:

Jeżeli żądanie po przejściu przez łańcuch handlerów nie zostanie obsłużone, możemy zaimplementować domyślne zachowanie, np. poprzez użycia wzorca Pustego Obiektu (ang. Null Object). Pozwoli nam to zachować płynność działania aplikacji, jak i uchroni nas przed nieoczekiwanym zachowaniem. Nie zawsze będzie to niestety możliwe, więc jest to też dobra okazja do wychwycenia potencjalnych defektów dotyczących informacji zawartych w danym żądaniu. Może warto takie zdarzenie zalogować w celu następnej weryfikacji przyczyny takiej sytuacji?

Hierarchia

Z praktycznej strony organizacja łańcucha też ma znaczenie, na co również zwraca uwagę Michał z Kruk S.A:

Warto jest zastanowić się, czy znana jest hierarchia, w odniesieniu do której powinniśmy wywoływać kolejne handlery. Jeżeli więc np. wybierzemy zastosowanie tego wzorca do walidacji danych, zacznijmy od tych ogólnych, najczęściej występujących błędów w danym kontekście. Najlepiej, jeżeli przy okazji są one najmniej wymagające dla procesora i pamięci lub nie korzystają z zasobów zewnętrznych jako zapytania do bazy danych, czy usługi REST. Czasami zatem warto nadać handlerom swoiste wagi, które określą również kolejność ich wywołania. Mogą one naturalnie wynikać z wymagań biznesowych lub być wynikiem ich analizy.

Połączenie z innymi wzorcami

Wspomnieliśmy już o połączeniu ze wzorcem kompozyt, który ułatwia wprowadzenie odpowiedniej struktury. Kolejną modyfikacją jest użycie wzorca komenda (ang. Command), do implementacji żądań. W tym wypadku można wywołać taką samą komendę w różnych kontekstach.

Warto też pochylić się przez moment nad tym, jak będzie wyglądać sam request. Możliwa jest jego parametryzacja, dzięki której będzie można obsługiwać więcej zachowań. Należy jednak pamiętać, że to skomplikuje logikę obiektów, które go obsługują.


Wady i zalety

  • + Oddzielenie sendera i receivera - wprowadzenie luźnych zależności
  • + Elastyczność w obsługiwaniu żądań
  • - Nieco utrudnione debugowanie, ze względu na dynamiczne ustalanie odpowiedzialnego obiektu
  • - Nie ma gwarancji, że żądanie zostanie obsłużone
Zobacz więcej na Bulldogjob

Masz coś do powiedzenia?

Podziel się tym z 100 tysiącami naszych czytelników

Dowiedz się więcej
Rocket dog