Programmering

Introduktion til designmønstre, Del 2: Gang-of-four klassikere revideret

I del 1 af denne tredelte serie, der introducerer designmønstre, henviste jeg til Designmønstre: Elementer af genanvendeligt objektorienteret design. Denne klassiker blev skrevet af Erich Gamma, Richard Helm, Ralph Johnson og John Vlissides, der kollektivt blev kendt som Gang of Four. Som de fleste læsere vil vide, Designmønstre præsenterer 23 softwaredesignmønstre, der passer ind i de kategorier, der er diskuteret i del 1: Skabelses-, strukturel og adfærdsmæssig.

Design mønstre på JavaWorld

David Gearys Java-designmønstre er en mesterlig introduktion til mange af Gang of Four-mønstre i Java-kode.

Designmønstre er kanonisk læsning for softwareudviklere, men mange nye programmører udfordres af dets referenceformat og omfang. Hvert af de 23 mønstre er beskrevet detaljeret i et skabelonformat, der består af 13 sektioner, som kan være meget at fordøje. En anden udfordring for nye Java-udviklere er, at Gang of Four-mønstrene udspringer af objektorienteret programmering med eksempler baseret på C ++ og Smalltalk, ikke Java-kode.

I denne vejledning udpakker jeg to af de almindeligt anvendte mønstre - Strategi og besøgende - fra en Java-udviklers perspektiv. Strategi er et ret simpelt mønster, der fungerer som et eksempel på, hvordan du får dine fødder våde med GoF-designmønstre generelt; Besøgende er mere kompleks og mellemliggende. Jeg starter med et eksempel, der skal demystificere den dobbelte forsendelsesmekanisme, som er en vigtig del af besøgende-mønsteret. Derefter demonstrerer jeg besøgende mønster i en compiler brugssag.

Efter mine eksempler her skal det hjælpe dig med at udforske og bruge de andre GoF-mønstre til dig selv. Derudover vil jeg give tip til at få mest muligt ud af bogen Gang of Four og afslutte med et resumé af kritik af brugen af ​​designmønstre til softwareudvikling. Denne diskussion kan være særlig relevant for udviklere, der er nye inden for programmering.

Udpakningsstrategi

Det Strategi mønster lader dig definere en familie af algoritmer som dem, der bruges til sortering, tekstsammensætning eller layoutadministration. Strategi lader dig også indkapsle hver algoritme i sin egen klasse og gøre dem udskiftelige. Hver indkapslet algoritme er kendt som en strategi. Ved runtime vælger en klient den passende algoritme til sine krav.

Hvad er en klient?

EN klient er ethvert stykke software, der interagerer med et designmønster. Selvom det typisk er et objekt, kan en klient også kode inden for en applikations offentlig statisk ugyldig hoved (String [] args) metode.

I modsætning til dekoratørmønsteret, der fokuserer på at ændre et objekt hudeller udseende, Strategi fokuserer på at ændre objektets tarme, hvilket betyder dens ændrede adfærd. Strategi giver dig mulighed for at undgå at bruge flere betingede udsagn ved at flytte betingede grene til deres egne strategiklasser. Disse klasser stammer ofte fra en abstrakt superklasse, som klienten refererer til og bruger til at interagere med en bestemt strategi.

Fra et abstrakt perspektiv involverer strategi Strategi, Betonstrategixog Sammenhæng typer.

Strategi

Strategi giver en fælles grænseflade til alle understøttede algoritmer. Liste 1 præsenterer Strategi interface.

Notering 1. ugyldig udførelse (int x) skal implementeres af alle konkrete strategier

offentlig grænseflade Strategi {public void execute (int x); }

Hvor konkrete strategier ikke er parametreret med almindelige data, kan du implementere dem via Java'er interface funktion. Hvor de er parametreret, vil du i stedet erklære en abstrakt klasse. For eksempel deler højrejustering, centerjustering og retfærdiggørelse af strategier for tekstjustering konceptet med en bredde til at udføre tekstjustering. Så du ville erklære dette bredde i den abstrakte klasse.

Betonstrategix

Hver Betonstrategix implementerer den fælles grænseflade og giver en algoritmeimplementering. Notering 2 redskaber Listing 1'er Strategi interface til at beskrive en specifik konkret strategi.

Notering 2. ConcreteStrategyA udfører en algoritme

offentlig klasse ConcreteStrategyA implementerer strategi {@ Override public void execute (int x) {System.out.println ("eksekveringsstrategi A: x =" + x); }}

Det ugyldig udførelse (int x) metode i liste 2 identificerer en bestemt strategi. Tænk på denne metode som en abstraktion for noget mere nyttigt, som en bestemt slags sorteringsalgoritme (fx Bubblesortering, Indsætningssortering eller Hurtig sortering) eller en bestemt form for layouthåndtering (f.eks. Flow Layout, Border Layout eller Gitterlayout).

