Programiranje

Pasti in izboljšave vzorca verige odgovornosti

Pred kratkim sem napisal dva programa Java (za operacijski sistem Microsoft Windows), ki morata ujeti globalne dogodke na tipkovnici, ki jih generirajo druge aplikacije, ki se istočasno izvajajo na istem namizju. Microsoft ponuja način, da to stori z registracijo programov kot globalnega poslušalca za kljuke s tipkovnico. Kodiranje ni trajalo dolgo, odpravljanje napak pa. Zdi se, da sta oba programa dobro delovala, če sta bila preizkušena ločeno, vendar sta bila neuspešna, ko sta bila preizkušena skupaj. Nadaljnji testi so pokazali, da program, ki se je najprej zagnal, vedno ni mogel ujeti ključnih globalnih dogodkov, vendar je kasneje zagnana aplikacija delovala povsem dobro.

Skrivnost sem razrešil po branju Microsoftove dokumentacije. Koda, ki program sam registrira kot poslušalca, je manjkala CallNextHookEx () klic, ki ga zahteva okvir kljuke. V dokumentaciji piše, da je vsak poslušalnik kavlja dodan v verigo kavlja po vrstnem redu zagona; zadnji začeti poslušalec bo na vrhu. Dogodki se pošljejo prvemu poslušalcu v verigi. Da lahko vsi poslušalci prejemajo dogodke, mora vsak poslušalec narediti CallNextHookEx () klic za posredovanje dogodkov poslušalcu ob njem. Če kateri od poslušalcev to pozabi, poznejši poslušalci ne bodo dobili dogodkov; posledično njihove načrtovane funkcije ne bodo delovale. To je bil natančen razlog, zakaj je moj drugi program deloval, prvi pa ne!

Skrivnost je bila rešena, vendar nisem bil zadovoljen z ogrodjem kljuke. Najprej moram "zapomniti", da vstavim CallNextHookEx () klic metode v mojo kodo. Drugič, moj program bi lahko onemogočil druge programe in obratno. Zakaj se to zgodi? Ker je Microsoft uvedel globalni okvir kljuk po natančno klasičnem vzorcu Verige odgovornosti (OR), ki ga je opredelila Gang of Four (GoF).

V tem članku obravnavam vrzel pri izvajanju OR, ki jo je predlagal GoF, in predlagam rešitev zanjo. To vam lahko pomaga, da se izognete isti težavi, ko ustvarite svoj okvir OR.

Klasični OR

Klasični vzorec OR, ki ga je v Vzorci oblikovanja:

"Izogibajte se povezovanju pošiljatelja zahteve s sprejemnikom, tako da dajete več predmetom priložnost, da zaprosilo obdela. Verižite prejemne predmete in prošnjo pošljite vzdolž verige, dokler predmet ne obdela."

Slika 1 prikazuje diagram razredov.

Tipična struktura objekta je lahko videti kot slika 2.

Iz zgornjih ilustracij lahko povzamemo, da:

  • Zahtevo lahko obravnava več upravljavcev
  • Zahtevo dejansko obravnava samo en upravljavec
  • Prosilec pozna le sklicevanje na enega vodnika
  • Prosilec ne ve, koliko upravljavcev lahko obravnava njegovo zahtevo
  • Prosilec ne ve, kateri uporabnik je obravnaval njegovo zahtevo
  • Vlagatelj nima nobenega nadzora nad upravljavci
  • Vodnike je bilo mogoče določiti dinamično
  • Sprememba seznama upravljavcev ne bo vplivala na kodo vlagatelja zahtevka

Spodnji segmenti kode prikazujejo razliko med kodo prosilca, ki uporablja CoR, in kodo vlagatelja zahtev, ki je ne.

Koda prosilca, ki ne uporablja OR:

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

Koda prosilca, ki uporablja OR:

 getChain (). handle (zahteva); 

