Programmering

Chain of Responsibility-mønsterets fallgruver og forbedringer

Nylig skrev jeg to Java-programmer (for Microsoft Windows OS) som må fange globale tastaturhendelser generert av andre applikasjoner som samtidig kjører på samme skrivebord. Microsoft gir en måte å gjøre det ved å registrere programmene som en global lytter på tastaturkroken. Koding tok ikke lang tid, men feilsøking gjorde det. De to programmene så ut til å fungere bra når de ble testet hver for seg, men mislyktes når de ble testet sammen. Ytterligere tester viste at når de to programmene kjørte sammen, var det programmet som ble lansert først ikke alltid i stand til å fange de globale nøkkelhendelsene, men applikasjonen som ble lansert senere fungerte helt fint.

Jeg løste mysteriet etter å ha lest Microsoft-dokumentasjonen. Koden som registrerer selve programmet som en kroklytter manglet CallNextHookEx () samtale kreves av kroken. Dokumentasjonen leser at hver kroklytter blir lagt til en krokkjede i rekkefølgen av oppstart; den siste lytteren startet vil være på toppen. Arrangementer sendes til den første lytteren i kjeden. For å tillate alle lyttere å motta begivenheter, må hver lytter lage CallNextHookEx () ring for å videreformidle hendelsene til lytteren ved siden av den. Hvis noen lyttere glemmer å gjøre det, vil ikke de påfølgende lytterne få begivenhetene; som et resultat vil de designede funksjonene ikke fungere. Det var den nøyaktige grunnen til at det andre programmet mitt fungerte, men det første ikke!

Mysteriet ble løst, men jeg var misfornøyd med krokerammen. Først krever det at jeg "husker" å sette inn CallNextHookEx () metode ring inn koden min. For det andre kan programmet mitt deaktivere andre programmer og omvendt. Hvorfor skjer det? Fordi Microsoft implementerte det globale hook-rammeverket etter nøyaktig det klassiske Chain of Responsibility (CoR) mønster definert av Gang of Four (GoF).

I denne artikkelen diskuterer jeg smutthullet for RegR-implementeringen foreslått av GoF og foreslår en løsning på det. Det kan hjelpe deg med å unngå det samme problemet når du lager ditt eget CoR-rammeverk.

Klassisk CoR

Det klassiske CoR-mønsteret definert av GoF i Design mønstre:

"Unngå å koble avsenderen av en forespørsel til mottakeren ved å gi mer enn ett objekt en sjanse til å håndtere forespørselen. Kjede mottaksobjektene og send forespørselen langs kjeden til en gjenstand håndterer den."

Figur 1 illustrerer klassediagrammet.

En typisk objektstruktur kan se ut som figur 2.

Fra illustrasjonene ovenfor kan vi oppsummere at:

  • Flere håndtere kan være i stand til å håndtere en forespørsel
  • Bare en handler håndterer forespørselen
  • Rekvirenten vet bare en henvisning til en behandler
  • Rekvirenten vet ikke hvor mange håndtere som kan håndtere forespørselen
  • Rekvirenten vet ikke hvilken behandler som håndterte forespørselen
  • Rekvirenten har ikke kontroll over håndtererne
  • Behandlerne kunne spesifiseres dynamisk
  • Endring av håndteringslisten påvirker ikke rekvirentens kode

Kodesegmentene nedenfor viser forskjellen mellom forespørselskode som bruker CoR og forespørselskode som ikke gjør det.

Forespørselskode som ikke bruker CoR:

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

Forespørselskode som bruker CoR:

 getChain (). håndtere (forespørsel); 