Liste 3 viser et sekund Strategi implementering.

Notering 3. ConcreteStrategyB udfører en anden algoritme

offentlig klasse ConcreteStrategyB implementerer strategi {@ Override public void execute (int x) {System.out.println ("eksekveringsstrategi B: x =" + x); }}

Sammenhæng

Sammenhæng giver den sammenhæng, hvori den konkrete strategi påberåbes. Liste 2 og 3 viser data, der overføres fra en kontekst til en strategi via en metodeparameter. Da en generisk strategigrænseflade deles af alle konkrete strategier, kræver nogle af dem muligvis ikke alle parametre. For at undgå spildte parametre (især når du overfører mange forskellige slags argumenter til kun få konkrete strategier), kan du i stedet sende en henvisning til konteksten.

I stedet for at sende en kontekstreference til metoden, kan du gemme den i den abstrakte klasse, hvilket gør din metode kald parameterløs. Imidlertid skulle sammenhængen specificere en mere omfattende grænseflade, der ville omfatte kontrakten om adgang til kontekstdata på en ensartet måde. Resultatet, som vist i liste 4, er en strammere kobling mellem strategier og deres sammenhæng.

Fortegnelse 4. Kontekst er konfigureret med en ConcreteStrategyx-forekomst

klassekontekst {privat strategistrategi; public Context (Strategistrategi) {setStrategy (strategi); } public void executeStrategy (int x) {strategy.execute (x); } public void setStrategy (Strategistrategi) {this.strategy = strategi; }}

Det Sammenhæng klasse i liste 4 gemmer en strategi, når den oprettes, giver en metode til efterfølgende at ændre strategien og giver en anden metode til at udføre den aktuelle strategi. Bortset fra at sende en strategi til konstruktøren kan dette mønster ses i java.awt .Container-klassen, hvis void setLayout (LayoutManager mgr) og ugyldig doLayout () metoder specificerer og udfører layout manager strategi.

StrategiDemo

Vi har brug for en klient til at demonstrere de tidligere typer. Liste 5 præsenterer a StrategiDemo klientklasse.

Notering 5. StrategiDemo

public class StrategyDemo {public static void main (String [] args) {Context context = new Context (new ConcreteStrategyA ()); context.executeStrategy (1); context.setStrategy (ny ConcreteStrategyB ()); context.executeStrategy (2); }}

En konkret strategi er forbundet med en Sammenhæng eksempel, når konteksten oprettes. Strategien kan efterfølgende ændres via et kontekstmetodeopkald.

Hvis du kompilerer disse klasser og løber StrategiDemo, skal du overholde følgende output:

udførelsesstrategi A: x = 1 udførelsesstrategi B: x = 2

Genbesøg af besøgende mønster

Besøgende er det sidste mønster til softwaredesign, der vises i Designmønstre. Selv om dette adfærdsmønster præsenteres sidst i bogen af ​​alfabetiske årsager, mener nogle, at det skal komme sidst på grund af dets kompleksitet. Begynderne hos besøgende kæmper ofte med dette softwaredesignmønster.

Som forklaret i Designmønstre, lader en besøgende dig føje operationer til klasser uden at ændre dem, en smule magi, der letter den såkaldte dobbeltforsendelsesteknik. For at forstå besøgermønsteret skal vi først fordøje dobbelt forsendelse.

Hvad er dobbelt forsendelse?

Java og mange andre sprog understøtter polymorfisme (mange former) via en teknik kendt som dynamisk forsendelse, hvor en meddelelse kortlægges til en bestemt kodesekvens ved kørsel. Dynamisk forsendelse klassificeres som enten enkelt eller flere forsendelser:

  • Enkel forsendelse: Givet et klassehierarki, hvor hver klasse implementerer den samme metode (det vil sige, at hver underklasse tilsidesætter den foregående klasses version af metoden) og givet en variabel, der er tildelt en forekomst af en af ​​disse klasser, kan typen kun regnes ud ved runtime. Antag for eksempel, at hver klasse implementerer metode Print(). Antag også, at en af ​​disse klasser instantieres ved kørsel, og at dens variabel tildeles variabel -en. Når Java-kompileren møder a.print ();, det kan kun bekræfte det -en's type indeholder en Print() metode. Det ved ikke, hvilken metode der skal ringes til. Ved kørsel undersøger den virtuelle maskine referencen i variablen -en og viser den aktuelle type for at kalde den rigtige metode. Denne situation, hvor en implementering er baseret på en enkelt type (typen af ​​forekomsten), er kendt som enkelt forsendelse.
  • Flere forsendelser: I modsætning til en enkelt forsendelse, hvor et enkelt argument bestemmer hvilken metode med det navn, der skal påberåbes, flere forsendelser bruger alle sine argumenter. Med andre ord generaliserer den dynamisk forsendelse til at arbejde med to eller flere objekter. (Bemærk, at argumentet i en enkelt forsendelse typisk er specificeret med en periodeseparator til venstre for metodens navn, der kaldes, f.eks. -en i a.print ().)

