Programování

Úskalí a vylepšení vzoru Chain of Responsibility

Nedávno jsem napsal dva programy Java (pro operační systém Microsoft Windows), které musí zachytit globální události klávesnice generované jinými aplikacemi běžícími současně na stejné ploše. Společnost Microsoft poskytuje způsob, jak toho dosáhnout, zaregistrováním programů jako globálního posluchače zavěšení klávesnice. Kódování netrvalo dlouho, ale ladění ano. Zdálo se, že tyto dva programy fungovaly dobře, když byly testovány samostatně, ale selhaly, když byly testovány společně. Další testy odhalily, že když oba programy běžely společně, program, který byl spuštěn jako první, nebyl vždy schopen zachytit globální klíčové události, ale aplikace spuštěná později fungovala dobře.

Záhadu jsem vyřešil po přečtení dokumentace společnosti Microsoft. V kódu, který registruje samotný program jako posluchače zavěšení, chyběl CallNextHookEx () volání vyžadované rámcem zavěšení. Dokumentace čte, že každý posluchač háku je přidán do řetězce háku v pořadí spuštění; poslední spuštěný posluchač bude nahoře. Události se odesílají prvnímu posluchači v řetězci. Aby všichni posluchači mohli přijímat události, musí si každý posluchač vytvořit CallNextHookEx () volání k přenosu událostí posluchači vedle něj. Pokud k tomu některý posluchač zapomene, následující posluchači události nedostanou; v důsledku toho nebudou jejich navržené funkce fungovat. To byl přesný důvod, proč můj druhý program fungoval, ale první ne!

Záhada byla vyřešena, ale byl jsem nešťastný z rámce háku. Nejprve to vyžaduje, abych si "pamatoval" na vložení CallNextHookEx () volání metody do mého kódu. Zadruhé, můj program mohl deaktivovat jiné programy a naopak. Proč se to stalo? Protože Microsoft implementoval globální hákový rámec přesně podle klasického vzoru Chain of Responsibility (CoR) definovaného Gang of Four (GoF).

V tomto článku diskutuji mezeru v provádění VR navrženou GoF a navrhuji její řešení. To vám může pomoci vyhnout se stejným problémům při vytváření vlastního rámce VR.

Classic VR

Klasický vzor VR definovaný GoF v Designové vzory:

„Vyvarujte se propojení odesílatele požadavku s jeho přijímačem tím, že dáte více než jednomu objektu šanci tento požadavek vyřídit.

Obrázek 1 ilustruje diagram tříd.

Typická struktura objektu může vypadat jako na obrázku 2.

Z výše uvedených ilustrací můžeme shrnout, že:

  • Více obslužných rutin může být schopen zpracovat požadavek
  • Pouze jeden obslužný program ve skutečnosti zpracovává požadavek
  • Žadatel zná pouze odkaz na jednoho obsluhujícího pracovníka
  • Žadatel neví, kolik obslužných programů dokáže jeho požadavek zpracovat
  • Žadatel neví, který obslužný program vyřídil jeho požadavek
  • Žadatel nemá nad manipulátory žádnou kontrolu
  • Obslužné rutiny lze určit dynamicky
  • Změna seznamu obslužných programů neovlivní kód žadatele

Níže uvedené segmenty kódu ukazují rozdíl mezi kódem žadatele, který používá VR, a kódem žadatele, který jej nepoužívá.

Kód žadatele, který nepoužívá VR:

 handlers = getHandlers (); for (int i = 0; i <handlers.length; i ++) {handlers [i] .handle (request); if (handlers [i] .handled ()) break; } 

Kód žadatele, který používá VR:

 getChain (). handle (požadavek); 

