Strategimønsteret forklaret ved hjælp af Java

I dette indlæg vil jeg tale om et af de populære designmønstre - Strategimønsteret. Hvis du ikke allerede er opmærksom på det, er designmønstrene en masse objektorienterede programmeringsprincipper oprettet af bemærkelsesværdige navne i softwareindustrien, ofte kaldet Gang of Four (GoF). Disse designmønstre har haft en enorm indflydelse på softwareøkosystemet og bruges til dato til at løse almindelige problemer i Objektorienteret programmering.

Lad os formelt definere strategimønsteret:

Strategimønsteret definerer en familie af algoritmer, indkapsler hver enkelt og gør dem udskiftelige. Strategi lader algoritmen variere uafhængigt af de klienter, der bruger den

Okay med det ude af vejen, lad os dykke ned i en kode for at forstå, hvad disse ord virkelig betyder. Vi tager et eksempel med en potentiel faldgrube og anvender derefter strategimønsteret for at se, hvordan det overvinder problemet.

Jeg viser dig, hvordan du opretter et dope dog-simulatorprogram for at lære strategimønsteret. Her er hvordan vores klasser vil se ud: En 'Hund' superklasse med almindelig adfærd og derefter konkrete hundeklasser oprettet ved at underklasse hundeklassen.

Sådan ser koden ud

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, da forskellige hunde har forskellige udseende. Alle de andre underklasser vil arve spise- og barkadfærd eller tilsidesætte det med deres egen implementering. Så langt så godt!

Hvad nu hvis du vil tilføje noget nyt? Lad os sige, at du har brug for en sej robothund, der kan gøre alle mulige tricks. Ikke et problem, vi skal bare tilføje en performTricks () -metode i vores hundesuperklasse, og vi er klar til at gå.

Men vent et øjeblik ... En robothund burde ikke være i stand til at spise rigtigt? Livløse genstande kan selvfølgelig ikke spise. Okay, hvordan løser vi dette problem da? Nå, vi kan tilsidesætte eat () -metoden for ikke at gøre noget, og det fungerer fint!

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

Godt gjort! Nu kan robothunde ikke spise, de kan kun gø eller udføre tricks. Hvad med gummihunde? De kan ikke spise eller udføre tricks. Og træhunde kan ikke spise, gø eller udføre tricks. Vi kan ikke altid tilsidesætte metoder til ikke at gøre noget, det er ikke rent, og det føles bare hacky. Forestil dig at gøre dette på et projekt, hvis designspecifikation bliver ved med at ændre sig hvert par måneder. Vores er bare et naivt eksempel, men du får ideen. Så vi er nødt til at finde en renere måde at løse dette problem på.

Kan grænsefladen løse vores problem?

Hvad med grænseflader? Lad os se, om de kan løse vores problem. Okay, så vi opretter en CanEat og en CanBark interface:

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

Vi har nu fjernet bark () og eat () metoder fra Dog superklassen og føjet dem til de respektive grænseflader. Så kun hunde, der kan gø, implementerer CanBark-grænsefladen, og hundene, der kan spise, implementerer CanEat-grænsefladen. Nu, ikke mere bekymre dig om, at hunde arver adfærd, som de ikke burde, vores problem er løst ... eller er det?

Hvad sker der, når vi skal foretage en ændring i hundens spiseadfærd? Lad os sige, at hunde fra nu af skal indeholde en vis mængde protein sammen med deres måltid. Du er nu nødt til at ændre eat () -metoden i alle underklasser af Dog. Hvad hvis der er 50 sådanne klasser, åh rædslen!

Så grænseflader løser kun delvist vores problem med, at hunde kun gør, hvad de er i stand til at gøre - men de skaber et andet problem helt. Grænseflader har ingen implementeringskode, så der er nul genanvendelighed og potentiale for masser af duplikatkode. Hvordan løser vi dette, spørger du? Strategimønster kommer til undsætning!

Strategimønsteret

Så vi vil gøre dette trin for trin. Lad mig introducere dig til et designprincip, inden vi fortsætter:

Identificer de dele af dit program, der varierer, og adskil dem fra det, der forbliver det samme.

Det er faktisk meget ligetil - princippet siger at adskille og "indkapsle" alt, hvad der ændres ofte, så al den kode, der ændres, bor ét sted. På den måde vil koden, der ændres, ikke have nogen indvirkning på resten af ​​programmet, og vores applikation er mere fleksibel og robust.

I vores tilfælde kan 'bark' og 'spise' adfærd tages ud af hundeklassen og kan indkapsles andre steder. Vi ved, at denne adfærd varierer på tværs af forskellige hunde, og de skal få deres egen separate klasse.

