Programmering

Kæden af ​​ansvarsmønsterets faldgruber og forbedringer

For nylig skrev jeg to Java-programmer (til Microsoft Windows OS), der skal fange globale tastaturhændelser genereret af andre applikationer, der samtidigt kører på det samme skrivebord. Microsoft giver en måde at gøre det ved at registrere programmerne som en global tastaturkroglytter. Kodning tog ikke lang tid, men fejlretning gjorde det. De to programmer syntes at fungere fint, når de blev testet separat, men mislykkedes, når de blev testet sammen. Yderligere tests afslørede, at når de to programmer kørte sammen, var det program, der blev lanceret først, altid ude af stand til at fange de globale nøglebegivenheder, men den applikation, der blev lanceret senere, fungerede fint.

Jeg løste mysteriet efter at have læst Microsoft-dokumentationen. Koden, der registrerer selve programmet som en kroglytter, manglede CallNextHookEx () opkald krævet af krogrammen. Dokumentationen læser, at hver kroglytter føjes til en krogkæde i opstartsrækkefølgen; den sidste lytter startede vil være øverst. Begivenheder sendes til den første lytter i kæden. For at tillade alle lyttere at modtage begivenheder, skal hver lytter oprette CallNextHookEx () kald for at viderebringe begivenhederne til lytteren ved siden af ​​den. Hvis nogen lytter glemmer at gøre det, får de efterfølgende lyttere ikke begivenhederne; som et resultat fungerer deres designede funktioner ikke. Det var den nøjagtige grund til, at mit andet program fungerede, men det første fungerede ikke!

Mysteriet blev løst, men jeg var utilfreds med krogen. Først kræver det mig at "huske" at indsætte CallNextHookEx () metode kald til min kode. For det andet kunne mit program deaktivere andre programmer og omvendt. Hvorfor sker det? Fordi Microsoft implementerede den globale krogramme efter nøjagtigt det klassiske Chain of Responsibility (CoR) mønster defineret af Gang of Four (GoF).

I denne artikel diskuterer jeg smuthullet i Regionsudvalgets implementering foreslået af GoF og foreslår en løsning på det. Det kan hjælpe dig med at undgå det samme problem, når du opretter din egen CoR-ramme.

Klassisk Regionsudvalg

Det klassiske CoR-mønster defineret af GoF i Designmønstre:

"Undgå at koble afsenderen af ​​en anmodning til modtageren ved at give mere end et objekt en chance for at håndtere anmodningen. Kæd de modtagende objekter og send anmodningen langs kæden, indtil en genstand håndterer den."

Figur 1 illustrerer klassediagrammet.

En typisk objektstruktur kan se ud som figur 2.

Fra ovenstående illustrationer kan vi opsummere, at:

  • Flere handlere kan muligvis håndtere en anmodning
  • Kun en handler håndterer faktisk anmodningen
  • Rekvirenten kender kun en henvisning til en handler
  • Rekvirenten ved ikke, hvor mange handlere, der er i stand til at håndtere sin anmodning
  • Rekvirenten ved ikke, hvilken handler der har håndteret sin anmodning
  • Rekvirenten har ingen kontrol over håndtererne
  • Handlerne kunne specificeres dynamisk
  • Ændring af håndteringslisten påvirker ikke rekvirentens kode

Kodesegmenterne nedenfor viser forskellen mellem anmoderkode, der bruger CoR og anmoderkode, der ikke gør det.

Anmoderkode, der ikke bruger CoR:

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

Anmoderkode, der bruger CoR:

 getChain (). håndtere (anmodning); 

