Ohjelmointi

Chain of Responsibility -kuvion sudenkuoppia ja parannuksia

Kirjoitin äskettäin kaksi Java-ohjelmaa (Microsoft Windows -käyttöjärjestelmälle), joiden on siepattava maailmanlaajuiset näppäimistötapahtumat, jotka muut sovellukset tuottavat samanaikaisesti samalla työpöydällä. Microsoft tarjoaa tavan tehdä se rekisteröimällä ohjelmat globaaliksi näppäimistön koukku kuuntelijaksi. Koodaaminen ei kestänyt kauan, mutta virheenkorjaus. Nämä kaksi ohjelmaa näyttivät toimivan hyvin, kun ne testattiin erikseen, mutta epäonnistuivat, kun ne testattiin yhdessä. Lisätestit paljastivat, että kun nämä kaksi ohjelmaa juoksivat yhdessä, ensin käynnistetty ohjelma ei aina pystynyt saamaan maailmanlaajuisia avaintapahtumia, mutta myöhemmin käynnistetty sovellus toimi hienosti.

Korjasin mysteerin lukemalla Microsoftin ohjeet. Koodista, joka rekisteröi ohjelman itse koukkukuuntelijaksi, puuttui CallNextHookEx () koukun kehyksen edellyttämä puhelu. Asiakirjoissa lukee, että jokainen koukku kuuntelija lisätään koukku ketjuun käynnistysjärjestyksessä; viimeinen aloitettu kuuntelija on huipulla. Tapahtumat lähetetään ketjun ensimmäiselle kuuntelijalle. Jotta kaikki kuuntelijat voivat vastaanottaa tapahtumia, jokaisen kuuntelijan on tehtävä CallNextHookEx () kutsu välittää tapahtumat sen vieressä olevalle kuuntelijalle. Jos joku kuuntelija unohtaa tehdä niin, seuraavat kuuntelijat eivät saa tapahtumia; seurauksena niiden suunnitellut toiminnot eivät toimi. Se oli tarkka syy miksi toinen ohjelma toimi, mutta ensimmäinen ei!

Mysteeri ratkaistiin, mutta olin tyytymätön koukkuihin. Ensinnäkin se vaatii minua "muistamaan" lisäämään CallNextHookEx () menetelmä kutsu koodiini. Toiseksi, ohjelmani voisi poistaa muut ohjelmat käytöstä ja päinvastoin. Miksi näin tapahtuu? Koska Microsoft otti globaalin koukkujärjestelmän käyttöön tarkalleen neljän ryhmän (GoF) määrittelemää perinteistä vastuullisuusketjun (CoR) mallia.

Tässä artikkelissa käsittelen GoF: n ehdottamaa AK: n toteuttamisen porsaanreikää ja ehdotan ratkaisua siihen. Tämä voi auttaa sinua välttämään saman ongelman, kun luot oman alueellisen komitean kehyksen.

Klassinen AK

GoF: n määrittelemä klassinen alueiden komitean malli Suunnittelumalleja:

"Vältä pyynnön lähettäjän liittämistä vastaanottimeen antamalla useammalle kuin yhdelle esineelle mahdollisuus käsitellä pyyntöä. Ketjua vastaanottavat kohteet ja välitä pyyntö ketjua pitkin, kunnes esine käsittelee sitä."

Kuva 1 kuvaa luokkakaavion.

Tyypillinen objektirakenne saattaa näyttää kuvalta 2.

Yllä olevista kuvista voimme tiivistää, että:

  • Useat käsittelijät voivat pystyä käsittelemään pyynnön
  • Ainoastaan ​​yksi käsittelijä käsittelee pyynnön
  • Pyytäjä tietää vain viittauksen yhteen käsittelijään
  • Pyynnön esittäjä ei tiedä kuinka monta käsittelijää pystyy käsittelemään pyynnön
  • Pyynnön esittäjä ei tiedä, mikä käsittelijä käsitteli pyyntönsä
  • Pyynnön esittäjällä ei ole valvontaa käsittelijöistä
  • Käsittelijät voitaisiin määrittää dynaamisesti
  • Käsittelijöiden luettelon muuttaminen ei vaikuta pyynnön tekijän koodiin

