Lekce 20 - Chain of responsibility
V minulé lekci, Iterator, jsme si ukázali návrhový vzor Iterator. Tento vzor chování zavádí samostatný objekt, který umožňuje lineární procházení kolekcemi, aniž bychom museli znát vnitřní strukturu těchto kolekcí.
V dnešním tutoriálu Návrhové vzory GoF si představíme návrhový vzor Chain of responsibility, který umožňuje oddělení odesílatele požadavku od jeho příjemců.
Motivace použití vzoru Chain of responsibility
Ve vývoji by se nám hodilo oddělit odesílatele požadavku od jeho příjemců tak, aby požadavek odesílatele byl předáván mezi příjemci až k tomu, který je kompetentní jej vyřídit. A zároveň ošetřit stav, kdy kompetentní příjemce nebude nalezen.
Pokud bychom napevno spojili třídu, která posílá nějaký požadavek a třídu, která jej zpracovává, porušili bychom návrhový vzor low coupling, o kterém se dočteme v lekci Návrhové vzory GRASP. Také bychom přišli o možnost napojení na více handlerů, které požadavek zpracovávají. Toho se v praxi často využívá při aplikování různých filtrů, které jsou v řetězu za sebou a požadavek postupně nějak zpracovávají.
A právě tyto problémy řeší návrhový vzor Chain of responsibility. Jeho použití není omezené jen na lineární datové struktury, nýbrž předávání odpovědnosti může probíhat i na úrovni k rodiči ve stromových strukturách. Někdy bývá vzor kombinovaný se vzorem Composite. Návrhový vzor Composite definuje nejefektivnější způsob, jak takové stromové struktury vytvářet. Na takový strom někdy může být referováno jako na Tree of responsibility.
Některé frameworky používají vzor Chain of responsibility na implementaci událostního modelu.
Definice vzoru Chain of responsibility
Asi nás nepřekvapí, že příjemce požadavku Handler
je ve
vzoru definován jako rozhraní. S tímto rozhraním komunikuje
odesílatel požadavku. Rozhraní příjemce požadavku implementují v řetězu
nebo stromu jednotlivé handlery požadavku, které jsou spolu propojené
pomocí referencí.
Podívejme se na grafické znázornění UML diagramem:

Handler
typicky poskytuje:
- metody pro nastavení dalšího handleru,
- polymorfní metodu pro vyřízení požadavku,
- metodu pro předání požadavku dalšímu handleru.
Příklad implementace vzoru Chain of responsibility
Na vzor Chain of responsibility se dá vymyslet spousta příkladů, jelikož tímto způsobem funguje velké množství služeb. Když si například objednáváme zboží z ciziny, náš požadavek vyřídí řetěz hned několik pošt. Reálné praktické užití ovšem zpravidla bývá v implementaci filtrů. Filtry jsou spojené za sebou a požadavek pohltí v případě, že není žádoucí. Až teprve poslední článek řetězce požadavek vyřídí.
Vytvořme si ukázku řetězu takových filtrů na požadavek přijetí emailu. Požadavek postupně obdrží:
- spamfilter,
- uživatelský filtr,
- handler, který jej vloží do příchozí pošty,
- handler, který vyvolá upozornění na nový email.
Udělejme si příklad na příchod emailu, který proputuje několika filtry. Filtry bude možné libovolně přidávat a měnit právě díky vzoru Chain of responsibility.
Abstraktní třída
HandlerPozadavku
Nejprve bychom definovali abstrakci pro handlery v řetězu:
-
public abstract class HandlerPozadavku { private HandlerPozadavku dalsi; public HandlerPozadavku SetDalsi(HandlerPozadavku dalsi) { this.dalsi = dalsi; return dalsi; } protected void NechVyriditDalsiho(Pozadavek pozadavek) { if (dalsi != null) dalsi.VyridPozadavek(pozadavek); } public abstract void VyridPozadavek(Pozadavek pozadavek); }
-
public abstract class HandlerPozadavku { private HandlerPozadavku dalsi; public HandlerPozadavku setDalsi(HandlerPozadavku dalsi) { this.dalsi = dalsi; return dalsi; } protected void nechVyriditDalsiho(Pozadavek pozadavek) { if (dalsi != null) dalsi.vyridPozadavek(pozadavek); } public abstract void vyridPozadavek(); }
-
abstract class HandlerPozadavku { private $dalsi; public function setDalsi($dalsi) { $this->dalsi = $dalsi; return $dalsi; } protected function nechVyriditDalsiho($pozadavek) { if ($this->dalsi !== null) $this->dalsi->vyridPozadavek($pozadavek); } abstract public function vyridPozadavek($pozadavek); }
-
function HandlerPozadavku() { this.dalsi = null; } HandlerPozadavku.prototype.setDalsi = function(dalsi) { this.dalsi = dalsi; return dalsi; }; HandlerPozadavku.prototype.nechVyriditDalsiho = function(pozadavek) { if (this.dalsi !== null) { this.dalsi.vyridPozadavek(pozadavek); } }; HandlerPozadavku.prototype.vyridPozadavek = function() { throw new Error('Abstraktní metoda vyridPozadavek() musí být implementována v dědících třídách'); };
-
from abc import ABC, abstractmethod class HandlerPozadavku(ABC): def __init__(self): self.dalsi = None def set_dalsi(self, dalsi): self.dalsi = dalsi return dalsi def nech_vyridit_dalsiho(self, pozadavek): if self.dalsi is not None: self.dalsi.vyrid_pozadavek(pozadavek) @abstractmethod def vyrid_pozadavek(self, pozadavek): pass
U Javascriptové verze si můžeme všimnout využití
vlastnosti prototype
. Ta umožňuje přiřadit k metodě funkce.
Metoda, ke které jsou nové funkce přiřazené se stává konstruktorem. V
kódu níže pomocí new HandlerPozadavku()
vytvoříme instanci
typu HandlerPozadavku
, kterou nahradíme rozhraní, které
Javascript nepodporuje.
V metodě setDalsi()
vracíme další handler v řetězu,
abychom mohli pomocí vzoru Method chaining (více v lekci Method
chaining a method cascading) rovnou poskládat celý řetěz. Řetěz bychom
poskládali například takto:
-
spamFiltr.SetDalsi(uzivatelskyFiltr).SetDalsi(prichoziPostaHandler);
-
spamFiltr.setDalsi(uzivatelskyFiltr).setDalsi(prichoziPostaHandler);
-
$spamFiltr->setDalsi($uzivatelskyFiltr)->setDalsi($prichoziPostaHandler);
-
spamFiltr.handler.setDalsi(uzivatelskyFiltr).handler.setDalsi(prichoziPostaHandler);
-
spamFiltr.setDalsi(uzivatelskyFiltr).setDalsi(prichoziPostaHandler)
Řetěz můžeme takto jednoduše pospojovat na jediném řádku.
Implementace konkrétních handlerů
Možná podoba konkrétních handlerů je následující:
-
public class SpamFiltr: HandlerPozadavku { public override void VyridPozadavek(Pozadavek pozadavek) { if (!pozadavek.Email.Text.Contains("free pills")) // Jednoduchý spamfiltr { NechVyriditDalsiho(pozadavek); } } } public class UzivatelskyFiltr: HandlerPozadavku { public List<string> ZakazaneAdresy = new List<string>(); public override void VyridPozadavek(Pozadavek pozadavek) { if (!ZakazaneAdresy.Contains(pozadavek.Email.Adresa)) // Jednoduchý uživatelský filtr { NechVyriditDalsiho(pozadavek); } } } public class PrichoziPostaHandler: HandlerPozadavku { private PrichoziPosta posta; public PrichoziPostaHandler(PrichoziPosta posta) { this.posta = posta; } public override void VyridPozadavek(Pozadavek pozadavek) { posta.Pridej(pozadavek.Email); } }
-
public class SpamFiltr extends HandlerPozadavku { public void vyridPozadavek(Pozadavek pozadavek) { if (!pozadavek.Email.Text.Contains("free pills")) { // Jednoduchý spamfiltr nechVyriditDalsiho(pozadavek); } } } public class UzivatelskyFiltr extends HandlerPozadavku { public List<String> zakazaneAdresy = new ArrayList<String>(); public void vyridPozadavek(Pozadavek pozadavek) { if (!zakazaneAdresy.contains(pozadavek.email.adresa)) { // Jednoduchý uživatelský filtr nechVyriditDalsiho(pozadavek); } } } public class PrichoziPostaHandler extends HandlerPozadavku { private PrichoziPosta posta; public PrichoziPostaHandler(PrichoziPosta posta) { this.posta = posta; } public void vyridPozadavek(Pozadavek pozadavek) { posta.pridej(pozadavek.email); } }
-
class SpamFiltr extends HandlerPozadavku { public function vyridPozadavek($pozadavek) { if (strpos($pozadavek->getEmail()->getText(), "free pills") === false) { // Jednoduchý spamfiltr $this->nechVyriditDalsiho($pozadavek); } } } class UzivatelskyFiltr extends HandlerPozadavku { public $zakazaneAdresy = []; public function vyridPozadavek($pozadavek) { if (!in_array($pozadavek->getEmail()->getAdresa(), $this->zakazaneAdresy)) { // Jednoduchý uživatelský filtr $this->nechVyriditDalsiho($pozadavek); } } } class PrichoziPostaHandler extends HandlerPozadavku { private $posta; public function __construct($posta) { $this->posta = $posta; } public function vyridPozadavek($pozadavek) { $this->posta->pridej($pozadavek->getEmail()); } }
-
function SpamFiltr() { this.handler = new HandlerPozadavku(); } SpamFiltr.prototype.vyridPozadavek = function(pozadavek) { if (!pozadavek.email.text.includes("free pills")) { // Jednoduchý spamfiltr this.handler.nechVyriditDalsiho(pozadavek); } }; function UzivatelskyFiltr() { this.handler = new HandlerPozadavku(); this.zakazaneAdresy = []; } UzivatelskyFiltr.prototype.vyridPozadavek = function(pozadavek) { if (!this.zakazaneAdresy.includes(pozadavek.email.adresa)) { // Jednoduchý uživatelský filtr this.handler.nechVyriditDalsiho(pozadavek); } }; function PrichoziPostaHandler(posta) { this.handler = new HandlerPozadavku(); this.posta = posta; } PrichoziPostaHandler.prototype.vyridPozadavek = function(pozadavek) { this.posta.pridej(pozadavek.email); };
-
class SpamFiltr(HandlerPozadavku): def vyridPozadavek(self, pozadavek): if "free pills" not in pozadavek.email.text: # Jednoduchý spamfiltr self.nechVyriditDalsiho(pozadavek) class UzivatelskyFiltr(HandlerPozadavku): def __init__(self): self.zakazaneAdresy = [] def vyridPozadavek(self, pozadavek): if pozadavek.email.adresa not in self.zakazaneAdresy: # Jednoduchý uživatelský filtr self.nechVyriditDalsiho(pozadavek) class PrichoziPostaHandler(HandlerPozadavku): def __init__(self, posta): self.posta = posta def vyridPozadavek(self, pozadavek): self.posta.pridej(pozadavek.email)
Funkčnosti jednotlivých filtrů jsou samozřejmě extrémně zjednodušené
a pouze ilustrativní. Celý řetěz bychom dali do pohybu předáním
požadavku jeho prvnímu článku, kterým je SpamFiltr
.
V další lekci, Command (Příkaz), si představíme návrhový vzor Command, jenž nám umožňuje zapouzdřit žádost o provedení operace do objektu.