CodeGym /Java-Blog /Random-DE /Strategieentwurfsmuster
Autor
John Selawsky
Senior Java Developer and Tutor at LearningTree

Strategieentwurfsmuster

Veröffentlicht in der Gruppe Random-DE
Hallo! In der heutigen Lektion werden wir über Strategiemuster sprechen. In den vorherigen Lektionen haben wir uns bereits kurz mit dem Konzept der Vererbung vertraut gemacht. Falls Sie es vergessen haben, möchte ich Sie daran erinnern, dass sich dieser Begriff auf eine Standardlösung für eine häufige Programmieraufgabe bezieht. Bei CodeGym sagen wir oft, dass Sie die Antwort auf fast jede Frage googeln können. Das liegt daran, dass Ihre Aufgabe, was auch immer sie sein mag, wahrscheinlich bereits von jemand anderem erfolgreich gelöst wurde. Muster sind bewährte Lösungen für die häufigsten Aufgaben oder Methoden zur Lösung problematischer Situationen. Dies sind wie „Räder“, die Sie nicht selbst neu erfinden müssen, aber Sie müssen wissen, wie und wann Sie sie verwenden :) Ein weiterer Zweck von Mustern besteht darin, eine einheitliche Architektur zu fördern. Den Code eines anderen zu lesen ist keine leichte Aufgabe! Jeder schreibt anderen Code, denn die gleiche Aufgabe kann auf viele Arten gelöst werden. Aber die Verwendung von Mustern hilft verschiedenen Programmierern, die Programmierlogik zu verstehen, ohne sich in jede Codezeile vertiefen zu müssen (selbst wenn sie sie zum ersten Mal sieht!). Heute betrachten wir eines der gebräuchlichsten Entwurfsmuster namens „Strategie“. Entwurfsmuster: Strategie – 2Stellen Sie sich vor, wir schreiben ein Programm, das aktiv mit Transportobjekten arbeitet. Es spielt keine Rolle, was genau unser Programm macht. Wir haben eine Klassenhierarchie mit einer übergeordneten Klasse „Conveyance“ und drei untergeordneten Klassen erstellt: Sedan , Truck und 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 drei untergeordneten Klassen erben zwei Standardmethoden von der übergeordneten Klasse: go() und stop() . Unser Programm ist ganz einfach: Unsere Autos können sich nur vorwärts bewegen und bremsen. Als Fortsetzung unserer Arbeit beschlossen wir, den Autos eine neue Methode zu geben: fill() (was „den Benzintank füllen“ bedeutet). Wir haben es zur übergeordneten Klasse „Conveyance“ hinzugefügt :

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!");
   }
}
Kann es in einer so einfachen Situation wirklich zu Problemen kommen? Tatsächlich haben sie bereits... Entwurfsmuster: Strategie – 3

public class Stroller extends Conveyance {

   public void fill() {
      
       // Hmm... This is a stroller for children. It doesn't need to be refueled :/
   }
}
In unserem Programm gibt es jetzt ein Fortbewegungsmittel (einen Kinderwagen), das nicht so gut in das Gesamtkonzept passt. Es könnte Pedale haben oder ferngesteuert sein, aber eines ist sicher: Es wird keinen Platz zum Einfüllen von Benzin geben. Unsere Klassenhierarchie hat dazu geführt, dass gemeinsame Methoden von Klassen geerbt werden, die sie nicht benötigen. Was sollen wir in dieser Situation tun? Nun, wir könnten die Methode fill() in der Klasse „Buggy“ überschreiben, sodass nichts passiert, wenn Sie versuchen, den Kinderwagen aufzutanken:

public class Stroller extends Conveyance {

   @Override
   public void fill() {
       System.out.println("A stroller cannot be refueled!");
   }
}
Dies kann jedoch kaum als erfolgreiche Lösung bezeichnet werden, wenn der einzige Grund darin besteht, dass Code doppelt vorhanden ist. Beispielsweise verwenden die meisten Klassen die Methode der übergeordneten Klasse, der Rest ist jedoch gezwungen, diese zu überschreiben. Wenn wir 15 Klassen haben und das Verhalten in 5–6 davon überschreiben müssen, wird die Codeduplizierung ziemlich umfangreich. Vielleicht können uns Schnittstellen helfen? Zum Beispiel so:

public interface Fillable {
  