Langt om længe, dobbelt forsendelse er et specielt tilfælde af flere forsendelser, hvor runtime-typerne af to objekter er involveret i opkaldet. Selvom Java understøtter enkelt forsendelse, understøtter det ikke dobbelt forsendelse direkte. Men vi kan simulere det.

Stoler vi alt for på dobbelt forsendelse?

Blogger Derek Greer mener, at brug af dobbelt forsendelse kan indikere et designproblem, som kan påvirke en applikations vedligeholdelse. Læs Greers blogindlæg "Dobbelt forsendelse er en kodelugt" og tilhørende kommentarer for detaljer.

Simulering af dobbelt forsendelse i Java-kode

Wikipedia's post om dobbelt forsendelse giver et C ++ - baseret eksempel, der viser, at det er mere end funktionsoverbelastning. I liste 6 præsenterer jeg Java-ækvivalenten.

Liste 6. Dobbelt forsendelse i Java-kode

public class DDDemo {public static void main (String [] args) {Asteroid theAsteroid = new Asteroid (); SpaceShip theSpaceShip = nyt SpaceShip (); ApolloSpacecraft theApolloSpacecraft = nyt ApolloSpacecraft (); theAsteroid.collideWith (theSpaceShip); theAsteroid.collideWith (theApolloSpacecraft); System.out.println (); ExplodingAsteroid theExplodingAsteroid = ny ExplodingAsteroid (); theExplodingAsteroid.collideWith (theSpaceShip); theExplodingAsteroid.collideWith (theApolloSpacecraft); System.out.println (); Asteroid theAsteroidReference = theExplodingAsteroid; theAsteroidReference.collideWith (theSpaceShip); theAsteroidReference.collideWith (theApolloSpacecraft); System.out.println (); SpaceShip theSpaceShipReference = thePolloSpacecraft; theAsteroid.collideWith (theSpaceShipReference); theAsteroidReference.collideWith (theSpaceShipReference); System.out.println (); theSpaceShipReference = theApolloSpacecraft; theAsteroidReference = theExplodingAsteroid; theSpaceShipReference.collideWith (theAsteroid); theSpaceShipReference.collideWith (theAsteroidReference); }} klasse SpaceShip {void collideWith (Asteroid inAsteroid) {inAsteroid.collideWith (this); }} klasse ApolloSpacecraft udvider SpaceShip {void collideWith (Asteroid inAsteroid) {inAsteroid.collideWith (this); }} klasse Asteroid {void collideWith (SpaceShip s) {System.out.println ("Asteroid hit a SpaceShip"); } void collideWith (ApolloSpacecraft as) {System.out.println ("Asteroid ramte et ApolloSpacecraft"); }} klasse ExplodingAsteroid udvider Asteroid {void collideWith (SpaceShip s) {System.out.println ("ExplodingAsteroid hit a SpaceShip"); } void collideWith (ApolloSpacecraft as) {System.out.println ("ExplodingAsteroid ramte et ApolloSpacecraft"); }}

Listing 6 følger dets C ++ -modstykke så tæt som muligt. De sidste fire linjer i hoved () metode sammen med void collideWith (Asteroid in Asteroid) metoder i Rumskib og ApolloSpacecraft demonstrere og simulere dobbelt forsendelse.

Overvej følgende uddrag fra slutningen af hoved ():

theSpaceShipReference = theApolloSpacecraft; theAsteroidReference = theExplodingAsteroid; theSpaceShipReference.collideWith (theAsteroid); theSpaceShipReference.collideWith (theAsteroidReference);

Den tredje og fjerde linje bruger en enkelt forsendelse til at finde ud af den korrekte støde sammen med() metode (i Rumskib eller ApolloSpacecraft) at påberåbe sig. Denne beslutning træffes af den virtuelle maskine baseret på typen af ​​referencen, der er gemt i theSpaceShipReference.

Indefra støde sammen med(), inAsteroid.collideWith (dette); bruger en enkelt forsendelse til at finde ud af den korrekte klasse (Asteroide eller Eksploderende asteroide) indeholdende det ønskede støde sammen med() metode. Fordi Asteroide og Eksploderende asteroide overbelaste støde sammen med(), typen af ​​argument det her (Rumskib eller ApolloSpacecraft) bruges til at skelne mellem det korrekte støde sammen med() metode til at ringe til.

Og med det har vi opnået dobbelt forsendelse. For at resumere ringede vi først støde sammen med() i Rumskib eller ApolloSpacecraft, og brugte derefter sit argument og det her at ringe til en af støde sammen med() metoder i Asteroide eller Eksploderende asteroide.

Når du løber DDDemo, skal du overholde følgende output: