Programmēšana

Atbildības ķēdes modeļa nepilnības un uzlabojumi

Nesen es uzrakstīju divas Java programmas (Microsoft Windows OS), kurām jāaptver globālie tastatūras notikumi, kurus ģenerē citas lietojumprogrammas, kas vienlaikus darbojas tajā pašā darbvirsmā. Microsoft nodrošina veidu, kā to izdarīt, reģistrējot programmas kā globālo tastatūras āķu klausītāju. Kodēšana nebija ilga, bet atkļūdošana - gan. Šķiet, ka abas programmas darbojas labi, ja tiek pārbaudītas atsevišķi, taču, pārbaudot kopā, tās neizdevās. Turpmākie testi atklāja, ka tad, kad abas programmas darbojās kopā, programma, kas tika palaista pirmā, vienmēr nespēja uztvert galvenos globālos notikumus, taču vēlāk palaistā lietojumprogramma darbojās lieliski.

Es noslēpumu atrisināju pēc Microsoft dokumentācijas izlasīšanas. Kodā, kas pati reģistrē programmu kā āķa klausītāju, trūka CallNextHookEx () zvans, kas nepieciešams āķa ietvarā. Dokumentācijā lasāms, ka katrs āķa klausītājs tiek pievienots āķa ķēdei startēšanas secībā; pēdējais iesāktais klausītājs būs topā. Notikumi tiek nosūtīti pirmajam klausītājam ķēdē. Lai ļautu visiem klausītājiem saņemt notikumus, katram klausītājam ir jāveic CallNextHookEx () zvans, lai notikumus nodotu klausītājam blakus. Ja kāds klausītājs to aizmirsīs, nākamie klausītāji nedabūs notikumus; kā rezultātā viņu izstrādātās funkcijas nedarbosies. Tas bija precīzs iemesls, kāpēc mana otrā programma darbojās, bet pirmā nedarbojās!

Noslēpums tika atrisināts, bet es nebiju apmierināts ar āķa karkasu. Pirmkārt, man ir nepieciešams "atcerēties" ievietot CallNextHookEx () metode izsaukt manu kodu. Otrkārt, mana programma varētu atspējot citas programmas un pretēji. Kāpēc tas notiek? Tā kā Microsoft globālo āķu sistēmu ieviesa tieši pēc klasiskā Atbildības ķēdes (RK) modeļa, ko definēja Četru grupa (GoF).

Šajā rakstā es apspriedu GoF ieteiktās RK ieviešanas nepilnības un ierosinu tās risinājumu. Tas var palīdzēt jums izvairīties no tās pašas problēmas, izveidojot savu RK sistēmu.

Klasiskā RK

Klasiskais RK modelis, ko GoF definējis 2005 Dizaina modeļi:

"Izvairieties saistīt pieprasījuma sūtītāju ar tā uztvērēju, dodot iespēju vairākiem objektiem apstrādāt pieprasījumu. Ķēdējiet saņemošos objektus un nododiet pieprasījumu gar ķēdi, līdz objekts to apstrādā."

1. attēlā parādīta klašu diagramma.

Tipiska objekta struktūra varētu izskatīties kā 2. attēls.

No iepriekš minētajām ilustrācijām mēs varam apkopot, ka:

  • Vairāki apstrādātāji var apstrādāt pieprasījumu
  • Faktiski pieprasījumu apstrādā tikai viens apstrādātājs
  • Pieprasītājs zina tikai atsauci uz vienu apdarinātāju
  • Pieprasītājs nezina, cik apstrādātāju spēj izpildīt tā pieprasījumu
  • Pieprasītājs nezina, kurš apstrādātājs apstrādāja tā pieprasījumu
  • Pieprasītājam nav nekādas kontroles pār apstrādātājiem
  • Apstrādātājus varēja noteikt dinamiski
  • Apstrādātāju saraksta maiņa neietekmēs pieprasītāja kodu

Tālāk redzamie kodu segmenti parāda atšķirību starp pieprasītāja kodu, kas izmanto RK, un pieprasītāja kodu, kas to neizmanto.

