Hei! I dagens leksjon skal vi snakke om strategimønster. I tidligere leksjoner har vi allerede kort blitt kjent med begrepet arv. I tilfelle du har glemt det, vil jeg minne deg på at dette begrepet refererer til en standardløsning på en vanlig programmeringsoppgave. Hos CodeGym sier vi ofte at du kan google svaret på nesten alle spørsmål. Dette er fordi oppgaven din, uansett hva den er, sannsynligvis allerede har blitt løst med hell av noen andre. Mønstre er utprøvde løsninger på de vanligste oppgavene, eller metoder for å løse problematiske situasjoner. Disse er som "hjul" som du ikke trenger å finne opp på egen hånd, men du trenger å vite hvordan og når du skal bruke dem :) Et annet formål med mønstre er å fremme enhetlig arkitektur. Å lese andres kode er ingen enkel oppgave! Alle skriver forskjellig kode, fordi samme oppgave kan løses på mange måter. Men bruken av mønstre hjelper forskjellige programmerere å forstå programmeringslogikken uten å fordype seg i hver linje med kode (selv når de ser den for første gang!) I dag ser vi på et av de vanligste designmønstrene kalt "Strategi". Tenk deg at vi skriver et program som aktivt vil jobbe med transportobjekter. Det spiller ingen rolle hva programmet vårt gjør. Vi har opprettet et klassehierarki med én forelderklasse og tre barneklasser: Sedan , Truck og F1Car .
public class Conveyance {
public void go() {
System.out.println("Moving forward");
}
public void stop() {
System.out.println("Braking!");
}
}
public class Sedan extends Conveyance {
}
public class Truck extends Conveyance {
}
public class F1Car extends Conveyance {
}
Alle tre underordnede klasser arver to standardmetoder fra overordnet: go() og stop() . Programmet vårt er veldig enkelt: bilene våre kan bare bevege seg fremover og sette på bremsene. Vi fortsatte arbeidet vårt og bestemte oss for å gi bilene en ny metode: fill() (som betyr "fylle bensintanken"). Vi la den til i klassen Conveyance- foreldre:
public class Conveyance {
public void go() {
System.out.println("Moving forward");
}
public void stop() {
System.out.println("Braking!");
}
public void fill() {
System.out.println("Refueling!");
}
}
Kan det virkelig oppstå problemer i en så enkel situasjon? Faktisk har de allerede...
public class Stroller extends Conveyance {
public void fill() {
// Hmm... This is a stroller for children. It doesn't need to be refueled :/
}
}
Vårt program har nå et transportmiddel (en barnevogn) som ikke passer godt inn i det generelle konseptet. Den kan ha pedaler eller være radiostyrt, men en ting er sikkert - den vil ikke ha noe sted å helle i gass. Klassehierarkiet vårt har ført til at vanlige metoder har blitt arvet av klasser som ikke trenger dem. Hva bør vi gjøre i denne situasjonen? Vel, vi kan overstyre fill() -metoden i Stroller- klassen slik at ingenting skjer når du prøver å fylle drivstoff på vognen:
public class Stroller extends Conveyance {
@Override
public void fill() {
System.out.println("A stroller cannot be refueled!");
}
}
Men dette kan neppe kalles en vellykket løsning uten annen grunn enn duplikatkode. For eksempel vil de fleste klassene bruke den overordnede klassens metode, men resten vil bli tvunget til å overstyre den. Hvis vi har 15 klasser og vi må overstyre atferd i 5-6 av dem, vil kodedupliseringen bli ganske omfattende. Kanskje grensesnitt kan hjelpe oss? For eksempel slik:
public interface Fillable {
public void fill();
}
Vi lager et utfyllbart grensesnitt med én fill()- metode. Deretter vil de transportmidlene som må fylles på, implementere dette grensesnittet, mens andre transportmidler (for eksempel vår barnevogn) ikke vil. Men dette alternativet passer ikke oss. I fremtiden kan klassehierarkiet vårt vokse til å bli veldig stort (tenk deg hvor mange forskjellige typer transportmidler det finnes i verden). Vi forlot den forrige versjonen som involverer arv, fordi vi ikke ønsker å overstyre fill ()metode mange, mange ganger. Nå må vi implementere det i hver klasse! Og hva om vi har 50? Og hvis det vil bli gjort hyppige endringer i programmet vårt (og dette er nesten alltid sant for ekte programmer!), må vi skynde oss gjennom alle 50 klassene og manuelt endre oppførselen til hver av dem. Så hva skal vi til slutt gjøre i denne situasjonen? For å løse problemet vårt, velger vi en annen måte. Vi vil nemlig skille klassens oppførsel fra klassen selv. Hva betyr det? Som du vet, har hvert objekt tilstand (et sett med data) og atferd (et sett med metoder). Vår transportklasses oppførsel består av tre metoder: go() , stop() og fill() . De to første metodene er fine akkurat som de er. Men vi vil flytte den tredje metoden ut avFormidlingsklasse . Dette vil skille atferden fra klassen (mer nøyaktig, det vil skille bare en del av atferden, siden de to første metodene forblir der de er). Så hvor skal vi sette fill()- metoden vår? Ingenting kommer til tankene :/ Det virker som det er akkurat der det skal være. Vi flytter det til et eget grensesnitt: FillStrategy !
public interface FillStrategy {
public void fill();
}
Hvorfor trenger vi et slikt grensesnitt? Det hele er enkelt. Nå kan vi lage flere klasser som implementerer dette grensesnittet:
public class HybridFillStrategy implements FillStrategy {
@Override
public void fill() {
System.out.println("Refuel with gas or electricity — your choice!");
}
}
public class F1PitstopStrategy implements FillStrategy {
@Override
public void fill() {
System.out.println("Refuel with gas only after all other pit stop procedures are complete!");
}
}
public class StandardFillStrategy implements FillStrategy {
@Override
public void fill() {
System.out.println("Just refuel with gas!");
}
}
Vi laget tre atferdsstrategier: en for vanlige biler, en for hybrider og en for Formel 1 racerbiler. Hver strategi implementerer en annen fyllingsalgoritme. I vårt tilfelle viser vi ganske enkelt en streng på konsollen, men hver metode kan inneholde litt kompleks logikk. Hva gjør vi videre?
public class Conveyance {
FillStrategy fillStrategy;
public void fill() {
fillStrategy.fill();
}
public void go() {
System.out.println("Moving forward");
}
public void stop() {
System.out.println("Braking!");
}
}
Vi bruker vårt FillStrategy- grensesnitt som et felt i Conveyance- overordnet klasse. Merk at vi ikke angir en spesifikk implementering – vi bruker et grensesnitt. Bilklassene vil trenge spesifikke implementeringer av FillStrategy- grensesnittet:
public class F1Car extends Conveyance {
public F1Car() {
this.fillStrategy = new F1PitstopStrategy();
}
}
public class HybridCar extends Conveyance {
public HybridCar() {
this.fillStrategy = new HybridFillStrategy();
}
}
public class Sedan extends Conveyance {
public Sedan() {
this.fillStrategy = new StandardFillStrategy();
}
}
La oss se på hva vi har!
public class Main {
public static void main(String[] args) {
Conveyance sedan = new Sedan();
Conveyance hybrid = new HybridCar();
Conveyance f1car = new F1Car();
sedan.fill();
hybrid.fill();
f1car.fill();
}
}
Konsoll utgang:
Just refuel with gas!
Refuel with gas or electricity — your choice!
Refuel with gas only after all other pit stop procedures are complete!
Flott! Påfyllingsprosessen fungerer som den skal! Ingenting hindrer oss forresten i å bruke strategien som en parameter i konstruktøren! For eksempel slik:
public class Conveyance {
private FillStrategy fillStrategy;
public Conveyance(FillStrategy fillStrategy) {
this.fillStrategy = fillStrategy;
}
public void fill() {
this.fillStrategy.fill();
}
public void go() {
System.out.println("Moving forward");
}
public void stop() {
System.out.println("Braking!");
}
}
public class Sedan extends Conveyance {
public Sedan() {
super(new StandardFillStrategy());
}
}
public class HybridCar extends Conveyance {
public HybridCar() {
super(new HybridFillStrategy());
}
}
public class F1Car extends Conveyance {
public F1Car() {
super(new F1PitstopStrategy());
}
}
La oss kjøre vår main() -metode (som forblir uendret). Vi får samme resultat! Konsoll utgang:
Just refuel with gas!
Refuel with gas or electricity — your choice!
Refuel with gas only after all other pit stop procedures are complete!
Strategidesignmønsteret definerer en familie av algoritmer, innkapsler hver av dem og sikrer at de er utskiftbare. Den lar deg modifisere algoritmene uavhengig av hvordan de brukes av klienten (denne definisjonen, hentet fra boken "Head First Design Patterns", virker utmerket for meg). Vi har allerede spesifisert familien av algoritmer vi er interessert i (måter å fylle drivstoff på biler) i separate grensesnitt med forskjellige implementeringer. Vi skilte dem fra selve bilen. Hvis vi nå trenger å gjøre noen endringer i en bestemt fyllingsalgoritme, vil det ikke påvirke bilklassene våre på noen måte. Og for å oppnå utskiftbarhet trenger vi bare å legge til en enkelt setter-metode til vår transportklasse :
public class Conveyance {
FillStrategy fillStrategy;
public void fill() {
fillStrategy.fill();
}
public void go() {
System.out.println("Moving forward");
}
public void stop() {
System.out.println("Braking!");
}
public void setFillStrategy(FillStrategy fillStrategy) {
this.fillStrategy = fillStrategy;
}
}
Nå kan vi endre strategier på farten:
public class Main {
public static void main(String[] args) {
Stroller stroller= new Stroller();
stroller.setFillStrategy(new StandardFillStrategy());
stroller.fill();
}
}
Hvis barnevogner plutselig begynner å gå på bensin, vil programmet vårt være klart til å håndtere dette scenariet :) Og det er omtrent det! Du har lært enda et designmønster som utvilsomt vil være essensielt og nyttig når du jobber med ekte prosjekter :) Til neste gang!
GO TO FULL VERSION