Od této chvíle vše vypadá perfektně. Pojďme se ale podívat na implementaci, kterou GoF navrhuje pro klasický VR:

 public class Handler {private Handler nástupce; public Handler (HelpHandler s) {nástupce = s; } public handle (ARequest request) {if (successor! = null) successor.handle (request); }} public class AHandler extends Handler {public handle (ARequest request) {if (someCondition) // Handling: do something else super.handle (request); }} 

Základní třída má metodu, Rukojeť(), který volá jeho nástupce, další uzel v řetězci, aby zpracoval požadavek. Podtřídy přepíšou tuto metodu a rozhodnou se, zda umožní řetězu pokračovat. Pokud uzel zpracovává požadavek, podtřída nebude volat super.handle () který zavolá nástupce a řetěz uspěje a zastaví se. Pokud uzel požadavek nezpracovává, podtřída musí volání super.handle () aby se řetěz roztočil, nebo se řetěz zastaví a selže. Protože toto pravidlo není vynuceno v základní třídě, jeho dodržování není zaručeno. Když vývojáři zapomenou uskutečnit hovor v podtřídách, řetězec selže. Zde je zásadní chyba rozhodování o provedení řetězce, které není předmětem podtříd, je spojeno se zpracováním požadavků v podtřídách. To porušuje princip objektově orientovaného designu: objekt by měl mít na mysli pouze své vlastní podnikání. Tím, že necháte podtřídu učinit rozhodnutí, vnesete do ní další zátěž a možnost chyby.

Mezera globálního rámce zavěšení systému Microsoft Windows a rámce filtru servletů Java

Implementace globálního rámce systému Microsoft Windows je stejná jako klasická implementace VR navržená GoF. Rámec závisí na jednotlivých hákových posluchačích, aby se CallNextHookEx () zavolat a předat událost prostřednictvím řetězce. Předpokládá, že si vývojáři pravidlo vždy zapamatují a nikdy nezapomenou uskutečnit hovor. Globální háček událostí není od přírody klasický VR. Událost musí být doručena všem posluchačům v řetězci bez ohledu na to, zda ji posluchač již zpracovává. Takže CallNextHookEx () volání se zdá být prací základní třídy, nikoli jednotlivých posluchačů. Nechat jednotlivým posluchačům uskutečnit hovor nedělá nic dobrého a zavádí možnost náhodného zastavení řetězce.

Rámec filtru servletů Java dělá podobnou chybu jako globální zavěšení systému Microsoft Windows. Z toho vyplývá přesně implementace navržená GoF. Každý filtr rozhoduje o tom, zda zavolá nebo zastaví řetěz voláním nebo nevolaním doFilter () na dalším filtru. Pravidlo je vynuceno prostřednictvím javax.servlet.Filter # doFilter () dokumentace:

"4. a) Buď vyvolat další entitu v řetězci pomocí FiltrŘetěz objekt (chain.doFilter ()), 4. b) nebo nepředá dvojici požadavek / odpověď další entitě v řetězci filtrů, aby zablokovala zpracování požadavku. “

Pokud jeden filtr zapomene udělat chain.doFilter () zavolat, kdy má, deaktivuje ostatní filtry v řetězci. Pokud jeden filtr vytvoří chain.doFilter () zavolat, kdy by mělo ne mít, vyvolá další filtry v řetězci.

Řešení

Pravidla vzoru nebo rámce by měla být vynucena prostřednictvím rozhraní, nikoli dokumentace. Počítání s tím, že si vývojáři pravidlo zapamatují, nemusí vždy fungovat. Řešením je oddělit rozhodování o provedení řetězce a zpracování požadavků přesunutím další() volání základní třídy. Nechte základní třídu učinit rozhodnutí a podtřídy nechejte zpracovat pouze požadavek. Když se podtřídy vyhýbají rozhodování, mohou se plně soustředit na své vlastní podnikání, čímž se vyhne výše popsané chybě.

Classic VR: Odesílejte požadavek řetězcem, dokud jeden uzel požadavek nevyřeší

Toto je implementace, kterou navrhuji pro klasický VR:

 / ** * Classic CoR, tj. Požadavek zpracovává pouze jeden ze zpracovatelů v řetězci. * / public abstract class ClassicChain {/ ** * Další uzel v řetězci. * / další soukromý ClassicChain; public ClassicChain (ClassicChain nextNode) {next = nextNode; } / ** * Počáteční bod řetězce, volaný klientem nebo předuzlem. * Volejte handle () v tomto uzlu a rozhodněte se, zda chcete pokračovat v řetězci. Pokud další uzel nemá hodnotu null a * tento uzel požadavek nezpracoval, zavoláním start () v dalším uzlu zpracovejte požadavek. * @param request the request parameter * / public final void start (ARequest request) {boolean handledByThisNode = this.handle (request); if (next! = null &&! handledByThisNode) next.start (požadavek); } / ** * Volano start (). * @param request the request parameter * @return a boolean indicates whether this node handled the request * / protected abstract boolean handle (ARequest request); } veřejná třída AClassicChain rozšiřuje ClassicChain {/ ** * Volá se start (). * @param request the request parameter * @return a boolean indicates whether this node handled the request * / protected boolean handle (ARequest request) {boolean handledByThisNode = false; if (someCondition) {// Do handling handledByThisNode = true; } návrat handledByThisNode; }} 

Implementace odděluje logiku rozhodování o provedení řetězce a zpracování požadavků rozdělením do dvou samostatných metod. Metoda Start() činí rozhodnutí o provedení řetězce a Rukojeť() vyřídí požadavek. Metoda Start() je výchozím bodem provedení řetězce. To volá Rukojeť() na tomto uzlu a rozhodne, zda postoupit řetězec do dalšího uzlu na základě toho, zda tento uzel zpracovává požadavek a zda je uzel vedle něj. Pokud aktuální uzel nezpracovává požadavek a další uzel není null, aktuální uzel Start() metoda posune řetězec voláním Start() na dalším uzlu nebo zastaví řetěz o ne povolání Start() na dalším uzlu. Metoda Rukojeť() v základní třídě je deklarován abstraktní a neposkytuje žádnou výchozí logiku zpracování, která je specifická pro podtřídu a nemá nic společného s rozhodováním o provedení řetězce. Podtřídy přepíšou tuto metodu a vrátí logickou hodnotu označující, zda podtřídy zpracovávají požadavek samy. Všimněte si, že logická hodnota vrácená podtřídou informuje Start() v základní třídě, zda podtřída zpracovala požadavek, ne zda pokračovat v řetězci. Rozhodnutí, zda pokračovat v řetězci, je zcela na rozhodnutí základní třídy Start() metoda. Podtřídy nemohou změnit logiku definovanou v Start() protože Start() je prohlášen za konečný.

V této implementaci zůstává okno příležitostí, které umožňuje podtřídám pokazit řetězec vrácením nezamýšlené logické hodnoty. Tento design je však mnohem lepší než stará verze, protože podpis metody vynucuje hodnotu vrácenou metodou; chyba je zachycena v době kompilace. Vývojáři již nemusí pamatovat na to, aby vytvořili další() zavolat nebo vrátit booleovskou hodnotu v jejich kódu.

Neklasický VR 1: Odesílejte požadavek řetězcem, dokud se jeden uzel nebude chtít zastavit

Tento typ implementace VR je mírnou variací oproti klasickému vzoru VR. Řetěz se zastaví ne proto, že jeden uzel zpracoval požadavek, ale proto, že jeden uzel chce zastavit. V takovém případě platí i zde klasická implementace VR s mírnou koncepční změnou: logická vlajka vrácená Rukojeť() metoda neoznačuje, zda byl požadavek zpracován. Spíše řekne základní třídě, zda by měl být řetězec zastaven. Rámec filtru servletů zapadá do této kategorie. Místo vynucení volání jednotlivých filtrů chain.doFilter ()Nová implementace vynutí, aby individuální filtr vrátil logickou hodnotu, která je kontraktována rozhraním, což vývojář nikdy nezapomene nebo nezmešká.

Neklasický CoR 2: Bez ohledu na vyřizování požadavků odešlete požadavek všem zpracovatelům

U tohoto typu provádění VR Rukojeť() nemusí vracet booleovský indikátor, protože požadavek je odeslán všem zpracovatelům bez ohledu na to. Tato implementace je jednodušší. Vzhledem k tomu, že globální zaváděcí rámec Microsoft Windows přirozeně patří k tomuto typu VR, měla by následující mezera opravit jeho mezeru:

 / ** * Neklasické VR 2, tj. Požadavek je odeslán všem zpracovatelům bez ohledu na zpracování. * / public abstract class NonClassicChain2 {/ ** * Další uzel v řetězci. * / private NonClassicChain2 další; public NonClassicChain2 (NonClassicChain2 nextNode) {next = nextNode; } / ** * Počáteční bod řetězce, volaný klientem nebo předuzlem. * Volejte handle () na tomto uzlu, pak volejte start () na dalším uzlu, pokud existuje další uzel. * @param request the request parameter * / public final void start (ARequest request) {this.handle (request); if (next! = null) next.start (požadavek); } / ** * Volano start (). * @param request the request parameter * / protected abstract void handle (ARequest request); } public class ANonClassicChain2 extends NonClassicChain2 {/ ** * Called by start (). * @param request the request parameter * / protected void handle (ARequest request) {// Do handling. }} 

Příklady

V této části vám ukážu dva příklady řetězců, které používají implementaci pro neklasický CoR 2 popsaný výše.

Příklad 1