Pieprasītāja kods, kas neizmanto RK:

 apstrādātāji = getHandlers (); for (int i = 0; i <apdarinātāji.length; i ++) {apstrādātāji [i] .rokturis (pieprasījums); if (apstrādātāji [i] .handled ()) saplīst; } 

Pieprasītāja kods, kas izmanto RK:

 getChain (). rokturis (pieprasījums); 

Pašlaik viss šķiet perfekts. Apskatīsim, kā GoF iesaka klasisko RK ieviešanu:

 publiskās klases hendleris {privātā hendlera pārņēmējs; publiskais apdarinātājs (HelpHandler s) {pēctecis = s; } publiskais rokturis (ARequest pieprasījums) {if (pēctecis! = null) pēctecis.handle (pieprasījums); }} public class AHandler paplašina Handler {public hand (ARequest pieprasījums) {if (someCondition) // Handling: dari kaut ko citu super.handle (pieprasījums); }} 

Bāzes klasei ir metode, rokturis (), kas aicina tā pēcteci - nākamo ķēdes mezglu - apstrādāt pieprasījumu. Apakšklases ignorē šo metodi un izlemj, vai ļaut ķēdei virzīties tālāk. Ja mezgls apstrādā pieprasījumu, apakšklase nezvanīs super.handle () tas aicina pēcteci, un ķēde gūst panākumus un apstājas. Ja mezgls neapstrādā pieprasījumu, apakšklase jābūt zvanu super.handle () lai ķēde turpinātu ripot, vai ķēde apstājas un neizdodas. Tā kā šis noteikums netiek izpildīts bāzes klasē, tā atbilstība netiek garantēta. Kad izstrādātāji aizmirst zvanīt apakšklasēs, ķēde neizdodas. Galvenais trūkums šeit ir tāds ķēdes izpildes lēmumu pieņemšana, kas nav apakšklases, ir saistīta ar pieprasījumu apstrādi apakšklasēs. Tas pārkāpj objektorientēta dizaina principu: objektam vajadzētu domāt tikai par savu biznesu. Ļaujot apakšklasei pieņemt lēmumu, jūs tai uzliekat papildu slogu un iespēju kļūdīties.

Microsoft Windows globālā āķa ietvara un Java servleta filtru ietvara caurums

Microsoft Windows globālā āķa ietvara ieviešana ir tāda pati kā klasiskā RK ieviešana, ko ieteica GoF. Sistēma ir atkarīga no katra klausītāja, kurš to izveidos CallNextHookEx () piezvaniet un pārsūtiet notikumu pa ķēdi. Tiek pieņemts, ka izstrādātāji vienmēr atcerēsies kārtulu un nekad neaizmirsīs piezvanīt. Pēc būtības globāla notikumu āķu ķēde nav klasiska RK. Pasākums jānogādā visiem klausītājiem ķēdē neatkarīgi no tā, vai klausītājs jau ar to rīkojas. Tātad CallNextHookEx () zvans, šķiet, ir pamata klases, nevis atsevišķu klausītāju darbs. Ļaujot atsevišķiem klausītājiem piezvanīt, tas neko labu nedod, un tiek ieviesta iespēja nejauši apturēt ķēdi.

Java servleta filtru ietvars pieļauj līdzīgu kļūdu kā Microsoft Windows globālais āķis. Tas precīzi seko GoF ieteiktajai ieviešanai. Katrs filtrs izlemj, vai roll vai apturēt ķēdi, zvanot vai nezvanot doFilter () uz nākamā filtra. Noteikums tiek izpildīts caur javax.servlet.Filter # doFilter () dokumentācija:

"4. a) Vai nu izsauciet nākamo ķēdes entītiju, izmantojot FilterChain objekts (chain.doFilter ()), 4. b) vai nenodod pieprasījuma / atbildes pāri nākamajai filtru ķēdes entītijai, lai bloķētu pieprasījumu apstrādi. "

Ja viens filtrs aizmirst izveidot chain.doFilter () zvaniet, kad tam vajadzētu būt, tas atspējos citus filtrus ķēdē. Ja viens filtrs padara chain.doFilter () zvaniet, kad vajadzētu ir, tas izsauks citus ķēdes filtrus.

Risinājums

Modeļa vai ietvara noteikumi jāievieš, izmantojot saskarnes, nevis dokumentāciju. Paļaušanās, ka izstrādātāji atcerēsies kārtulu, ne vienmēr darbojas. Risinājums ir atdalīt ķēdes izpildes lēmumu pieņemšanu un pieprasījumu apstrādi, pārvietojot Nākamais() zvans uz bāzes klasi. Ļaujiet pamatklasei pieņemt lēmumu un ļaujiet apakšklasēm apstrādāt tikai pieprasījumu. Novēršot lēmumu pieņemšanu, apakšklases var pilnībā koncentrēties uz savu biznesu, tādējādi izvairoties no iepriekš aprakstītās kļūdas.

Klasiskā RK: nosūtiet pieprasījumu pa ķēdi, līdz viens mezgls apstrādā pieprasījumu

RK klasiskajā klasifikācijā es to iesaku:

 / ** * Klasiskā RK, t.i., pieprasījumu apstrādā tikai viens no ķēdes apstrādātājiem. * / public abstract class ClassicChain {/ ** * Nākamais mezgls ķēdē. * / nākamā privātā ClassicChain; public ClassicChain (ClassicChain nextNode) {next = nextNode; } / ** * Ķēdes sākuma punkts, ko izsauc klients vai iepriekšējs mezgls. * Šajā mezglā izsauciet rokturi () un izlemiet, vai turpināt ķēdi. Ja nākamais mezgls nav nulle un * šis mezgls nav apstrādājis pieprasījumu, izsauciet nākamā mezgla sākumu (), lai apstrādātu pieprasījumu. * @param pieprasīt pieprasījuma parametru * / public final void start (ARequest pieprasījums) {Boolean handledByThisNode = this.handle (pieprasījums); if (next! = null &&! handledByThisNode) next.start (pieprasījums); } / ** * Zvanīja pēc sākuma (). * @param pieprasījuma pieprasījuma parametrs * @return boolean norāda, vai šis mezgls apstrādāja pieprasījumu * / aizsargāts abstrakts loģiskais teksts (ARequest pieprasījums); } publiskā klase AClassicChain paplašina ClassicChain {/ ** * Izsauca start (). * @param pieprasījuma pieprasījuma parametrs * @return boolean norāda, vai šis mezgls apstrādāja pieprasījumu * / aizsargāts Būla rokturis (ARequest pieprasījums) {boolean handledByThisNode = false; if (someCondition) {// Vai rīkojieties ar handledByThisNode = true; } atgriešanās handledByThisNode; }} 

Īstenošana atdala ķēdes izpildes lēmumu pieņemšanas loģiku un pieprasījumu apstrādi, sadalot tās divās atsevišķās metodēs. Metode sākt() pieņem ķēdes izpildes lēmumu un rokturis () apstrādā pieprasījumu. Metode sākt() ir ķēdes izpildes sākumpunkts. Tas aicina rokturis () šajā mezglā un izlemj, vai virzīt ķēdi uz nākamo mezglu, pamatojoties uz to, vai šis mezgls apstrādā pieprasījumu un vai mezgls atrodas blakus tam. Ja pašreizējais mezgls neapstrādā pieprasījumu un nākamais mezgls nav nulle, pašreizējā mezgla sākt() metode virza ķēdi, zvanot sākt() nākamajā mezglā vai apstādina ķēdi zvana sākt() uz nākamā mezgla. Metode rokturis () bāzes klasē tiek pasludināts par abstraktu, nenodrošinot noklusējuma apstrādes loģiku, kas ir raksturīga apakšklasei un kurai nav nekāda sakara ar ķēdes izpildes lēmumu pieņemšanu. Apakšklases ignorē šo metodi un atgriež Būla vērtību, norādot, vai apakšklases pašas apstrādā pieprasījumu. Ņemiet vērā, ka Būla atgriezās apakšklasē sākt() bāzes klasē, vai apakšklase ir izpildījusi pieprasījumu, nevis vai turpināt ķēdi. Lēmums par ķēdes turpināšanu ir pilnībā atkarīgs no bāzes klases sākt() metodi. Apakšklases nevar mainīt loģiku, kas definēta sākt() jo sākt() tiek pasludināts par galīgu.

Šajā ieviešanā paliek iespēju logs, ļaujot apakšklasēm sajaukt ķēdi, atgriežot neparedzētu Būla vērtību. Tomēr šis dizains ir daudz labāks nekā vecā versija, jo metodes paraksts izpilda metodi atdoto vērtību; kļūda tiek notverta kompilēšanas laikā. Izstrādātājiem vairs nav jāatceras vai nu izveidot Nākamais() zvaniet vai atgrieziet Būla vērtību savā kodā.

Neklasisks RK 1: nosūtiet pieprasījumu pa ķēdi, līdz viens mezgls vēlas apstāties

Šāda veida RK ieviešana ir neliela RK klasiskā modeļa variācija. Ķēde apstājas nevis tāpēc, ka viens mezgls ir apstrādājis pieprasījumu, bet gan tāpēc, ka viens mezgls vēlas apstāties. Tādā gadījumā šeit tiek piemērota arī klasiskā RK ieviešana, ar nelielām konceptuālām izmaiņām: Būla karogu, ko rokturis () metode nenorāda, vai pieprasījums ir apstrādāts. Drīzāk tas pamatklasei pasaka, vai ķēde ir jāpārtrauc. Serversīklietu filtra ietvars ietilpst šajā kategorijā. Tā vietā, lai piespiestu atsevišķus filtrus zvanīt chain.doFilter (), jaunā ieviešana liek individuālajam filtram atgriezt Būla vērtību, ar kuru saskarne ir noslēgta, kaut ko izstrādātājs nekad neaizmirst vai palaiž garām.

Neklasisks RK 2: neatkarīgi no pieprasījumu apstrādes nosūtiet pieprasījumu visiem apstrādātājiem

Šāda veida RK īstenošanai rokturis () nav jāatdod Būla rādītājs, jo pieprasījums tiek nosūtīts visiem apstrādātājiem neatkarīgi no tā. Šī ieviešana ir vienkāršāka. Tā kā Microsoft Windows globālais āķa ietvars pēc būtības pieder šāda veida RK, šī nepilnība jānovērš šādai ieviešanai:

 / ** * Neklasisks CoR 2, t.i., pieprasījums tiek nosūtīts visiem apstrādātājiem neatkarīgi no apstrādes. * / public abstract class NonClassicChain2 {/ ** * Nākamais mezgls ķēdē. * / nākamā privātā NonClassicChain2; public NonClassicChain2 (NonClassicChain2 nextNode) {next = nextNode; } / ** * Ķēdes sākuma punkts, ko izsauc klients vai iepriekšējs mezgls. * Zvana rokturi () šajā mezglā, pēc tam izsauciet startu () nākamajā mezglā, ja pastāv nākamais mezgls. * @param pieprasījuma pieprasījuma parametrs * / public final void start (ARequest pieprasījums) {this.handle (pieprasījums); if (next! = null) next.start (pieprasījums); } / ** * Zvanīja pēc sākuma (). * @param pieprasīt pieprasījuma parametru * / aizsargāts abstrakts void rokturis (ARequest pieprasījums); } publiskā klase ANonClassicChain2 paplašina NonClassicChain2 {/ ** * Izsauca start (). * @param pieprasiet pieprasījuma parametru * / protected void hand (ARequest pieprasījums) {// Vai apstrādāt. }} 

Piemēri

Šajā sadaļā es parādīšu divus ķēdes piemērus, kuros izmantota iepriekš aprakstītā neklasiskā RK 2 ieviešana.

1. piemērs

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