Alla olevat koodisegmentit osoittavat eron pyynnön tekijän koodissa, joka käyttää AK: ta, ja pyynnön tekijän koodissa, joka ei käytä.

Pyyntökoodi, joka ei käytä AK: ta:

 käsittelijät = getHandlers (); for (int i = 0; i <käsittelijät.pituus; i ++) {käsittelijät [i] .kahva (pyyntö); jos (käsittelijät [i] .käsitelty ()) rikkoutuvat; } 

Pyyntökoodi, joka käyttää AK: ta:

 getChain (). kahva (pyyntö); 

Tällä hetkellä kaikki näyttää täydelliseltä. Mutta katsotaanpa GoF: n ehdotusta klassisen AK: n toteuttamiseksi:

 julkisen luokan käsittelijä {yksityinen käsittelijän seuraaja; julkinen käsittelijä (HelpHandler s) {seuraaja = s; } julkinen kahva (ARequest-pyyntö) {if (seuraaja! = null) seuraaja.kahva (pyyntö); }} julkinen luokka AHandler laajentaa Handleria {julkinen kahva (ARequest-pyyntö) {if (someCondition) // Käsittely: tee jotain muuta super.handle (pyyntö); }} 

Perusluokalla on menetelmä, kahva(), joka kutsuu seuraajansa, ketjun seuraavan solmun, käsittelemään pyynnön. Alaluokat ohittavat tämän menetelmän ja päättävät, sallitaanko ketjun edetä. Jos solmu käsittelee pyynnön, alaluokka ei soita super.kahva () joka kutsuu seuraajaa, ja ketju onnistuu ja pysähtyy. Jos solmu ei käsittele pyyntöä, alaluokka on pakko puhelu super.kahva () ketju pyörii tai ketju pysähtyy ja epäonnistuu. Koska tätä sääntöä ei noudateta perusluokassa, sen noudattamista ei taata. Kun kehittäjät unohtavat soittamisen alaluokissa, ketju epäonnistuu. Perusvirhe tässä on se ketjun toteuttamista koskeva päätöksenteko, joka ei ole alaluokkien liiketoiminta, yhdistetään pyyntöjen käsittelyyn alaluokissa. Se rikkoo olio-suuntautuneen suunnittelun periaatetta: esineen tulisi pitää mielessä vain oma liiketoimintansa. Antamalla alaluokan tehdä päätös tuo sinulle ylimääräistä taakkaa ja mahdollisuuden virheitä.

Microsoft Windowsin globaalin koukkujärjestelmän ja Java-palvelinsuodattimen kehyksen reikä

Microsoft Windowsin globaalin koukkujärjestelmän toteutus on sama kuin GoF: n ehdottama perinteinen AK: n toteutus. Kehys riippuu yksittäisistä koukkujen kuuntelijoista CallNextHookEx () soita ja välitä tapahtuma ketjun kautta. Se olettaa, että kehittäjät muistaa aina säännön eivätkä koskaan unohda soittamista. Luonnostaan ​​maailmanlaajuinen tapahtumakoukku ei ole AK: n klassinen. Tapahtuma on toimitettava kaikille ketjun kuuntelijoille riippumatta siitä, käsitteleekö kuuntelija jo sitä. Joten CallNextHookEx () puhelu näyttää olevan perusluokan, ei yksittäisten kuuntelijoiden tehtävä. Yksittäisten kuuntelijoiden antaminen soittoon ei tee mitään hyvää ja tarjoaa mahdollisuuden ketjun pysäyttämiseen vahingossa.

Java-servlet-suodatinkehys tekee samanlaisen virheen kuin Microsoft Windowsin globaali koukku. Se seuraa tarkasti GoF: n ehdottamaa toteutusta. Kukin suodatin päättää ketjun pyörittämisen vai pysäyttämisen soittamalla vai ei doFilter () seuraavalla suodattimella. Sääntö pannaan täytäntöön javax.servlet.Filter # doFilter () dokumentointi:

"4. a) Käynnistä ketjun seuraava kokonaisuus käyttämällä SuodatinKetju esine (chain.doFilter ()), 4. b) tai ei välitä pyyntö / vastaus-paria seuraavalle suodatinketjun yksikölle estämään pyynnön käsittely. "

Jos yksi suodatin unohtaa tehdä chain.doFilter () Soita, kun sen pitäisi olla, se poistaa ketjun muut suodattimet käytöstä. Jos yksi suodatin tekee chain.doFilter () soita, kun sen pitäisi ei on, se käyttää muita suodattimia ketjussa.

Ratkaisu

Kaavan tai kehyksen säännöt tulisi panna täytäntöön käyttöliittymien, ei dokumentaation kautta. Laskeminen kehittäjien muistamaan sääntö ei aina toimi. Ratkaisu on irrottaa ketjun suorittamista koskeva päätöksenteko ja pyyntöjen käsittely siirtämällä Seuraava() kutsu perusluokkaan. Anna pääluokan tehdä päätös ja anna alakategorioiden hoitaa vain pyyntö. Ohittamalla päätöksenteon alaluokat voivat keskittyä kokonaan omaan liiketoimintaansa välttäen edellä kuvatun virheen.

Perinteinen CoR: Lähetä pyyntö ketjun kautta, kunnes yksi solmu käsittelee pyynnön

Tätä ehdotan perinteiselle AK: lle:

 / ** * Classic CoR, eli pyynnön käsittelee vain yksi ketjun käsittelijöistä. * / public abstract class ClassicChain {/ ** * Ketjun seuraava solmu. * / seuraava yksityinen ClassicChain; public ClassicChain (ClassicChain nextNode) {seuraava = seuraavaNode; } / ** * Ketjun aloituskohta, asiakas tai esisolmu. * Kutsu tämän solmun kahva () ja päätä, jatkaako ketju. Jos seuraava solmu ei ole nolla ja * tämä solmu ei käsitellyt pyyntöä, kutsu seuraavan solmun aloitus () käsittelemään pyyntöä. * @param pyydä pyyntöparametri * / public final void start (ARequest request) {boolean handledByThisNode = this.handle (request); if (seuraava! = null &&! handledByThisNode) seuraava.aloitus (pyyntö); } / ** * Soitettu alusta (). * @param pyytää pyyntöparametri * @return boolean ilmaisee, käsittikö tämä solmu pyyntöä * / suojattu abstrakti looginen kahva (ARequest-pyyntö); } julkisen luokan AClassicChain laajentaa ClassicChainia {/ ** * Käynnistää start (). + if (someCondition) {// Käsittele handledByThisNode = true; } return handledByThisNode; }} 

Toteutus erottaa ketjun suorituspäätöksen logiikan ja pyynnön käsittelyn jakamalla ne kahteen erilliseen menetelmään. Menetelmä alkaa() tekee ketjun toteuttamispäätöksen ja kahva() käsittelee pyynnön. Menetelmä alkaa() on ketjun suorituksen lähtökohta. Se kutsuu kahva() tällä solmulla ja päättää ketjun siirtämisen seuraavalle solmulle sen perusteella, käsitteleekö tämä solmu pyyntöä ja onko solmu sen vieressä. Jos nykyinen solmu ei käsittele pyyntöä ja seuraava solmu ei ole nolla, nykyisen solmun alkaa() menetelmä etenee ketjua soittamalla alkaa() seuraavassa solmussa tai pysäyttää ketjun ei kutsumus alkaa() seuraavassa solmussa. Menetelmä kahva() perusluokassa julistetaan abstraktiksi, eikä siinä ole oletuskäsittelylogiikkaa, joka on alaluokakohtainen eikä sillä ole mitään tekemistä ketjun suorituksen päätöksenteon kanssa. Alaluokat ohittavat tämän menetelmän ja palauttavat Boolen-arvon, joka osoittaa, käsittelevätkö alaluokat pyynnön itse. Huomaa, että alaluokan palauttama Boolean tieto kertoo alkaa() perusluokassa, onko alaluokka käsitellyt pyynnön, ei jatkaako ketjua. Päätös ketjun jatkamisesta on täysin perusluokan tehtävä alkaa() menetelmä. Alaluokat eivät voi muuttaa kohdassa määriteltyä logiikkaa alkaa() koska alkaa() julistetaan lopulliseksi.

Tässä toteutuksessa jää mahdollisuuksien ikkuna, jonka avulla alaluokat voivat sekoittaa ketjun palauttamalla tahattoman Boolen-arvon. Tämä malli on kuitenkin paljon parempi kuin vanha versio, koska metodin allekirjoitus pakottaa menetelmän palauttaman arvon; virhe havaitaan kääntöhetkellä. Kehittäjien ei enää tarvitse muistaa kumpaakaan tehdä Seuraava() soita tai palauta Boolen-arvo koodissaan.

Ei-klassinen CoR 1: Lähetä pyyntö ketjun läpi, kunnes yksi solmu haluaa pysähtyä

Tämäntyyppinen AK: n toteutus on pieni muunnelma AK: n klassisesta mallista. Ketju ei pysähdy, koska yksi solmu on käsitellyt pyynnön, vaan siksi, että yksi solmu haluaa pysähtyä. Siinä tapauksessa myös alueiden komitean klassinen toteutus pätee pienellä käsitteellisellä muutoksella: Boolean lippu, jonka kahva() menetelmä ei osoita, onko pyyntö käsitelty. Pikemminkin se kertoo perusluokalle, pitäisikö ketju pysäyttää. Servlet-suodatinkehys sopii tähän luokkaan. Sen sijaan, että pakottaisit yksittäisiä suodattimia soittamaan chain.doFilter (), uusi toteutus pakottaa yksittäisen suodattimen palauttamaan Boolean, jonka käyttöliittymä supistaa, mitä kehittäjä ei koskaan unohda tai missaa.

Ei-klassinen CoR 2: Pyyntöjen käsittelystä riippumatta lähetä pyyntö kaikille käsittelijöille

Tämäntyyppisessä AK: n toteutuksessa kahva() ei tarvitse palauttaa Boolen-indikaattoria, koska pyyntö lähetetään kaikille käsittelijöille riippumatta. Tämä toteutus on helpompaa. Koska Microsoft Windows: n maailmanlaajuinen koukkujärjestelmä kuuluu luonteeltaan tämäntyyppiseen alueiden komiteaan, seuraavan toteutuksen pitäisi korjata sen aukko:

 / ** * Ei-klassinen CoR 2, toisin sanoen pyyntö lähetetään kaikille käsittelijöille käsittelystä riippumatta. * / public abstract class NonClassicChain2 {/ ** * Ketjun seuraava solmu. * / seuraava yksityinen NonClassicChain2; public NonClassicChain2 (NonClassicChain2 nextNode) {seuraava = seuraavaNode; } / ** * Ketjun aloituskohta, asiakas tai esisolmu. * Tämän solmun soittokahva () ja sitten seuraava solmu, jos seuraava solmu on olemassa. * @param pyydä pyyntöparametri * / public final void start (ARequest request) {this.handle (request); if (seuraava! = null) seuraava. aloita (pyyntö); } / ** * Soitettu alusta (). * @param pyydä pyyntöparametri * / suojattu abstrakti void-kahva (ARequest-pyyntö); } julkinen luokka ANonClassicChain2 laajentaa NonClassicChain2 {/ ** * Käynnisti start (). * @param pyydä pyyntöparametri * / suojattu void-kahva (ARequest-pyyntö) {// Tee käsittely. }} 

Esimerkkejä

Tässä osassa esitän sinulle kaksi ketjuesimerkkiä, jotka käyttävät edellä kuvattua ei-klassisen CoR 2: n toteutusta.

Esimerkki 1