Od zdaj se zdi vse popolno. Poglejmo pa izvedbo, ki jo GoF predlaga za klasični OR:

 vodnik javnega razreda {naslednik zasebnega vodnika; javni vodnik (HelpHandler s) {naslednik = s; } javni ročaj (zahteva ARequest) {if (naslednik! = null) naslednik.handle (zahteva); }} javni razred AHandler razširi Handler {javni ročaj (zahteva ARequest) {if (someCondition) // Ravnanje: naredite nekaj drugega super.handle (zahteva); }} 

Osnovni razred ima metodo, ročaj (), ki pokliče naslednika, naslednje vozlišče v verigi, da obravnava zahtevo. Podrazredi preglasijo to metodo in se odločijo, ali bodo verigi dovolili nadaljevanje. Če vozlišče obravnava zahtevo, podrazred ne bo poklical super.handle () ki pokliče naslednika in veriga uspe in se ustavi. Če vozlišče ne obravnava zahteve, podrazred mora pokličite super.handle () da veriga ostane valjana ali pa se veriga ustavi in ​​odpove. Ker v osnovnem razredu to pravilo ni uveljavljeno, njegova skladnost ni zagotovljena. Ko razvijalci pozabijo poklicati v podrazredih, veriga ne uspe. Temeljna napaka je v tem odločanje o izvrševanju verige, ki se ne ukvarja s podrazredi, je povezano z obdelavo zahtev v podrazredih. To krši načelo objektno usmerjenega oblikovanja: objekt naj se ukvarja samo s svojim poslom. Če dovolite, da podrazred odloča, mu predstavljate dodatno breme in možnost napak.

Luknja globalnega okvira kljukic Microsoft Windows in ogrodja filtrov Java servlet

Izvajanje globalnega ogrodja kljukov Microsoft Windows je enako kot klasična implementacija OR, ki jo predlaga GoF. Okvir je odvisen od posameznega poslušalca, ki ga posluša CallNextHookEx () pokličite in posredujte dogodek skozi verigo. Predpostavlja, da se bodo razvijalci vedno spomnili pravila in nikoli ne bodo pozabili poklicati. Po naravi globalna veriga dogodkov ni klasičen OR. Dogodek je treba dostaviti vsem poslušalcem v verigi, ne glede na to, ali poslušalec z njim že ravna. Torej CallNextHookEx () Zdi se, da je klic naloga osnovnega razreda in ne posameznih poslušalcev. Če dovolite, da posamezni poslušalci pokličejo, nič ne koristi in uvaja možnost nenamerne ustavitve verige.

Okvir filtra za programčke programa Java naredi podobno napako kot globalni kavelj Microsoft Windows. Natančno sledi izvedbi, ki jo predlaga GoF. Vsak filter se odloči, ali bo verigo zavrtel ali ustavil s klicem ali ne doFilter () na naslednjem filtru. Pravilo se uveljavlja skozi javax.servlet.Filter # doFilter () dokumentacija:

"4. a) Ali uporabite naslednjo entiteto v verigi z uporabo FilterChain predmet (chain.doFilter ()), 4. b) ali pa par zahtev / odzivov ne posreduje naslednji entiteti v filtrirni verigi, da blokira obdelavo zahteve. "

Če en filter pozabi narediti chain.doFilter () pokliče, ko bi moral, bo onemogočil druge filtre v verigi. Če en filter naredi chain.doFilter () pokličite, ko bi morali ne imajo, bo sprožil druge filtre v verigi.

Rešitev

Pravila vzorca ali okvira je treba uveljavljati prek vmesnikov in ne z dokumentacijo. Če računamo, da se bodo razvijalci spomnili pravila, ne deluje vedno. Rešitev je ločiti odločanje o izvrševanju verige in obdelavo zahtev s premikanjem Naslednji() klic osnovnega razreda. Naj odloča osnovni razred, podrazredi pa naj obravnavajo samo zahtevo. Z izogibanjem odločanju se lahko podrazredi popolnoma osredotočijo na svoje podjetje in se tako izognejo zgoraj opisani napaki.

Classic CoR: Pošlji zahtevo po verigi, dokler eno vozlišče ne obravnava zahteve

To je izvedba, ki jo predlagam za klasični OR:

 / ** * Classic CoR, tj. Zahtevo obravnava samo eden od upravljavcev v verigi. * / javni abstraktni razred ClassicChain {/ ** * Naslednje vozlišče v verigi. * / zasebni ClassicChain naslednji; javna ClassicChain (ClassicChain nextNode) {next = nextNode; } / ** * Začetna točka verige, ki jo pokliče odjemalec ali predhodno vozlišče. * Pokličite ročaj () na tem vozlišču in se odločite, ali želite nadaljevati verigo. Če naslednje vozlišče ni nulo in * to vozlišče ni obravnavalo zahteve, za obdelavo zahteve pokličite start () na naslednjem vozlišču. * @param zahteva parameter zahteve * / javni končni void start (zahteva ARequest) {boolean handledByThisNode = this.handle (zahteva); if (next! = null &&! handledByThisNode) next.start (zahteva); } / ** * Pokliče start (). * @param zahteva parameter zahteve * @return logična vrednost označuje, ali je to vozlišče obravnavalo zahtevo * / zaščiteni abstraktni logični ročaj (zahteva ARequest); } javni razred AClassicChain razširja ClassicChain {/ ** * Pokliče ga start (). * @param zahteva parameter zahteve * @return boolean označuje, ali je to vozlišče obravnavalo zahtevo * / zaščiteni logični ročaj (zahteva ARequest) {boolean handledByThisNode = false; if (someCondition) {// Ali ravnanje handledByThisNode = true; } return handledByThisNode; }} 

Izvedba ločuje logiko odločanja o izvrševanju verige in obdelavo zahtev, tako da jih razdeli na dve ločeni metodi. Metoda začetek () sprejme odločitev o izvršitvi verige in ročaj () obravnava zahtevo. Metoda začetek () je izhodišče za izvedbo verige. Kliče ročaj () na tem vozlišču in se odloči, ali bo verigo premaknil na naslednje vozlišče, glede na to, ali to vozlišče obravnava zahtevo in ali je vozlišče zraven. Če trenutno vozlišče ne obravnava zahteve in naslednje vozlišče ni nič, je trenutno vozlišče začetek () metoda napreduje verigo s klicem začetek () na naslednjem vozlišču ali ustavi verigo do ne klicanje začetek () na naslednjem vozlišču. Metoda ročaj () v osnovnem razredu je razglašen za abstraktnega, ne zagotavlja privzete logike ravnanja, ki je specifična za podrazred in nima nič skupnega z odločanjem o izvajanju verige. Podrazredi preglasijo to metodo in vrnejo logično vrednost, ki kaže, ali podrazredi sami obravnavajo zahtevo. Upoštevajte, da logična vrednost, ki jo vrne podrazred, obvešča začetek () v osnovnem razredu, ali je podrazred obdelal zahtevo, in ne, ali naj nadaljuje verigo. Odločitev o nadaljevanju verige je popolnoma odvisna od osnovnega razreda začetek () metoda. Podrazredi ne morejo spremeniti logike, definirane v začetek () Ker začetek () je razglašen za dokončnega.

Pri tej izvedbi ostane okno priložnosti, ki omogoča podrazredom, da zmedejo verigo z vrnitvijo nenamerne logične vrednosti. Vendar je ta zasnova veliko boljša od stare različice, ker podpis metode uveljavlja vrednost, ki jo vrne metoda; napaka je ujeta v času prevajanja. Razvijalci niso več dolžni ne pozabiti niti narediti Naslednji() pokličejo ali vrnejo logično vrednost v svoji kodi.

Neklasični OR 1: Pošljite zahtevo skozi verigo, dokler se eno vozlišče ne ustavi

Ta vrsta izvajanja OR je majhna različica klasičnega vzorca OR. Veriga se ustavi ne zato, ker je eno vozlišče obravnavalo zahtevo, ampak zato, ker se eno vozlišče želi ustaviti. V tem primeru tudi tu velja klasična izvedba OR z rahlo konceptualno spremembo: logična zastava, ki jo vrne ročaj () metoda ne označuje, ali je bila zahteva obdelana. Namesto tega osnovnemu razredu pove, ali je treba verigo ustaviti. V to kategorijo spada ogrodje filtra za programčke. Namesto da bi posamezne filtre silili klicati chain.doFilter (), nova izvedba prisili posamezni filter, da vrne logično vrednost, ki jo določi vmesnik, česar razvijalec nikoli ne pozabi ali zamudi.

Neklasični OR 2: Ne glede na obdelavo zahtev pošljite zahtevo vsem upravljavcem

Za to vrsto izvajanja OR je treba ročaj () ni treba vrniti logičnega indikatorja, ker je zahteva poslana vsem obdelovalcem, ne glede na to. Ta izvedba je lažja. Ker globalni okvir kljuka Microsoft Windows po naravi spada v to vrsto OR, bi morala njegova vrzel odpraviti naslednja izvedba:

 / ** * Neklasični CoR 2, tj. Zahteva se pošlje vsem upravljavcem, ne glede na ravnanje. * / javni abstraktni razred NonClassicChain2 {/ ** * Naslednje vozlišče v verigi. * / private NonClassicChain2 naslednji; javno NonClassicChain2 (NonClassicChain2 nextNode) {next = nextNode; } / ** * Začetna točka verige, ki jo pokliče odjemalec ali predhodno vozlišče. * Ročaj klica () na tem vozlišču, nato pa pokličite start () na naslednjem vozlišču, če naslednje vozlišče obstaja. * @param zahteva parameter zahteve * / javni končni void start (zahteva ARequest) {this.handle (zahteva); if (next! = null) next.start (zahteva); } / ** * Pokliče start (). * @param zahteva parameter parametra * / zaščiteni abstraktni prazninski ročaj (zahteva ARequest); } javni razred ANonClassicChain2 razširja NonClassicChain2 {/ ** * Pokliče ga start (). * @param zahteva parameter parametra * / zaščiten void handle (ARequest request) {// Naredite. }} 

Primeri

V tem razdelku vam bom pokazal dva verižna primera, ki uporabljata zgoraj opisano izvedbo za neklasični OR 2.

Primer 1

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