Per nå virker alt perfekt. Men la oss se på implementeringen GoF foreslår for den klassiske CoR:

 offentlig klasse Handler {privat Handler etterfølger; public Handler (HelpHandler s) {etterfølger = s; } offentlig håndtak (ARequest request) {if (successor! = null) successor.handle (request); }} offentlig klasse AHandler utvider Handler {public handle (ARequest request) {if (someCondition) // Handling: do something else super.handle (request); }} 

Baseklassen har en metode, håndtak(), som kaller etterfølgeren, den neste noden i kjeden, for å håndtere forespørselen. Underklassene overstyrer denne metoden og bestemmer om kjeden skal gå videre. Hvis noden håndterer forespørselen, vil ikke underklassen ringe super.handle () som kaller etterfølgeren, og kjeden lykkes og stopper. Hvis noden ikke håndterer forespørselen, blir underklassen anrop super.handle () for å holde kjedet rullende, eller kjedet stopper og svikter. Fordi denne regelen ikke håndheves i basisklassen, er den ikke garantert. Når utviklere glemmer å ringe i underklasser, mislykkes kjeden. Den grunnleggende feilen her er at beslutningstaking av kjedeutførelse, som ikke er underklassens virksomhet, er kombinert med forespørselhåndtering i underklassene. Det bryter med et prinsipp med objektorientert design: et objekt skal bare huske sin egen virksomhet. Ved å la en underklasse ta avgjørelsen, innfører du ekstra belastning på den og muligheten for feil.

Loophole of Microsoft Windows global hook framework og Java servlet filter framework

Implementeringen av Microsoft Windows globale hook framework er den samme som den klassiske CoR-implementeringen foreslått av GoF. Rammeverket avhenger av de enkelte kroklytterne for å lage CallNextHookEx () ring og viderefør begivenheten gjennom kjeden. Det antar at utviklere alltid vil huske regelen og aldri glemme å ringe. Av natur er ikke en global hendelseskjedekjede klassisk RU. Arrangementet må leveres til alle lyttere i kjeden, uavhengig av om en lytter allerede håndterer det. Så CallNextHookEx () samtale ser ut til å være jobben til baseklassen, ikke de enkelte lytterne. Å la de enkelte lytterne ringe, gjør ikke noe og introduserer muligheten for å stoppe kjeden ved et uhell.

Rammeverket for Java-servletfilter gjør en lignende feil som Microsoft Windows-kroken. Den følger nøyaktig implementeringen foreslått av GoF. Hvert filter bestemmer om det skal rulle eller stoppe kjeden ved å ringe eller ikke ringe doFilter () på neste filter. Regelen håndheves gjennom javax.servlet.Filter # doFilter () dokumentasjon:

"4. a) Enten påkaller neste enhet i kjeden ved hjelp av Filterkjede objekt (chain.doFilter ()), 4. b) eller ikke videreformidle forespørsel / svar-paret til neste enhet i filterkjeden for å blokkere forespørsel. "

Hvis ett filter glemmer å lage chain.doFilter () ringe når det skulle ha det, vil det deaktivere andre filtre i kjeden. Hvis ett filter lager chain.doFilter () ring når det skal ikke har, vil den påkalle andre filtre i kjeden.

Løsning

Reglene for et mønster eller et rammeverk skal håndheves gjennom grensesnitt, ikke dokumentasjon. Å stole på at utviklere husker regelen, fungerer ikke alltid. Løsningen er å koble beslutningsprosessen mellom kjedekjøringen og forespørselhåndteringen ved å flytte neste () ring til basisklassen. La baseklassen ta avgjørelsen, og la kun underklasser håndtere forespørselen. Ved å styre unna beslutningsprosesser kan underklasser fullstendig fokusere på egen virksomhet, og dermed unngå feilen beskrevet ovenfor.

Klassisk CoR: Send forespørsel gjennom kjeden til en node håndterer forespørselen

Dette er implementeringen jeg foreslår for det klassiske CoR:

 / ** * Classic CoR, dvs. forespørselen håndteres av bare en av håndtererne i kjeden. * / public abstract class ClassicChain {/ ** * Neste node i kjeden. * / private ClassicChain neste; offentlig ClassicChain (ClassicChain nextNode) {next = nextNode; } / ** * Startpunktet til kjeden, kalt av klient eller pre-node. * Ring håndtaket () på denne noden, og bestem om du vil fortsette kjeden. Hvis neste node ikke er null og * denne noden ikke håndterte forespørselen, ring start () på neste node for å håndtere forespørselen. * @param be forespørselsparameteren * / offentlig endelig ugyldig start (ARforespørsel) {boolean handledByThisNode = this.handle (forespørsel); hvis (neste! = null &&! handledByThisNode) neste.start (forespørsel); } / ** * Kalt etter start (). * @param be om forespørselsparameteren * @retur en boolsk indikerer om denne noden håndterte forespørselen * / beskyttet abstrakt boolsk håndtak (ARequest request); } offentlig klasse AClassicChain utvider ClassicChain {/ ** * Kalt av start (). * @param forespørsel om forespørselsparameteren * @retur en boolsk indikerer om denne noden håndterte forespørselen * / beskyttet boolsk håndtak (ARequest-forespørsel) {boolsk handledByThisNode = false; if (someCondition) {// Gjør håndtering handlingByThisNode = true; } return handlingByThisNode; }} 

Implementeringen avkobler beslutningstagningslogikken og forespørselhåndteringen ved å dele dem inn i to separate metoder. Metode start() tar kjedekjøringen og håndtak() håndterer forespørselen. Metode start() er kjedeutførelsens utgangspunkt. Det kaller håndtak() på denne noden og bestemmer om kjeden skal videreføres til neste node basert på om denne noden håndterer forespørselen og om en node er ved siden av den. Hvis den nåværende noden ikke håndterer forespørselen, og den neste noden ikke er null, er den nåværende noden start() metoden fremmer kjeden ved å ringe start() på neste node eller stopper kjeden forbi ikke ringer start() på neste node. Metode håndtak() i baseklassen blir erklært abstrakt, og gir ingen standardhåndteringslogikk, som er underklassespesifikk og ikke har noe å gjøre med beslutningstaking av kjedeutførelse. Underklasser overstyrer denne metoden og returnerer en boolsk verdi som indikerer om underklassene håndterer forespørselen selv. Vær oppmerksom på at Boolean som returneres av en underklasse informerer start() i basisklassen om underklassen har håndtert forespørselen, ikke om kjeden skal fortsette. Beslutningen om å fortsette kjeden er helt opp til baseklassen start() metode. Underklassene kan ikke endre logikken som er definert i start() fordi start() blir erklært endelig.

I denne implementeringen gjenstår et vindu med muligheter, slik at underklassene kan ødelegge kjeden ved å returnere en utilsiktet boolsk verdi. Imidlertid er denne designen mye bedre enn den gamle versjonen, fordi metodesignaturen håndhever verdien som returneres av en metode; feilen blir fanget på kompileringstidspunktet. Utviklere er ikke lenger pålagt å huske å enten lage neste () ring eller returner en boolsk verdi i koden.

Ikke-klassisk CoR 1: Send forespørsel gjennom kjeden til en node vil stoppe

Denne typen implementering av CoR er en liten variasjon av det klassiske CoR-mønsteret. Kjeden stopper ikke fordi en node har håndtert forespørselen, men fordi en node vil stoppe. I så fall gjelder den klassiske RU-implementeringen også her, med en liten konseptuell endring: det boolske flagget returnert av håndtak() metoden angir ikke om forespørselen er behandlet. Snarere forteller det basisklassen om kjeden skal stoppes. Servettfilterrammeverket passer i denne kategorien. I stedet for å tvinge individuelle filtre til å ringe chain.doFilter (), tvinger den nye implementeringen det enkelte filteret til å returnere en boolsk, som er inngått av grensesnittet, noe utvikleren aldri glemmer eller savner.

Ikke-klassisk CoR 2: Uansett håndtering av forespørsler, send forespørsel til alle håndterere

For denne typen implementering av RU, håndtak() trenger ikke å returnere den boolske indikatoren, fordi forespørselen blir sendt til alle håndtere uansett. Denne implementeringen er enklere. Fordi Microsoft Windows globale hook-rammeverk av natur tilhører denne typen CoR, bør følgende implementering fikse smutthullet:

 / ** * Ikke-klassisk CoR 2, dvs. forespørselen sendes til alle håndtere uavhengig av håndtering. * / public abstract class NonClassicChain2 {/ ** * Neste node i kjeden. * / private NonClassicChain2 neste; offentlig NonClassicChain2 (NonClassicChain2 nextNode) {neste = nextNode; } / ** * Startpunktet til kjeden, kalt av klient eller pre-node. * Ring håndtak () på denne noden, og ring deretter start () på neste node hvis neste node eksisterer. * @param be forespørselsparameteren * / offentlig endelig ugyldig start (ARforespørsel) {this.handle (request); hvis (neste! = null) neste.start (forespørsel); } / ** * Kalt etter start (). * @param be om forespørselsparameteren * / beskyttet abstrakt ugyldig håndtak (ARequest request); } offentlig klasse ANonClassicChain2 utvider NonClassicChain2 {/ ** * Kalt av start (). * @param be om forespørselsparameteren * / beskyttet ugyldig håndtak (ARequest request) {// Gjør håndtering. }} 

Eksempler

I denne delen vil jeg vise deg to kjedeeksempler som bruker implementeringen for ikke-klassisk CoR 2 beskrevet ovenfor.

Eksempel 1

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