Fra nu af virker alt perfekt. Men lad os se på implementeringen, som GoF foreslår for det klassiske CoR:

 offentlig klasse Handler {privat Handler efterfølger; public Handler (HelpHandler s) {efterfølger = s; } offentligt håndtag (ARequest anmodning) {if (successor! = null) successor.handle (anmodning); }} offentlig klasse AHandler udvider Handler {public handle (ARequest anmodning) {if (someCondition) // Håndtering: gør noget andet super.handle (anmodning); }} 

Baseklassen har en metode, håndtere(), der kalder sin efterfølger, den næste knude i kæden, til at håndtere anmodningen. Underklasserne tilsidesætter denne metode og beslutter, om kæden skal komme videre. Hvis noden håndterer anmodningen, ringer ikke underklassen super.handle () det kalder efterfølgeren, og kæden lykkes og stopper. Hvis noden ikke håndterer anmodningen, underklassen skal opkald super.handle () for at holde kæden rullende, ellers stopper kæden og svigter. Da denne regel ikke håndhæves i basisklassen, garanteres den ikke, at den overholdes. Når udviklere glemmer at foretage opkaldet i underklasser, mislykkes kæden. Den grundlæggende fejl her er, at beslutningstagning om kædeudførelse, som ikke er underklassens forretning, er forbundet med anmodningshåndtering i underklasserne. Det er i strid med et princip om objektorienteret design: et objekt skal kun huske sin egen forretning. Ved at lade en underklasse træffe beslutningen, indfører du ekstra byrde for den og muligheden for fejl.

Loophole i Microsoft Windows globale hook framework og Java servlet filter framework

Implementeringen af ​​Microsoft Windows globale hook framework er den samme som den klassiske CoR-implementering foreslået af GoF. Rammen afhænger af, at de enkelte kroglyttere opretter CallNextHookEx () kalde og viderebringe begivenheden gennem kæden. Det forudsætter, at udviklere altid vil huske reglen og aldrig glemme at foretage opkaldet. En global krogkæde til begivenheder er af natur ikke klassisk RU. Begivenheden skal leveres til alle lyttere i kæden, uanset om en lytter allerede håndterer den. Så CallNextHookEx () opkald synes at være jobbet for basisklassen, ikke de enkelte lyttere. At lade de enkelte lyttere foretage opkaldet gør ikke noget godt og introducerer muligheden for at stoppe kæden ved et uheld.

Java-servlet-filterrammen laver en lignende fejl som Microsoft Windows-globale krog. Det følger nøjagtigt den implementering, som GoF foreslår. Hvert filter beslutter, om kæden skal rulle eller stoppe ved at ringe eller ikke ringe doFilter () på det næste filter. Reglen håndhæves gennem javax.servlet.Filter # doFilter () dokumentation:

"4. a) Enten påkalder den næste enhed i kæden ved hjælp af Filterkæde objekt (chain.doFilter ()), 4. b) eller ikke videregive anmodnings- / svarparret til den næste enhed i filterkæden for at blokere behandlingen af ​​anmodningen. "

Hvis et filter glemmer at lave chain.doFilter () når det skulle have det, vil det deaktivere andre filtre i kæden. Hvis et filter fremstiller chain.doFilter () ring, når det skal ikke har, påkalder det andre filtre i kæden.

Opløsning

Reglerne for et mønster eller en ramme skal håndhæves gennem grænseflader, ikke dokumentation. At stole på, at udviklere husker reglen, fungerer ikke altid. Løsningen er at afkoble beslutningstagning med kædeudførelse og håndtering af anmodninger ved at flytte Næste() kald til basisklassen. Lad basisklassen tage beslutningen, og lad kun underklasser håndtere anmodningen. Ved at undgå beslutningstagning kan underklasser helt fokusere på deres egen forretning og dermed undgå den ovenfor beskrevne fejl.

Klassisk CoR: Send anmodning gennem kæden, indtil en node håndterer anmodningen

Dette er den implementering, jeg foreslår for det klassiske Regionsudvalg:

 / ** * Klassisk Regionsudvalg, dvs. anmodningen håndteres af kun en af ​​håndtererne i kæden. * / public abstract class ClassicChain {/ ** * Den næste node i kæden. * / private ClassicChain næste; offentlig ClassicChain (ClassicChain nextNode) {next = nextNode; } / ** * Startpunkt for kæden, kaldet af klient eller præknude. * Ring til håndtaget () på denne node, og beslut om du vil fortsætte kæden. Hvis den næste node ikke er null, og * denne node ikke håndterede anmodningen, skal du ringe til start () på næste node for at håndtere anmodningen. * @param anmodning om anmodningsparameter * / offentlig endelig ugyldig start (ARequest anmodning) {boolean handledByThisNode = this.handle (anmodning); hvis (næste! = null &&! handledByThisNode) næste.start (anmodning); } / ** * Kaldt efter start (). * @param anmoder om anmodningsparameteren * @retur en boolsk indikerer, om denne node håndterede anmodningen * / beskyttet abstrakt boolsk håndtag (ARequest anmodning); } offentlig klasse AClassicChain udvider ClassicChain {/ ** * Kaldes af start (). * @param anmoder om anmodningsparameteren * @retur en boolsk indikerer, om denne node håndterede anmodningen * / beskyttet boolesk håndtag (ARequest-anmodning) {boolean handledByThisNode = false; if (someCondition) {// Gør håndtering handlingByThisNode = sand; } returneret handledByThisNode; }} 

Implementeringen afkobler kædeudførelseslogik og anmodningshåndtering ved at opdele dem i to separate metoder. Metode Start() træffer beslutning om kædeudførelse og håndtere() håndterer anmodningen. Metode Start() er kædeudførelsens udgangspunkt. Det kalder håndtere() på denne node og beslutter, om kæden skal gå videre til den næste node baseret på, om denne node håndterer anmodningen, og om en node er ved siden af ​​den. Hvis den aktuelle node ikke håndterer anmodningen, og den næste node ikke er null, er den aktuelle node Start() metoden fremmer kæden ved at ringe Start() på den næste knude eller stopper kæden ved ikke ringer Start() på den næste knude. Metode håndtere() i basisklassen erklæres abstrakt og giver ingen standardhåndteringslogik, som er underklassespecifik og ikke har noget at gøre med beslutningstagning om kædeudførelse. Underklasser tilsidesætter denne metode og returnerer en boolsk værdi, der angiver, om underklasserne selv håndterer anmodningen. Bemærk, at den boolske, der returneres af en underklasse, informerer Start() i basisklassen, om underklassen har håndteret anmodningen, ikke om kæden skal fortsættes. Beslutningen om at fortsætte kæden er helt op til baseklassen Start() metode. Underklasserne kan ikke ændre den logik, der er defineret i Start() fordi Start() erklæres endelig.

I denne implementering forbliver et vindue af muligheder, der tillader underklasserne at ødelægge kæden ved at returnere en utilsigtet boolsk værdi. Dette design er dog meget bedre end den gamle version, fordi metodesignaturen håndhæver den værdi, der returneres ved en metode; fejlen er fanget på kompileringstidspunktet. Udviklere er ikke længere forpligtet til at huske at enten lave Næste() ring eller returner en boolsk værdi i deres kode.

Ikke-klassisk CoR 1: Send anmodning gennem kæden, indtil en node vil stoppe

Denne type CoR-implementering er en lille variation af det klassiske CoR-mønster. Kæden stopper ikke fordi en node har håndteret anmodningen, men fordi en node vil stoppe. I så fald gælder den klassiske RU-implementering også her med en lille konceptuel ændring: det boolske flag returneret af håndtere() metode angiver ikke, om anmodningen er blevet håndteret. Det fortæller snarere basisklassen, om kæden skal stoppes. Servletfilterrammen passer til denne kategori. I stedet for at tvinge individuelle filtre til at ringe chain.doFilter (), tvinger den nye implementering det enkelte filter til at returnere en boolsk, som er kontraheret af grænsefladen, noget udvikleren aldrig glemmer eller savner.

Ikke-klassisk CoR 2: Uanset håndtering af anmodninger, send anmodning til alle handlere

For denne type implementering af Regionsudvalget håndtere() behøver ikke at returnere den boolske indikator, fordi anmodningen sendes til alle handlere uanset. Denne implementering er lettere. Fordi Microsoft Windows globale krogramme af natur hører til denne type Regionsudvalg, bør følgende implementering rette sit smuthul:

 / ** * Ikke-klassisk CoR 2, dvs. anmodningen sendes til alle handlere uanset håndtering. * / public abstract class NonClassicChain2 {/ ** * Den næste node i kæden. * / private NonClassicChain2 næste; public NonClassicChain2 (NonClassicChain2 nextNode) {next = nextNode; } / ** * Startpunkt for kæden, kaldet af klient eller præknude. * Opkaldshåndtag () på denne node, derefter kald start () på næste node, hvis næste node findes. * @param anmoder om anmodningsparameteren * / offentlig endelig ugyldig start (ARequest anmodning) {this.handle (anmodning); hvis (næste! = null) næste.start (anmodning); } / ** * Kaldt efter start (). * @param anmodning om anmodningsparameter * / beskyttet abstrakt ugyldigt håndtag (ARequest anmodning); } offentlig klasse ANonClassicChain2 udvider NonClassicChain2 {/ ** * Kaldt ved start (). * @param anmoder om anmodningsparameteren * / beskyttet ugyldigt håndtag (ARequest-anmodning) {// Gør håndtering. }} 

Eksempler

I dette afsnit viser jeg dig to kædeeksempler, der bruger implementeringen til ikke-klassisk CoR 2 beskrevet ovenfor.

Eksempel 1