Programavimas

Atsakomybės grandinės modelio spąstai ir patobulinimai

Neseniai parašiau dvi „Java“ programas (skirtas „Microsoft Windows“ operacinei sistemai), kurios turi sugauti pasaulinius klaviatūros įvykius, kuriuos generuoja kitos programos, vienu metu veikiančios tame pačiame darbalaukyje. „Microsoft“ pateikia būdą tai padaryti, užregistruodama programas kaip visuotinį klaviatūros kablio klausytoją. Kodavimas neilgai truko, bet derinimas - ne. Atrodė, kad abi programos veikia gerai, kai bandomos atskirai, tačiau nepavyko, kai bandomos kartu. Tolesni bandymai atskleidė, kad kai abi programos buvo paleistos kartu, pirmoji paleista programa visada nesugebėjo sugauti svarbiausių pasaulinių įvykių, tačiau vėliau paleista programa veikė puikiai.

Paslaptį pašalinau perskaitęs „Microsoft“ dokumentus. Trūko kodo, kuris užregistruoja pačią programą kaip kablio klausytoją „CallNextHookEx“ () skambutis, reikalingas kablio karkasui. Dokumentuose rašoma, kad kiekvienas kablio klausytojas pridedamas prie kablio grandinės paleidimo tvarka; paskutinis pradėtas klausytojas bus viršuje. Įvykiai siunčiami pirmajam klausytojui grandinėje. Kad visi klausytojai galėtų priimti įvykius, kiekvienas klausytojas privalo tai padaryti „CallNextHookEx“ () skambutis, kad įvykiai būtų perduoti šalia jo esančiam klausytojui. Jei kuris nors klausytojas pamiršta tai padaryti, kiti klausytojai negaus įvykių; dėl to jų suprojektuotos funkcijos neveiks. Tai buvo tiksli priežastis, kodėl mano antroji programa veikė, bet pirmoji - ne!

Paslaptis buvo išspręsta, bet aš buvau nepatenkinta kablio karkasu. Pirma, man reikia „atsiminti“ įterpti „CallNextHookEx“ () metodo iškvietimas į mano kodą. Antra, mano programa gali išjungti kitas programas ir atvirkščiai. Kodėl taip nutinka? Nes „Microsoft“ įdiegė visuotinę „hook-end“ sistemą vadovaudamasi klasikiniu „Atsakomybės grandinės“ (RK) modeliu, apibrėžtu „Keturių gauja“ (GoF).

Šiame straipsnyje aptariu GoF pasiūlytą RK įgyvendinimo spragą ir siūlau jos sprendimą. Tai gali padėti išvengti tos pačios problemos kuriant savo RK sistemą.

Klasikinis RK

Klasikinis RK modelis, kurį GoF apibrėžė 2005 m Dizaino modeliai:

"Venkite susieti užklausos siuntėją su jo imtuvu, suteikdami daugiau nei vienam objektui galimybę tvarkyti užklausą. Prijunkite priėmimo objektus ir perduokite užklausą palei grandinę, kol objektas ją apdoros."

1 paveiksle pavaizduota klasės schema.

Tipiška objekto struktūra gali atrodyti kaip 2 paveiksle.

Iš pirmiau pateiktų iliustracijų galime apibendrinti, kad:

  • Keli tvarkytojai gali tvarkyti užklausą
  • Prašymą iš tikrųjų tvarko tik vienas tvarkytojas
  • Prašytojas žino tik nuorodą į vieną prižiūrėtoją
  • Pareiškėjas nežino, kiek tvarkytojų sugeba patenkinti jo užklausą
  • Prašytojas nežino, kuris tvarkytojas įvykdė jo prašymą
  • Prašytojas nekontroliuoja prižiūrėtojų
  • Prižiūrėtojus būtų galima dinamiškai nurodyti
  • Tvarkytojų sąrašo pakeitimas neturės įtakos prašytojo kodui

