Programovanie

Nástrahy a vylepšenia v rámci reťazca zodpovednosti

Nedávno som napísal dva programy Java (pre operačný systém Microsoft Windows), ktoré musia zachytávať globálne udalosti klávesnice generované inými aplikáciami bežiacimi súčasne na tej istej pracovnej ploche. Spoločnosť Microsoft poskytuje spôsob, ako to dosiahnuť, a to tak, že programy zaregistruje ako globálneho poslucháča zaháknutia klávesnice. Kódovanie netrvalo dlho, ale ladenie áno. Zdá sa, že tieto dva programy fungovali dobre, keď sa testovali samostatne, ale zlyhali, keď sa testovali spoločne. Ďalšie testy odhalili, že keď dva programy bežali spolu, program, ktorý sa spustil ako prvý, nebol vždy schopný zachytiť globálne kľúčové udalosti, ale neskôr spustená aplikácia fungovala dobre.

Záhadu som vyriešil po prečítaní dokumentácie spoločnosti Microsoft. V kóde, ktorý zaregistroval samotný program ako poslucháča háku, chýbala položka CallNextHookEx () hovor požadovaný rámcom háku. V dokumentácii sa uvádza, že každý poslucháč háku je pridaný do reťaze hákov v poradí spustenia; posledný začatý poslucháč bude na vrchu. Udalosti sa odosielajú prvému poslucháčovi v reťazci. Aby každý poslucháč mohol prijímať udalosti, musí každý poslucháč urobiť CallNextHookEx () volanie na prenos udalostí k poslucháčovi vedľa neho. Ak to ktorýkoľvek poslucháč zabudne urobiť, následní poslucháči udalosti nedostanú; vo výsledku nebudú fungovať ich navrhnuté funkcie. To bol presný dôvod, prečo môj druhý program fungoval, ale prvý nie!

Záhada bola vyriešená, ale bol som nešťastný z rámca háku. Najprv si vyžaduje, aby som „pamätal“ na vloženie CallNextHookEx () metóda volania do môjho kódu. Po druhé, môj program by mohol zakázať iné programy a naopak. Prečo sa to stalo? Pretože Microsoft implementoval globálny háčikový rámec presne podľa klasického vzoru Chain of Responsibility (CoR) definovaného Gangom štyroch (GoF).

V tomto článku sa zaoberám medzerou v implementácii VR navrhnutou GoF a navrhujem jej riešenie. To vám môže pomôcť vyhnúť sa rovnakým problémom pri vytváraní vlastného rámca VR.

Klasický VR

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

"Vyvarujte sa spájania odosielateľa žiadosti s jej príjemcom tým, že dáte šancu vybaviť viac než jednému objektu. Prijímané objekty pripútajte reťazou a prechádzajte požiadavkou po reťazci, kým ju objekt nespracuje."

Obrázok 1 zobrazuje diagram tried.

Typická štruktúra objektu môže vyzerať ako obrázok 2.

Z vyššie uvedených ilustrácií môžeme zhrnúť, že:

  • Požiadavka môže byť schopná spracovať viac obslužných programov
  • Iba jeden obslužný program v skutočnosti vybavuje požiadavku
  • Žiadateľ pozná iba odkaz na jedného spracovateľa
  • Žiadateľ nevie, koľko spracovateľov je schopných vybaviť jeho požiadavku
  • Žiadateľ nevie, ktorý obslužný program vybavil jeho požiadavku
  • Žiadateľ nemá kontrolu nad manipulačnými prostriedkami
  • Obslužné programy sa dali špecifikovať dynamicky
  • Zmena zoznamu obslužných programov neovplyvní kód žiadateľa

Nasledujúce segmenty kódu demonštrujú rozdiel medzi kódom žiadateľa, ktorý používa Cor, a kódom žiadateľa, ktorý ho nepoužíva.

Kód žiadateľa, ktorý nepoužíva VR:

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

Kód žiadateľa, ktorý používa VR:

 getChain (). handle (žiadosť); 