   public void fill();
}
Wir erstellen eine Fillable- Schnittstelle mit einer fill()- Methode. Dann werden die Fahrzeuge, die betankt werden müssen, diese Schnittstelle implementieren, während andere Fahrzeuge (z. B. unser Kinderwagen) dies nicht tun. Aber diese Option passt nicht zu uns. In Zukunft könnte unsere Klassenhierarchie sehr groß werden (stellen Sie sich vor, wie viele verschiedene Arten von Fortbewegungsmitteln es auf der Welt gibt). Wir haben die vorherige Version mit Vererbung aufgegeben, weil wir fill() nicht überschreiben wollenMethode viele, viele Male. Jetzt müssen wir es in jeder Klasse implementieren! Und was wäre, wenn wir 50 hätten? Und wenn in unserem Programm häufig Änderungen vorgenommen werden (und das gilt fast immer für echte Programme!), müssten wir alle 50 Klassen durchgehen und das Verhalten jeder einzelnen Klasse manuell ändern. Was sollten wir also letztendlich in dieser Situation tun? Um unser Problem zu lösen, wählen wir einen anderen Weg. Wir trennen nämlich das Verhalten unserer Klasse von der Klasse selbst. Was bedeutet das? Wie Sie wissen, hat jedes Objekt einen Zustand (eine Reihe von Daten) und ein Verhalten (eine Reihe von Methoden). Das Verhalten unserer Transportklasse besteht aus drei Methoden: go() , stop() und fill() . Die ersten beiden Methoden sind so wie sie sind in Ordnung. Aber wir werden die dritte Methode aus dem entfernenTransportklasse . Dadurch wird das Verhalten von der Klasse getrennt (genauer gesagt wird nur ein Teil des Verhaltens getrennt, da die ersten beiden Methoden dort bleiben, wo sie sind). Wo sollen wir also unsere fill()- Methode platzieren? Da fällt mir nichts ein :/ Es scheint, als wäre es genau dort, wo es sein sollte. Wir werden es auf eine separate Schnittstelle verschieben: FillStrategy !

public interface FillStrategy {

   public void fill();
}
Warum brauchen wir eine solche Schnittstelle? Es ist alles unkompliziert. Jetzt können wir mehrere Klassen erstellen, die diese Schnittstelle implementieren:

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!");
   }
}
Wir haben drei Verhaltensstrategien entwickelt: eine für normale Autos, eine für Hybridfahrzeuge und eine für Formel-1-Rennwagen. Jede Strategie implementiert einen anderen Betankungsalgorithmus. In unserem Fall zeigen wir einfach eine Zeichenfolge auf der Konsole an, aber jede Methode könnte eine komplexe Logik enthalten. Was machen wir als Nächstes?

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!");
   }
  
}
Wir verwenden unsere FillStrategy- Schnittstelle als Feld in der übergeordneten Klasse Conveyance . Beachten Sie, dass wir keine spezifische Implementierung angeben, sondern eine Schnittstelle verwenden. Die Autoklassen benötigen spezifische Implementierungen der FillStrategy- Schnittstelle:

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

Schauen wir uns an, was wir haben!

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

Just refuel with gas! 
Refuel with gas or electricity — your choice! 
Refuel with gas only after all other pit stop procedures are complete!
Großartig! Der Tankvorgang funktioniert wie er soll! Übrigens hindert uns nichts daran, die Strategie als Parameter im Konstruktor zu verwenden! Zum Beispiel so:

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());
   }
}
Lassen Sie uns unsere main() -Methode ausführen (die unverändert bleibt). Wir kommen zum gleichen Ergebnis! Konsolenausgabe:

Just refuel with gas! 
Refuel with gas or electricity — your choice! 
Refuel with gas only after all other pit stop procedures are complete!
Das Strategieentwurfsmuster definiert eine Familie von Algorithmen, kapselt jeden von ihnen und stellt sicher, dass sie austauschbar sind. Damit können Sie die Algorithmen unabhängig davon ändern, wie sie vom Client verwendet werden (diese Definition aus dem Buch „Head First Design Patterns“ scheint mir ausgezeichnet zu sein). Entwurfsmuster: Strategie – 4Wir haben die Algorithmenfamilie, an der wir interessiert sind (Möglichkeiten zum Betanken von Autos), bereits in separaten Schnittstellen mit unterschiedlichen Implementierungen spezifiziert. Wir haben sie vom Auto selbst getrennt. Wenn wir nun Änderungen an einem bestimmten Betankungsalgorithmus vornehmen müssen, hat dies keinerlei Auswirkungen auf unsere Fahrzeugklassen. Und um Austauschbarkeit zu erreichen, müssen wir unserer Conveyance- Klasse lediglich eine einzelne Setter-Methode hinzufügen :

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;
   }
}
Jetzt können wir Strategien im Handumdrehen ändern:

public class Main {

   public static void main(String[] args) {

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

       stroller.fill();
   }
}
Wenn Kinderwagen plötzlich mit Benzin laufen, ist unser Programm auf dieses Szenario vorbereitet :) Und das war's auch schon! Sie haben ein weiteres Designmuster gelernt, das bei der Arbeit an echten Projekten zweifellos wichtig und hilfreich sein wird :) Bis zum nächsten Mal!
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION