Hoi! In de les van vandaag zullen we het hebben over Strategiepatroon. In voorgaande lessen hebben we al kort kennis gemaakt met het begrip overerving. Voor het geval je het vergeten bent, herinner ik je eraan dat deze term verwijst naar een standaardoplossing voor een algemene programmeertaak. Bij CodeGym zeggen we vaak dat je op bijna elke vraag het antwoord kunt googlen. Dit komt omdat uw taak, wat het ook is, waarschijnlijk al met succes is opgelost door iemand anders. Patronen zijn beproefde oplossingen voor de meest voorkomende taken of methoden om problematische situaties op te lossen. Dit zijn als "wielen" die je niet zelf opnieuw hoeft uit te vinden, maar je moet wel weten hoe en wanneer je ze moet gebruiken :) Een ander doel van patronen is het bevorderen van een uniforme architectuur. De code van iemand anders lezen is geen gemakkelijke taak! Iedereen schrijft andere code, omdat dezelfde taak op veel manieren kan worden opgelost. Maar het gebruik van patronen helpt verschillende programmeurs de programmeerlogica te begrijpen zonder zich in elke regel code te verdiepen (zelfs wanneer ze deze voor het eerst zien!) Vandaag kijken we naar een van de meest voorkomende ontwerppatronen genaamd "Strategie". Ontwerppatroon: Strategie - 2Stel je voor dat we een programma schrijven dat actief zal werken met Conveyance-objecten. Het maakt eigenlijk niet uit wat ons programma precies doet. We hebben een klassenhiërarchie gemaakt met één Conveyance- ouderklasse en drie onderliggende klassen: Sedan , Truck en 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 drie de onderliggende klassen erven twee standaardmethoden van de ouder: go() en stop() . Ons programma is heel eenvoudig: onze auto's kunnen alleen vooruit rijden en remmen. We zetten ons werk voort en besloten de auto's een nieuwe methode te geven: fill() (wat betekent: "vul de benzinetank"). We hebben het toegevoegd aan de bovenliggende klasse Conveyance :

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!");
   }
}
Kunnen er in zo'n eenvoudige situatie echt problemen ontstaan? Sterker nog, ze hebben al... Ontwerppatroon: Strategie - 3

public class Stroller extends Conveyance {

   public void fill() {
      
       // Hmm... This is a stroller for children. It doesn't need to be refueled :/
   }
}
Ons programma heeft nu een vervoermiddel (een kinderwagen) dat niet mooi in het algemene concept past. Het kan pedalen hebben of radiografisch worden bestuurd, maar één ding is zeker: er is geen plaats om benzine in te gieten. Onze klassenhiërarchie heeft ervoor gezorgd dat gemeenschappelijke methoden worden overgenomen door klassen die ze niet nodig hebben. Wat moeten we doen in deze situatie? Welnu, we kunnen de methode fill() in de klasse Stroller overschrijven , zodat er niets gebeurt wanneer u de wandelwagen probeert bij te tanken:

public class Stroller extends Conveyance {

   @Override
   public void fill() {
       System.out.println("A stroller cannot be refueled!");
   }
}
Maar dit kan nauwelijks een succesvolle oplossing worden genoemd, alleen al vanwege de dubbele code. De meeste klassen zullen bijvoorbeeld de methode van de bovenliggende klasse gebruiken, maar de rest zal deze moeten overschrijven. Als we 15 klassen hebben en we moeten gedrag in 5-6 van hen overschrijven, zal de codeduplicatie behoorlijk uitgebreid worden. Misschien kunnen interfaces ons helpen? Bijvoorbeeld als volgt:

public interface Fillable {
  
   public void fill();
}
We maken een Fillable- interface met één fill()- methode. Vervolgens zullen die vervoermiddelen die moeten worden bijgetankt deze interface implementeren, terwijl andere vervoermiddelen (bijvoorbeeld onze kinderwagen) dat niet zullen doen. Maar deze optie past niet bij ons. In de toekomst kan onze klassenhiërarchie erg groot worden (stel je eens voor hoeveel verschillende soorten vervoermiddelen er in de wereld zijn). We hebben de vorige versie met overerving verlaten, omdat we de fill() niet willen overschrijvenmethode vele, vele malen. Nu moeten we het in elke klas implementeren! En wat als we er 50 hebben? En als er regelmatig wijzigingen in ons programma worden aangebracht (en dit geldt bijna altijd voor echte programma's!), zouden we alle 50 klassen moeten doorlopen en het gedrag van elk van hen handmatig moeten veranderen. Dus wat moeten we uiteindelijk doen in deze situatie? Om ons probleem op te lossen, kiezen we een andere manier. We zullen namelijk het gedrag van onze klas scheiden van de klas zelf. Wat betekent dat? Zoals u weet, heeft elk object een status (een set gegevens) en gedrag (een set methoden). Het gedrag van onze overdrachtsklasse bestaat uit drie methoden: go() , stop() en fill() . De eerste twee methoden zijn prima zoals ze zijn. Maar we zullen de derde methode uit deTransport klasse. Dit scheidt het gedrag van de klasse (nauwkeuriger gezegd, het scheidt slechts een deel van het gedrag, aangezien de eerste twee methoden blijven waar ze zijn). Dus waar moeten we onze fill() methode plaatsen? Er komt niets in me op : / Het lijkt alsof het precies is waar het zou moeten zijn. We verplaatsen het naar een aparte interface: FillStrategy !

public interface FillStrategy {

   public void fill();
}
Waarom hebben we zo'n interface nodig? Het is allemaal eenvoudig. Nu kunnen we verschillende klassen maken die deze interface implementeren:

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!");
   }
}
We hebben drie gedragsstrategieën ontwikkeld: één voor gewone auto's, één voor hybrides en één voor Formule 1-raceauto's. Elke strategie implementeert een ander tankalgoritme. In ons geval geven we gewoon een string weer op de console, maar elke methode kan complexe logica bevatten. Wat doen we hierna?

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!");
   }
  
}
We gebruiken onze FillStrategy- interface als een veld in de bovenliggende klasse Conveyance . Merk op dat we geen specifieke implementatie aangeven - we gebruiken een interface. De autoklassen hebben specifieke implementaties van de FillStrategy- interface nodig:

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();
   }
}

Laten we eens kijken wat we hebben!

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();
   }
}
Console-uitvoer:

Just refuel with gas! 
Refuel with gas or electricity — your choice! 
Refuel with gas only after all other pit stop procedures are complete!
Geweldig! Het tankproces werkt zoals het hoort! Niets belet ons trouwens om de strategie als parameter in de constructor te gebruiken! Bijvoorbeeld als volgt:

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());
   }
}
Laten we onze methode main() uitvoeren (die ongewijzigd blijft). We krijgen hetzelfde resultaat! Console-uitvoer:

Just refuel with gas! 
Refuel with gas or electricity — your choice! 
Refuel with gas only after all other pit stop procedures are complete!
Het strategieontwerppatroon definieert een familie van algoritmen, kapselt ze allemaal in en zorgt ervoor dat ze uitwisselbaar zijn. Hiermee kun je de algoritmen aanpassen, ongeacht hoe ze door de klant worden gebruikt (deze definitie, ontleend aan het boek "Head First Design Patterns", lijkt me uitstekend). Ontwerppatroon: Strategie - 4We hebben de familie van algoritmen waarin we geïnteresseerd zijn (manieren om auto's bij te tanken) al gespecificeerd in afzonderlijke interfaces met verschillende implementaties. We hebben ze gescheiden van de auto zelf. Als we nu wijzigingen moeten aanbrengen in een bepaald tankalgoritme, heeft dit op geen enkele manier invloed op onze autoklassen. En om uitwisselbaarheid te bereiken, hoeven we alleen maar een enkele settermethode toe te voegen aan onze Conveyance- klasse:

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;
   }
}
Nu kunnen we strategieën on the fly veranderen:

public class Main {

   public static void main(String[] args) {

       Stroller stroller= new Stroller();
       stroller.setFillStrategy(new StandardFillStrategy());

       stroller.fill();
   }
}
Als kinderwagens plotseling op benzine gaan rijden, is ons programma klaar om dit scenario aan te kunnen :) En dat is het zo'n beetje! Je hebt nog een ontwerppatroon geleerd dat ongetwijfeld essentieel en nuttig zal zijn bij het werken aan echte projecten :) Tot de volgende keer!