Vi skal skabe to sæt klasser bortset fra hundeklassen, en til at definere spiseadfærd og en til gøende opførsel. Vi bruger grænseflader til at repræsentere adfærd som 'EatBehavior' og 'BarkBehavior', og den konkrete adfærdsklasse implementerer disse grænseflader. Så hundeklassen implementerer ikke grænsefladen længere. Vi opretter separate klasser, hvis eneste opgave er at repræsentere den specifikke adfærd!

Sådan ser EatBehavior-grænsefladen ud

interface EatBehavior { public void eat(); }

Og Bark Behavior

interface BarkBehavior { public void bark(); }

Alle de klasser, der repræsenterer denne adfærd, implementerer den respektive grænseflade.

Betonkurser til 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"); }

Konkrete klasser til 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"); } }

Mens vi foretager konkrete implementeringer ved at underklasse superklassen 'Dog', vil vi naturligvis være i stand til at tildele adfærdene dynamisk til hundenes tilfælde. Det var trods alt ufleksibiliteten af ​​den tidligere kode, der forårsagede problemet. Vi kan definere settermetoder i underklassen Dog, der giver os mulighed for at indstille forskellige adfærd ved runtime.

Det bringer os til et andet designprincip:

Programmer til en grænseflade og ikke til en implementering.

Hvad dette betyder er, at i stedet for at bruge de konkrete klasser bruger vi variabler, der er supertyper af disse klasser. Med andre ord bruger vi variabler af typen EatBehavior og BarkBehavior og tildeler disse variabler objekter af klasser, der implementerer denne adfærd. På den måde behøver hundeklasser ikke at have nogen information om de faktiske objekttyper af disse variabler!

For at gøre konceptet klart her er et eksempel, der adskiller de to måder - Overvej en abstrakt dyreklasse, der har to konkrete implementeringer, hund og kat.

Programmering til en implementering vil være:

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

Sådan ser programmering til en grænseflade ud:

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

Her ved vi, at dyr indeholder en forekomst af en 'hund', men vi kan bruge denne reference polymorf overalt i vores kode. Alt, hvad vi holder af, er, at dyreinstansen er i stand til at reagere på animalSound () -metoden, og den relevante metode, afhængigt af det tildelte objekt, bliver kaldt.

Det var meget at tage i. Uden yderligere forklaring, lad os se, hvordan vores 'Dog' superklasse ser ud nu:

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

Vær meget opmærksom på metoderne i denne klasse. Hundeklassen 'delegerer' nu opgaven med at spise og gø i stedet for at implementere det selv eller arve det (underklasse). I metoden doBark () kalder vi simpelthen metoden bark () på det objekt, der refereres til af barkBehavior. Nu er vi ligeglad med genstandens faktiske type, vi er kun interesserede i, om den ved, hvordan man gøer!

Nu sandhedens øjeblik, lad os oprette 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"); } ... }

Hvad sker der i konstruktøren af ​​Labrador-klassen? vi tildeler supertypen de konkrete forekomster (husk, at interface-typerne er arvet fra Dog superklassen). Når vi nu kalder doEat () på Labrador-forekomsten, overdrages ansvaret til ProteinDiet-klassen, og det udfører metoden eat ().

Strategimønsteret i aktion

Okay, lad os se dette i aktion. Tiden er kommet til at køre vores 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 gøre dette program bedre? Ved at tilføje fleksibilitet! Lad os tilføje setter-metoder på hundeklassen for at kunne bytte adfærd ved kørsel. Lad os tilføje to flere metoder til hundens superklasse:

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

Nu kan vi ændre vores program og vælge den adfærd, vi kan lide ved kørsel!

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! } }

Lad os se på det store billede:

Vi har hundens superklasse og klassen 'Labrador', som er en underklasse af hund. Så har vi familien af ​​algoritmer (Behaviors) "indkapslet" med deres respektive adfærdstyper.

Se på den formelle definition, som jeg gav i begyndelsen: algoritmerne er intet andet end adfærdsgrænsefladerne. Nu kan de bruges ikke kun i dette program, men andre programmer kan også gøre brug af det. Bemærk forholdet mellem klasserne i diagrammet. IS-A og HAS-A relationer kan udledes af diagrammet.

Det er det! Jeg håber, du har fået et stort overblik over strategimønsteret. Strategimønsteret er yderst nyttigt, når du har visse adfærd i din app, der konstant ændres.

Dette bringer os til slutningen af ​​Java-implementeringen. Tak så meget for at holde fast ved mig hidtil! Hvis du er interesseret i at lære om Kotlin-versionen, skal du holde øje med det næste indlæg. Jeg taler om interessante sprogfunktioner, og hvordan vi kan reducere al ovenstående kode i en enkelt Kotlin-fil :)

PS

Jeg har læst bogen Head First Design Patterns, og det meste af dette indlæg er inspireret af dets indhold. Jeg vil meget anbefale denne bog til alle, der er på udkig efter en mild introduktion til designmønstre.