Strategimønsteret forklart ved hjelp av Java

I dette innlegget vil jeg snakke om et av de populære designmønstrene - Strategimønsteret. Hvis du ikke allerede er klar over det, er designmønstrene en rekke objektorienterte programmeringsprinsipper opprettet av bemerkelsesverdige navn i programvareindustrien, ofte referert til som Gang of Four (GoF). Disse designmønstrene har hatt en enorm innvirkning på programvarens økosystem og brukes til dags dato for å løse vanlige problemer i Objektorientert programmering.

La oss formelt definere strategimønsteret:

Strategimønsteret definerer en familie av algoritmer, innkapsler hver og gjør dem utskiftbare. Strategi lar algoritmen variere uavhengig av klientene som bruker den

OK med det ute av veien, la oss dykke ned i noen koder for å forstå hva disse ordene egentlig betyr. Vi tar et eksempel med en potensiell fallgruve og bruker strategimønsteret for å se hvordan det overvinner problemet.

Jeg skal vise deg hvordan du lager et dophundsimulatorprogram for å lære deg strategimønsteret. Her er hvordan klassene våre vil se ut: En 'Dog' superklasse med vanlig oppførsel og deretter konkrete hundeklasser opprettet ved å underklasse Dog-klassen.

Slik ser koden ut