Odteraz sa všetko zdá byť dokonalé. Pozrime sa však na implementáciu, ktorú GoF navrhuje pre klasický VR:

 public class Handler {nástupca súkromného handlera; public Handler (HelpHandler s) {nástupca = 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á trieda má metódu, rukoväť (), ktorý volá svojho nástupcu, ďalší uzol v reťazci, aby vybavil žiadosť. Podtriedy prepíšu túto metódu a rozhodnú sa, či umožnia reťazi pokračovať. Ak uzol spracuje požiadavku, podtrieda nebude volať super.handle () ktorý zavolá nástupcu a reťaz uspeje a zastaví sa. Ak uzol nezvláda požiadavku, podtrieda musieť hovor super.handle () aby sa reťaz udržala v chode, alebo sa reťaz zastaví a zlyhá. Pretože toto pravidlo nie je vynucované v základnej triede, nie je zaručené jeho dodržiavanie. Keď vývojári zabudnú uskutočniť hovor v podtriedach, reťazec zlyhá. Zásadnou chybou je tu rozhodovanie o vykonaní reťazca, ktoré nie je predmetom podtried, je spojené s vybavovaním požiadaviek v podtriedach. To porušuje zásadu objektovo-orientovaného dizajnu: objektu by malo vadiť iba jeho vlastné podnikanie. Tým, že necháte podtriedu rozhodnúť, predstavujete pre ňu ďalšiu záťaž a možnosť chyby.

Medzera v globálnom rámci operačného systému Microsoft Windows a v rámci filtrovacieho servletu Java

Implementácia globálneho rámca systému Microsoft Windows hook je rovnaká ako klasická implementácia VR navrhovaná GoF. Rámec závisí od konkrétneho poslucháča háku, ktorý si vytvorí CallNextHookEx () zavolajte a odošlite udalosť reťazou. Predpokladá, že si vývojári budú pravidlo pamätať vždy a nikdy nezabudnú uskutočniť hovor. Globálny reťazec háčikov udalostí nie je svojou povahou klasický VR. Udalosť musí byť doručená všetkým poslucháčom v reťazci bez ohľadu na to, či ju poslucháč už ovláda. Takže CallNextHookEx () Zdá sa, že hovor je prácou základnej triedy, nie individuálnych poslucháčov. Umožnenie hovoru jednotlivým poslucháčom nie je dobré a predstavuje možnosť náhodného zastavenia reťaze.

Rámec filtra servletov Java robí podobnú chybu ako globálny háčik Microsoft Windows. Sleduje presne implementáciu navrhnutú GoF. Každý filter rozhoduje o tom, či reťaz pretočí, alebo nezastaví doFilter () na nasledujúcom filtri. Pravidlo sa presadzuje prostredníctvom javax.servlet.Filter # doFilter () dokumentácia:

"4. a) Buď vyvolajte nasledujúcu entitu v reťazci pomocou FilterChain objekt (chain.doFilter ()), 4. b) alebo neodovzdá pár požiadavka / odpoveď ďalšiemu subjektu v reťazci filtra, aby zablokoval spracovanie žiadosti. “

Ak jeden filter zabudne urobiť chain.doFilter () zavolajte, keď má mať, deaktivuje sa tak ďalšie filtre v reťazci. Ak jeden filter vytvára chain.doFilter () volať, kedy by malo nie mať, vyvolá ďalšie filtre v reťazci.

Riešenie

Pravidlá vzoru alebo rámca by sa mali presadzovať prostredníctvom rozhraní, nie dokumentácie. Spoliehanie sa na to, že si pravidlo pamätajú vývojári, nemusí vždy fungovať. Riešením je oddelenie rozhodovania o vykonaní reťazca a vybavovania požiadaviek presunutím Ďalšie() volanie do základnej triedy. Nechajte rozhodnutie základnej triedy a nechajte podtriedy, aby vybavili iba požiadavku. Ak sa podtriedy vyhýbajú rozhodovaniu, môžu sa úplne sústrediť na svoje vlastné podnikanie, čím sa vyhnú chybe opísanej vyššie.

Classic VR: Pošlite žiadosť reťazcom, kým jeden uzol nespracuje žiadosť

Toto je implementácia, ktorú navrhujem pre klasický VR:

 / ** * Classic CoR, t. J. Žiadosť vybavuje iba jeden zo spracovateľov v reťazci. * / public abstract class ClassicChain {/ ** * Ďalší uzol v reťazci. * / ďalej súkromný ClassicChain; public ClassicChain (ClassicChain nextNode) {next = nextNode; } / ** * Počiatočný bod reťazca, volaný klientom alebo predbežným uzlom. * Zavolajte handle () na tomto uzle a rozhodnite sa, či chcete v reťazci pokračovať. Ak nasledujúci uzol nemá hodnotu null a * tento uzol požiadavku nespracoval, na vybavenie žiadosti zavolajte start () na ďalšom uzle. * @param request the request parameter * / public final void start (ARequest request) {boolean handledByThisNode = this.handle (request); if (next! = null &&! handledByThisNode) next.start (požiadavka); } / ** * Volané začiatkom (). * @param request the request parameter * @return a boolean indicates whether this node handled the request * / protected abstract boolean handle (ARequest request); } public class AClassicChain extends ClassicChain {/ ** * Called by 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; }} 

Implementácia oddeľuje logiku rozhodovania o vykonaní reťazca a vybavovanie požiadaviek rozdelením do dvoch samostatných metód. Metóda štart () robí rozhodnutie o vykonaní reťazca a rukoväť () vybavuje žiadosť. Metóda štart () je východiskovým bodom vykonania reťazca. Volá rukoväť () na tomto uzle a rozhodne, či postúpi reťazec do ďalšieho uzla na základe toho, či tento uzol vybavuje požiadavku a či je uzol vedľa neho. Ak aktuálny uzol nespracuje požiadavku a ďalší uzol nemá hodnotu null, bude mať aktuálny uzol štart () metóda posúva reťazec volaním štart () na nasledujúcom uzle alebo zastaví reťaz o nie volanie štart () na nasledujúcom uzle. Metóda rukoväť () v základnej triede je vyhlásený za abstraktný a neposkytuje žiadnu predvolenú manipulačnú logiku, ktorá je špecifická pre podtriedu a nemá nič spoločné s rozhodovaním o vykonaní reťazca. Podtriedy prepíšu túto metódu a vrátia boolovskú hodnotu označujúcu, či podtriedy zvládajú požiadavku samy. Všimnite si, že logická hodnota vrátená podtriedou informuje štart () v základnej triede, či podtrieda vybavila požiadavku, nie či má pokračovať v reťazci. Rozhodnutie, či pokračovať v reťazci, je úplne na základnej triede štart () metóda. Podtriedy nemôžu meniť logiku definovanú v štart () pretože štart () je vyhlásený za konečný.

V tejto implementácii zostáva priestor príležitosti, ktorý umožňuje podtriedam pokaziť reťazec vrátením nechcenej booleovskej hodnoty. Tento dizajn je však oveľa lepší ako stará verzia, pretože podpis metódy vynucuje hodnotu vrátenú metódou; chyba sa zachytí v čase kompilácie. Od vývojárov sa už nevyžaduje, aby pamätali na to, že musia buď vytvoriť Ďalšie() zavolajte alebo vráťte booleovskú hodnotu v ich kóde.

Neklasický VR 1: Pošlite žiadosť cez reťazec, kým sa jeden uzol nebude chcieť zastaviť

Tento typ implementácie VR je miernou obmenou oproti klasickému vzoru VR. Reťazec sa zastaví nie preto, že jeden uzol vybavil požiadavku, ale preto, že jeden uzol sa chce zastaviť. V takom prípade platí aj tu klasická implementácia VR s miernou koncepčnou zmenou: logická vlajka vrátená rukoväť () metóda nenaznačuje, či bola požiadavka vybavená. Namiesto toho hovorí základnej triede, či má byť reťaz zastavená. Rámec servletového filtra zapadá do tejto kategórie. Namiesto toho, aby ste nútili jednotlivé filtre volať chain.doFilter (), nová implementácia vynúti, aby individuálny filter vrátil logickú hodnotu, ktorá je kontrahovaná rozhraním, čo vývojár nikdy nezabudne alebo nezmešká.

Neklasický CoR 2: Bez ohľadu na vybavenie žiadosti pošlite žiadosť všetkým spracovateľom

Pre tento typ implementácie VR rukoväť () nemusí vracať booleovský indikátor, pretože požiadavka sa odošle všetkým spracovateľom bez ohľadu na to. Táto implementácia je jednoduchšia. Pretože globálny háčikový rámec Microsoft Windows prirodzene patrí k tomuto typu VR, mala by medzeru napraviť nasledujúca implementácia:

 / ** * Neklasické VR 2, t. J. Žiadosť sa posiela všetkým spracovateľom bez ohľadu na vybavenie. * / public abstract class NonClassicChain2 {/ ** * Nasledujúci uzol v reťazci. * / private NonClassicChain2 ďalej; public NonClassicChain2 (NonClassicChain2 nextNode) {next = nextNode; } / ** * Počiatočný bod reťazca, volaný klientom alebo predbežným uzlom. * Volajte handle () na tomto uzle, potom zavolajte start () na ďalšom uzle, ak existuje ďalší uzol. * @param request the request parameter * / public final void start (ARequest request) {this.handle (request); if (next! = null) next.start (požiadavka); } / ** * Volané začiatkom (). * @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. }} 

Príklady

V tejto časti vám ukážem dva reťazové príklady, ktoré využívajú implementáciu pre neklasický CoR 2 popísanú vyššie.

Príklad 1

$config[zx-auto] not found$config[zx-overlay] not found