Wzorce projektowe – Łańcuch Odpowiedzialności
Łańcuch Odpowiedzialności to czynnościowy wzorzec projektowy, tworzący listę obiektów, które po kolei analizują żądanie i jeżeli potrafią je obsłużyć, to przystępują do procesu jego realizacji. W przeciwnym wypadku przekazują żądanie dalej, do kolejnego ogniwa w łańcuchu.
Charakterystyka wzorca łańcuch odpowiedzialności
Łańcuch Odpowiedzialności (Chain of Responsibility) nazywany jest również łańcuchem zobowiązań, ponieważ jego głównym zadaniem jest przekazanie zobowiązania obsługi żądania na kolejny element w łańcuchu. Łańcuch takich obiektów tworzy listę jednokierunkową, w której odpowiedzialność za przetworzone żądanie spada na najlepiej przygotowany do tego obiekt.
Struktura kodu i zastosowanie opisywanego tu wzorca jest bardzo proste, co pokazuje poniższy diagram klas UML. Łańcuch odpowiedzialności składa się z obiektów (ogniw), które kolejno sprawdzają czy są w stanie przetworzyć przekazane im żądanie. Jeżeli tak to realizują zadanie i zwracają wynik, natomiast jeżeli nie, to przekazują żądanie dalej, do kolejnego ogniwa w łańcuchu. Każde z ogniw powinno wskazywać na koleje, oczywiście oprócz ostatniego, który oprócz kodu przetwarzającego żądanie, powinien obsłużyć sytuację, w której żaden z obiektów nie był w stanie zrealizować zadania.
Przykład zastosowania wzorca łańcuch odpowiedzialności – operacje na liczbach
Przykład w języku PHP, opisany poniżej, przedstawia sposób i implementacji i użycia wzorca Łańcuch Odpowiedzialności. Zadaniem skryptu będzie wykonanie prostych operacji matematycznych na dwóch liczbach.
Jako pierwszą napiszemy prostą klasę, która będzie reprezentowała nasze żądanie do przetworzenia. Klasa posiada właściwości przechowujące liczby, na których będzie wykonywana operacja oraz rodzaj tej operacji.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
namespace DesignPatterns\ChainOfResponsibility\Numbers; class Numbers { private $num1; private $num2; private $calcType; public function __construct($num1, $num2, $calcType) { $this->num1 = $num1; $this->num2 = $num2; $this->calcType = $calcType; } public function getNum1() { return $this->num1; } public function getNum2() { return $this->num2; } public function getCalcType() { return $this->calcType; } } |
Teraz przystąpmy do napisania struktury programu, opartego na omawianym wzorcu. W pierwszej kolejności będzie to abstrakcyjna klasa reprezentująca abstrakcyjne ogniwo łańcucha. Klasa ta będzie rozszerzana w dalszej części przez konkretne ogniwa. Zawiera ona referencje do kolejnego ogniwa oraz metodę ustawiającą to ogniwo. Posiada również abstrakcyjną funkcję realizującą przekazywane żądanie. Metoda ta będzie implementowana w kolejnych elementach łańcucha.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
namespace DesignPatterns\ChainOfResponsibility\Numbers; abstract class Chain { protected $nextChainElement; public function setNextChainElement(Chain $nextChainElement) { $this->nextChainElement = $nextChainElement; } public abstract function calculate(Numbers $request); } |
Przystąpmy do napisania poszczególnych ogniw wzorca. Będą to klasy realizujące poszczególne działania matematyczne – dodawanie, odejmowanie, mnożenie i dzielenie. Jak widać na poniższych listingach poszczególne klasy implementują metodę obsługującą żądanie, w której najpierw sprawdzają, czy są w stanie to zrobić. Jeżeli tak zwracają wynik, jeżeli nie, to wywołują tę samą metodę w kolejnym elemencie łańcucha.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
namespace DesignPatterns\ChainOfResponsibility\Numbers; use DesignPatterns\ChainOfResponsibility\Numbers\Chain; class NumbersAdd extends Chain { public function calculate(Numbers $request) { if($request->getCalcType() == 'add') { $result = $request->getNum1() + $request->getNum2(); echo 'Dodawanie: '.$request->getNum1().' + '.$request->getNum2().' = '.$result; return $result; } $this->nextChainElement->calculate($request); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
namespace DesignPatterns\ChainOfResponsibility\Numbers; use DesignPatterns\ChainOfResponsibility\Numbers\Chain; class NumbersSubtract extends Chain { public function calculate(Numbers $request) { if($request->getCalcType() == 'sub') { $result = $request->getNum1() - $request->getNum2(); echo 'Odejmowanie: '.$request->getNum1().' - '.$request->getNum2().' = '.$result; return $result; } $this->nextChainElement->calculate($request); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
namespace DesignPatterns\ChainOfResponsibility\Numbers; use DesignPatterns\ChainOfResponsibility\Numbers\Chain; class NumbersMultiply extends Chain { public function calculate(Numbers $request) { if($request->getCalcType() == 'mul') { $result = $request->getNum1() * $request->getNum2(); echo 'Mnożenie: '.$request->getNum1().' * '.$request->getNum2().' = '.$result; return $result; } $this->nextChainElement->calculate($request); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
namespace DesignPatterns\ChainOfResponsibility\Numbers; use DesignPatterns\ChainOfResponsibility\Numbers\Chain; class NumbersDivide extends Chain { public function calculate(Numbers $request) { if($request->getCalcType() == 'div') { $result = $request->getNum1() / $request->getNum2(); echo 'Dzielenie: '.$request->getNum1().' / '.$request->getNum2().' = '.$result; return $result; } echo 'Koniec operacji'; } } |
Mając tak napisany skrypt, zadaniem klienta jest odpowiednie przygotowanie łańcucha, tzn. ustawienie kolejnych jego elementów, a następnie wywołania odpowiedniej metody na pierwszym obiekcie. W przypadku, gdy klient źle zbuduje łańcuch może dojść do sytuacji, gdzie żądanie nie zostanie w ogóle obsłużone.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
use DesignPatterns\ChainOfResponsibility\Numbers\Numbers; use DesignPatterns\ChainOfResponsibility\Numbers\NumbersAdd; use DesignPatterns\ChainOfResponsibility\Numbers\NumbersSubtract; use DesignPatterns\ChainOfResponsibility\Numbers\NumbersMultiply; use DesignPatterns\ChainOfResponsibility\Numbers\NumbersDivide; $chainAdd = new NumbersAdd(); $chainSubtract = new NumbersSubtract(); $chainMultiply = new NumbersMultiply(); $chainDivide = new NumbersDivide(); $chainAdd->setNextChainElement($chainSubtract); $chainSubtract->setNextChainElement($chainMultiply); $chainMultiply->setNextChainElement($chainDivide); $request1 = new Numbers(4, 2, 'mul'); $chainAdd->calculate($request1); $request2 = new Numbers(4, 2, 'sub'); $chainAdd->calculate($request2); |
Bardzo często zdarza się, że klient nie ma pojęcia, które ogniwo łańcucha zrealizuje zlecone zadanie. Wynik działania powyższego testu jest jak najbardziej poprawny.
1 2 |
Mnożenie: 4 * 2 = 8 Odejmowanie: 4 - 2 = 2 |
Powyższy przykład można pobrać tutaj: numbers-calc
Przykład na GitHub: https://github.com/molitorys/design-patterns/tree/master/src/ChainOfResponsibility/Numbers
Podsumowanie
Zaletą stosowania wzorca łańcucha zobowiązań jest możliwość dynamicznego dodawania lub usuwania jego elementów. Ogniwa łańcucha są niezależne od siebie i nie muszą znać struktury łańcucha. Stosowanie wzorca zaleca się wszędzie tam, gdzie więcej niż jeden obiekt może obsłużyć żądanie, ale nie wiadomo z góry który z nich to zrobi oraz gdy obiekt obsługujący żądanie powinien być ustalony automatycznie.