public abstract class Dog { public abstract void display(); //different dogs have different looks! public void eat(){} public void bark(){} // Other dog-like methods ... }

Display () -metoden er abstrakt siden forskjellige hunder har forskjellige utseende. Alle de andre underklassene vil arve spiser og bark atferd eller overstyre den med sin egen implementering. Så langt så bra!

Nå, hva om du ville legge til litt ny oppførsel? La oss si at du trenger en kul robothund som kan gjøre alle slags triks. Ikke et problem, vi trenger bare å legge til en performTricks () -metode i hundens superklasse, og vi er gode å gå.

Men vent litt ... En robothund burde ikke kunne spise riktig? Livløse objekter kan selvfølgelig ikke spise. Greit, hvordan løser vi dette problemet da? Vel, vi kan overstyre eat () -metoden for ikke å gjøre noe, og det fungerer helt fint!

public class RobotDog extends Dog { @override public void eat(){} // Do nothing }

Bra gjort! Nå kan ikke robothunder spise, de kan bare bjeffe eller utføre triks. Hva med gummihunder? De kan ikke spise eller utføre triks. Og trehunder kan ikke spise, bjeffe eller utføre triks. Vi kan ikke alltid overstyre metoder for å gjøre ingenting, det er ikke rent og det føles bare hacky. Tenk deg å gjøre dette på et prosjekt hvis designspesifikasjon stadig endres noen få måneder. Vårt er bare et naivt eksempel, men du får ideen. Så vi må finne en renere måte å løse dette problemet på.

Kan grensesnittet løse problemet vårt?

Hva med grensesnitt? La oss se om de kan løse problemet vårt. OK, så vi lager et CanEat og et CanBark-grensesnitt:

interface CanEat { public void eat(); } interface CanBark { public void bark(); }

Vi har nå fjernet bark () og eat () -metodene fra Dog-superklassen og lagt dem til de respektive grensesnittene. Slik at bare hundene som kan bjeffe vil implementere CanBark-grensesnittet og hundene som kan spise vil implementere CanEat-grensesnittet. Nå, ikke lenger bekymre deg for at hunder arver atferd som de ikke burde, problemet vårt er løst ... eller er det?

Hva skjer når vi må gjøre en endring i hundens spiseatferd? La oss si at fra nå av må hver hund inkludere en mengde protein sammen med måltidet. Du må nå endre eat () -metoden til alle underklassene til Dog. Hva om det er 50 slike klasser, skrekk!

Så grensesnitt løser bare delvis vårt problem med at hunder bare gjør det de er i stand til å gjøre - men de skaper et annet problem helt. Grensesnitt har ikke implementeringskode, så det er null gjenbrukbarhet og potensial for mange dupliserte koder. Hvordan løser vi dette spør du? Strategimønster kommer til unnsetning!

Strategimønsteret

Så vi vil gjøre dette trinn for trinn. Før vi fortsetter, la meg introdusere deg for et designprinsipp:

Identifiser delene av programmet som varierer, og skille dem fra det som forblir det samme.

Det er faktisk veldig greit - prinsippet sier å skille og "kapsle inn" alt som endres ofte, slik at all koden som endres bor på ett sted. På den måten vil koden som endres ikke ha noen innvirkning på resten av programmet og applikasjonen vår er mer fleksibel og robust.

I vårt tilfelle kan 'bark' og 'spise' oppførsel tas ut av hundeklassen og kan innkapsles andre steder. Vi vet at denne oppførselen varierer fra forskjellige hunder, og de må få sin egen klasse.

Vi skal lage to sett med klasser bortsett fra hundeklassen, en for å definere spiseatferd og en for bjeffende atferd. Vi vil bruke grensesnitt for å representere oppførselen som 'EatBehavior' og 'BarkBehavior', og den konkrete atferdsklassen vil implementere disse grensesnittene. Så, hundeklassen implementerer ikke grensesnittet lenger. Vi lager separate klasser hvis eneste jobb er å representere den spesifikke oppførselen!

Slik ser EatBehavior-grensesnittet ut

interface EatBehavior { public void eat(); }

Og Bark Behavior

interface BarkBehavior { public void bark(); }

Alle klassene som representerer denne oppførselen vil implementere det respektive grensesnittet.

Betongklasser for BarkBehavior

public class PlayfulBark implements BarkBehavior { @override public void bark(){ System.out.println("Bark! Bark!"); } } public class Growl implements BarkBehavior { @override public void bark(){ System.out.println("This is a growl"); } public class MuteBark implements BarkBehavior { @override public void bark(){ System.out.println("This is a mute bark"); }

Betongklasser for EatBehavior

public class NormalDiet implements EatBehavior { @override public void eat(){ System.out.println("This is a normal diet"); } } public class ProteinDiet implements EatBehavior { @override public void eat(){ System.out.println("This is a protein diet"); } }

Nå mens vi gjør konkrete implementeringer ved å underklasse superklassen 'Dog', vil vi naturligvis kunne tildele atferdene dynamisk til hundenes forekomster. Tross alt var det ufleksibiliteten til den forrige koden som forårsaket problemet. Vi kan definere settermetoder på Dog-underklassen som gjør at vi kan angi forskjellige atferd ved kjøretid.

Det bringer oss til et annet designprinsipp:

Program til et grensesnitt og ikke en implementering.

Hva dette betyr er at i stedet for å bruke de konkrete klassene, bruker vi variabler som er supertyper av disse klassene. Med andre ord bruker vi variabler av typen EatBehavior og BarkBehavior og tilordner disse variablene objekter av klasser som implementerer denne oppførselen. På den måten trenger ikke hundeklassene å ha noen informasjon om de faktiske objekttypene til disse variablene!

For å gjøre konseptet klart her er et eksempel som skiller de to måtene - Tenk på en abstrakt dyreklasse som har to konkrete implementeringer, hund og katt.

Programmering til en implementering vil være:

Dog d = new Dog(); d.bark();

Slik ser programmering til et grensesnitt ut:

Animal animal = new Dog(); animal.animalSound();

Her vet vi at dyr inneholder en forekomst av en 'hund', men vi kan bruke denne referansen polymorfisk overalt ellers i koden vår. Alt vi bryr oss om er at dyreinstansen er i stand til å svare på animalSound () -metoden og den riktige metoden, avhengig av objektet som tildeles, blir ringt.

Det var mye å ta inn. Uten nærmere forklaring, la oss se hvordan superklassen vår 'Dog' ser ut nå:

public abstract class Dog { EatBehavior eatBehavior; BarkBehaviour barkBehavior; public Dog(){} public void doBark() { barkBehavior.bark(); } public void doEat() { eatBehavior.eat(); } }

Vær nøye med metodene i denne klassen. Hundeklassen 'delegerer' nå oppgaven med å spise og bjeffe i stedet for å implementere av seg selv eller arve den (underklasse). I doBark () -metoden kaller vi bare bark () -metoden på objektet det refereres til av barkBehavior. Nå bryr vi oss ikke om objektets faktiske type, vi bryr oss bare om det vet å bjeffe!

Nå, sannhetens øyeblikk, la oss lage en konkret hund!

public class Labrador extends Dog { public Labrador(){ barkBehavior = new PlayfulBark(); eatBehavior = new NormalDiet(); } public void display(){ System.out.println("I'm a playful Labrador"); } ... }

Hva skjer i konstruktøren til Labrador-klassen? vi tilordner de konkrete tilfellene til supertypen (husk at grensesnitttypene er arvet fra Dog superklassen). Nå, når vi kaller doEat () på Labrador-forekomsten, blir ansvaret overlevert til ProteinDiet-klassen, og den utfører metoden eat ().

Strategimønsteret i aksjon

Ok, la oss se dette i aksjon. Tiden er inne for å kjøre vårt dope Dog simulator-program!

public class DogSimulatorApp { public static void main(String[] args) { Dog lab = new Labrador(); lab.doEat(); // Prints "This is a normal diet" lab.doBark(); // "Bark! Bark!" } }

Hvordan kan vi gjøre dette programmet bedre? Ved å legge til fleksibilitet! La oss legge til settermetoder på hundeklassen for å kunne bytte atferd ved kjøretid. La oss legge til to metoder til Dog superklassen:

public void setEatBehavior(EatBehavior eb){ eatBehavior = eb; } public void setBarkBehavior(BarkBehavior bb){ barkBehavior = bb; }

Nå kan vi endre programmet vårt og velge hvilken oppførsel vi liker når du kjører!

public class DogSimulatorApp { public static void main(String[] args){ Dog lab = new Labrador(); lab.doEat(); // This is a normal diet lab.setEatBehavior(new ProteinDiet()); lab.doEat(); // This is a protein diet lab.doBark(); // Bark! Bark! } }

La oss se på det store bildet:

Vi har hundeklassen og klassen 'Labrador' som er en underklasse av hunden. Så har vi familien av algoritmer (Behaviors) "innkapslet" med deres respektive atferdstyper.

Ta en titt på den formelle definisjonen jeg ga i begynnelsen: algoritmene er ikke annet enn atferdsgrensesnittene. Nå kan de brukes ikke bare i dette programmet, men andre programmer kan også bruke det. Legg merke til forholdet mellom klassene i diagrammet. Forholdet IS-A og HAS-A kan utledes fra diagrammet.

Det er det! Jeg håper du har fått en stor oversikt over strategimønsteret. Strategimønsteret er ekstremt nyttig når du har visse atferd i appen din som endrer seg konstant.

Dette bringer oss til slutten av Java-implementeringen. Tusen takk for at du har holdt fast med meg så langt! Hvis du er interessert i å lære om Kotlin-versjonen, følg med på neste innlegg. Jeg snakker om interessante språkfunksjoner og hvordan vi kan redusere alle ovennevnte koder i en enkelt Kotlin-fil :)

PS

Jeg har lest Head First Design Patterns-boken, og det meste av dette innlegget er inspirert av innholdet. Jeg vil anbefale denne boken på det sterkeste til alle som leter etter en mild introduksjon til Design Patterns.