Žemiau pateikti kodų segmentai rodo skirtumą tarp užklausos teikėjo kodo, kuriame naudojamas RK, ir užklausos teikėjo kodo, kuris nenaudojamas.

Užklausos pateikėjo kodas, nenaudojantis RK:

 tvarkytojai = getHandlers (); for (int i = 0; i <handlers.length; i ++) {handlers [i]. rankena (prašymas); jei (tvarkytojai [i] .tvarkomi ()) lūžta; } 

Užklausos teikėjo kodas, kuriame naudojamas RK:

 getChain (). rankena (užklausa); 

Šiuo metu viskas atrodo tobula. Bet pažiūrėkime, kaip GoF siūlo klasikinį RK:

 viešosios klasės tvarkytojas {privatus tvarkytojo įpėdinis; viešasis tvarkytojas („HelpHandler s“) {teisių perėmėjas = s; } viešoji rankena (ARequest užklausa) {if (teisių perėmėjas! = null) teisių perėmėjas.handle (prašymas); }} public class AHandler prailgina Handler {public hand (ARequest request) {if (someCondition) // Handling: daryk dar ką nors super.handle (request); }} 

Pagrindinė klasė turi metodą, rankena (), kuris paskambina savo įpėdiniu, kitu grandinės mazgu, tvarkyti užklausą. Poklasiai pakeičia šį metodą ir nusprendžia, ar leisti grandinei judėti toliau. Jei mazgas tvarko užklausą, poklasis nepaskambins super.handle () kad paskambina įpėdiniu, o grandinė pavyksta ir sustoja. Jei mazgas netvarko užklausos, poklasis turi skambutis super.handle () grandinė nesisuka arba grandinė sustoja ir sugenda. Kadangi ši taisyklė nėra vykdoma pagrindinėje klasėje, jos laikymasis nėra garantuojamas. Kai kūrėjai pamiršta skambinti poklasiuose, grandinė nepavyksta. Esminis trūkumas čia yra tas grandinės vykdymo sprendimų priėmimas, kuris nėra poklasių reikalas, kartu su prašymų tvarkymu poklasiuose. Tai pažeidžia į objektą orientuoto projektavimo principą: objektas turi galvoti tik apie savo verslą. Leisdami poklasiui priimti sprendimą, jūs įtrauksite jam papildomą naštą ir klaidos galimybę.

„Microsoft Windows“ pasaulinės kablio sistemos ir „Java“ servleto filtro sistemos spraga

„Microsoft Windows“ visuotinio kablio pagrindo diegimas yra toks pat, kaip klasikinio RK diegimo, kurį pasiūlė GoF. Karkasas priklauso nuo kiekvieno klausytojo, kuris jį sukurs „CallNextHookEx“ () paskambinkite ir perduokite įvykį per grandinę. Manoma, kad kūrėjai visada prisimins taisyklę ir niekada nepamirš skambinti. Iš prigimties pasaulinė įvykių kablių grandinė nėra klasikinė RK. Renginys turi būti pristatytas visiems klausytojams, neatsižvelgiant į tai, ar klausytojas jau jį tvarko. Taigi „CallNextHookEx“ () Atrodo, kad skambutis yra pagrindinės klasės, o ne atskirų klausytojų darbas. Leidimas paskambinti atskiriems klausytojams neduoda jokios naudos ir suteikia galimybę netyčia sustabdyti grandinę.

„Java“ servleto filtro sistema daro panašią klaidą kaip ir „Microsoft Windows“ visuotinis kabliukas. Tai tiksliai atitinka GoF pasiūlytą įgyvendinimą. Kiekvienas filtras nusprendžia, ar riedėti, ar sustabdyti grandinę, skambindamas, ar ne „doFilter“ () ant kito filtro. Taisyklė vykdoma per javax.servlet.Filter # doFilter () dokumentacija:

"4. a) Arba iškvieskite kitą grandinės esmę naudodami „FilterChain“ objektas (chain.doFilter ()), 4. b) arba neperduoti užklausos / atsakymo poros kitam filtro grandinės subjektui, kad blokuotų užklausos apdorojimą. "

Jei vienas filtras pamiršta padaryti chain.doFilter () skambinkite, kai turėtų būti, bus išjungti kiti grandinės filtrai. Jei vienas filtras daro chain.doFilter () skambinkite, kai turėtų ne turi, jis sukvies kitus grandinės filtrus.

Sprendimas

Šablono ar sistemos taisyklės turėtų būti vykdomos naudojant sąsajas, o ne dokumentus. Tikimasi, kad kūrėjai prisimins taisyklę, ne visada pavyksta. Sprendimas yra atsieti grandinės vykdymo sprendimų priėmimą ir užklausų tvarkymą perkeliant Kitas() skambutis į bazinę klasę. Tegul pagrindinė klasė priima sprendimą, o poklasiai tegul tvarko tik užklausą. Nepaisydami sprendimų priėmimo, poklasiai gali visiškai sutelkti dėmesį į savo verslą, taip išvengdami aukščiau aprašytos klaidos.

Klasikinis RK: siųskite užklausą per grandinę, kol vienas mazgas apdoros užklausą

Tai aš siūlau klasikinio RK įgyvendinimą:

 / ** * Klasikinis RK, t. Y. Prašymą tvarko tik vienas iš grandinės tvarkytojų. * / public abstract class ClassicChain {/ ** * Kitas grandinės mazgas. * / privatus „ClassicChain“ kitas; viešasis „ClassicChain“ („ClassicChain nextNode“) {next = nextNode; } / ** * Pradinis grandinės taškas, iškviestas kliento arba išankstinio mazgo. * Šiame mazge iškvieskite rankeną () ir nuspręskite, ar tęsti grandinę. Jei kitas mazgas nėra nulinis ir * šis mazgas netvarkė užklausos, iškvieskite start () kitame mazge, kad tvarkytumėte užklausą. * @param užklausa užklausos parametras * / public final void start (ARequest užklausa) {Boolean handledByThisNode = this.handle (užklausa); if (next! = null &&! handledByThisNode) next.start (užklausa); } / ** * Paskambino pradžia (). * @param užklausa užklausos parametras * @return loginė reikšmė nurodo, ar šis mazgas tvarkė užklausą * / apsaugota abstrakčioji loginė rankena (ARequest užklausa); } viešosios klasės „AClassicChain“ praplečia „ClassicChain“ {/ ** * Paskambino pradžia (). * @param užklausa užklausos parametras * @return loginė reikšmė nurodo, ar šis mazgas tvarkė užklausą * / apsaugota loginė rankena (ARequest užklausa) {Boolean handledByThisNode = false; if (someCondition) {// Ar tvarkyti handledByThisNode = true; } return handledByThisNode; }} 

Diegimas atsieja grandinės vykdymo sprendimų priėmimo logiką ir užklausų tvarkymą, padalydamas juos į du atskirus metodus. Metodas pradžia () priima grandinės vykdymo sprendimą ir rankena () tvarko prašymą. Metodas pradžia () yra grandinės vykdymo pradinis taškas. Tai ragina rankena () šiame mazge ir nusprendžia, ar perkelti grandinę į kitą mazgą pagal tai, ar šis mazgas tvarko užklausą ir ar mazgas yra šalia jo. Jei dabartinis mazgas netvarko užklausos ir kitas mazgas nėra nulinis, dabartinio mazgo pradžia () metodas virsta grandine skambindamas pradžia () kitame mazge arba sustabdo grandinę ne skambinimas pradžia () ant kito mazgo. Metodas rankena () bazinėje klasėje yra paskelbtas abstraktus, nepateikiant numatytosios tvarkymo logikos, kuri yra specifinė poklasiui ir neturi nieko bendra su grandinės vykdymo sprendimų priėmimu. Poklasiai pakeičia šį metodą ir pateikia Bulio reikšmę, nurodančią, ar poklasiai patys tvarko užklausą. Atkreipkite dėmesį, kad Booleanas, kurį grąžino poklasis, informuoja pradžia () bazinėje klasėje, ar poklasis įvykdė prašymą, o ne tai, ar tęsti grandinę. Sprendimas, ar tęsti grandinę, priklauso tik nuo pagrindinės klasės pradžia () metodas. Poklasiai negali pakeisti logikos, apibrėžtos pradžia () nes pradžia () yra paskelbta galutine.

Šiame įgyvendinime lieka galimybių langas, leidžiantis poklasiams sujaukti grandinę, grąžinant nenumatytą loginę vertę. Tačiau šis dizainas yra daug geresnis nei senoji versija, nes metodo parašas įgyvendina metodo grąžintą vertę; klaida pagauta kompiliavimo metu. Kūrėjai nebereikia prisiminti, kad jie turi Kitas() paskambinkite arba grąžinkite loginę reikšmę savo kode.

Neklasikinis RK 1: Siųskite užklausą per grandinę, kol vienas mazgas nori sustoti

Šio tipo RK įgyvendinimas yra nedidelis klasikinio RK modelio variantas. Grandinė sustoja ne todėl, kad vienas mazgas apdorojo užklausą, bet todėl, kad vienas mazgas nori sustoti. Tokiu atveju čia taip pat taikomas klasikinis RK įgyvendinimas, šiek tiek pakeitus koncepciją: Būlo vėliava, kurią grąžino rankena () metodas nenurodo, ar užklausa buvo apdorota. Atvirkščiai, jis nurodo pagrindinei klasei, ar grandinė turėtų būti sustabdyta. Servleto filtro karkasas tinka šiai kategorijai. Užuot privertę skambinti atskirus filtrus chain.doFilter (), naujas diegimas priverčia atskirą filtrą grąžinti loginę reikšmę, kurią sutraukia sąsaja, ko kūrėjas niekada nepamiršta ir nepraleidžia.

Neklasikinis RK 2: Nepaisant užklausų tvarkymo, siųskite užklausą visiems tvarkytojams

Šio tipo RK įgyvendinimas rankena () nereikia grąžinti loginio rodiklio, nes užklausa siunčiama visiems tvarkytojams, neatsižvelgiant į tai. Tai lengviau įgyvendinti. Kadangi „Microsoft Windows“ visuotinis kablio pagrindas iš prigimties priklauso šio tipo RK, spragą turėtų pašalinti šis diegimas:

 / ** * Neklasikinis CoR 2, t. Y. Užklausa siunčiama visiems tvarkytojams, neatsižvelgiant į tvarkymą. * / public abstract class NonClassicChain2 {/ ** * Kitas grandinės mazgas. * / privatus „NonClassicChain2“ kitas; public NonClassicChain2 (NonClassicChain2 nextNode) {kitas = kitasNode; } / ** * Pradinis grandinės taškas, iškviestas kliento arba išankstinio mazgo. * Skambinkite rankena () šiame mazge, tada paskambinkite start () kitame mazge, jei yra kitas mazgas. * @param užklausa užklausos parametras * / public final void start (ARequest užklausa) {this.handle (užklausa); if (next! = null) next.start (prašymas); } / ** * Paskambino pradžia (). * @param užklausa užklausos parametras * / saugoma abstrakti negaliojanti rankena (ARequest užklausa); } viešoji klasė „ANonClassicChain2“ pratęsia „NonClassicChain2“ {/ ** * Paskambino pradžia (). * @param užklausa užklausos parametras * / protected void hand (ARequest request) {// Atlikite tvarkymą. }} 

Pavyzdžiai

Šiame skyriuje aš jums parodysiu du grandinės pavyzdžius, kuriuose naudojamas aukščiau aprašytas netradicinio RK 2 įgyvendinimas.

1 pavyzdys

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