Hallo! In früheren Vorlesungen sind wir bereits auf ein solches Konzept als „Designmuster“ gestoßen. Falls Sie es vergessen haben, erinnern wir Sie daran: Dieser Begriff bezeichnet eine bestimmte Standardlösung für ein häufiges Problem in der Programmierung.
Bei JavaRush sagen wir oft, dass die Antwort auf fast jede Frage gegoogelt werden kann. Daher hat wahrscheinlich schon jemand ein ähnliches Problem wie Ihres erfolgreich gelöst. Muster sind also bewährte und praxiserprobte Lösungen für die häufigsten Probleme oder Methoden zur Lösung von Problemsituationen. Dies sind genau die „Fahrräder“, die Sie auf keinen Fall selbst erfinden müssen, aber Sie müssen wissen, wie und wann Sie sie anwenden :) Eine weitere Aufgabe von Mustern besteht darin, die Architektur auf einen einzigen Standard zu bringen. Den Code eines anderen zu lesen ist keine leichte Aufgabe! Jeder schreibt es anders, denn das gleiche Problem kann auf viele Arten gelöst werden. Aber die Verwendung von Mustern ermöglicht es verschiedenen Programmierern, die Logik des Programms zu verstehen, ohne sich in jede Codezeile vertiefen zu müssen (auch wenn sie sie zum ersten Mal sehen!). Heute werden wir uns eines der häufigsten Muster namens „Strategie“ ansehen.
Stellen wir uns vor, wir schreiben ein Programm, das aktiv mit dem Car-Objekt arbeitet. In diesem Fall ist es nicht einmal besonders wichtig, was genau unser Programm macht. Zu diesem Zweck haben wir ein Vererbungssystem mit einer übergeordneten Klasse
Wir haben die für uns interessante Algorithmenfamilie (Typen von Tankwagen) in separate Schnittstellen mit mehreren Implementierungen isoliert. Wir haben sie vom eigentlichen Wesen des Autos getrennt. Wenn wir also jetzt Änderungen an diesem oder jenem Tankvorgang vornehmen müssen, hat dies keinerlei Auswirkungen auf unsere Fahrzeugklassen. Um die Austauschbarkeit zu erreichen, müssen wir unserer Klasse lediglich eine Setter-Methode hinzufügen
![Designmuster „Strategie“ – 2](https://cdn.javarush.com/images/article/d36666bd-a280-4878-8d6f-ab5cff873cdd/800.jpeg)
Auto
und drei untergeordneten Klassen erstellt: Sedan
, Truck
und F1Car
.
public class Auto {
public void gas() {
System.out.println("Едем вперед");
}
public void stop() {
System.out.println("Тормозим!");
}
}
public class Sedan extends Auto {
}
public class Truck extends Auto {
}
public class F1Car extends Auto {
}
Alle drei untergeordneten Klassen erben zwei Standardmethoden von der übergeordneten Klasse – gas()
und stop()
unser Programm ist völlig einfach: Autos können nur vorwärts fahren und bremsen. Wir setzten unsere Arbeit fort und beschlossen, den Autos eine neue Methode hinzuzufügen – fill()
(Tanken). Fügen wir es der übergeordneten Klasse hinzu Auto
:
public class Auto {
public void gas() {
System.out.println("Едем вперед");
}
public void stop() {
System.out.println("Тормозим!");
}
public void fill() {
System.out.println("Заправить бензин!");
}
}
Es scheint, dass in einer so einfachen Situation Probleme auftreten könnten? Nun, tatsächlich sind sie bereits entstanden ... ![Designmuster „Strategie“ – 3](https://cdn.javarush.com/images/article/a4a7d554-f812-47f0-8652-6d958446528d/800.jpeg)
public class ChildrenBuggies extends Auto {
public void fill() {
//хм... Это детский багги, его не надо заправлять :/
}
}
In unserem Programm ist ein Auto aufgetaucht, das nicht in das Gesamtkonzept passt – ein Kinderbuggy. Es mag mit Pedalantrieb oder Funksteuerung ausgestattet sein, aber eines ist sicher: Es gibt keinen Ort, an dem man Benzin nachfüllen kann. Unser Vererbungsschema hat dazu geführt, dass wir gängige Methoden sogar an Klassen weitergegeben haben, die sie nicht benötigen. Was sollen wir in einer solchen Situation tun? Nun, Sie können zum Beispiel die Methode fill()
in der Klasse überschreiben, ChildrenBuggies
sodass beim Versuch, den Buggy aufzutanken, nichts passiert:
public class ChildrenBuggies extends Auto {
@Override
public void fill() {
System.out.println("Игрушечную машину нельзя заправить!");
}
}
Diese Lösung kann jedoch kaum als erfolgreich bezeichnet werden, zumindest aufgrund der Codeduplizierung. Beispielsweise verwenden die meisten Klassen eine Methode der übergeordneten Klasse, andere Klassen sind jedoch gezwungen, diese zu überschreiben. Wenn wir 15 Klassen haben und in 5–6 Klassen gezwungen sind, das Verhalten zu überschreiben, wird die Codeduplizierung ziemlich umfangreich. Vielleicht können uns Schnittstellen helfen? Zum Beispiel dieses hier:
public interface Fillable {
public void fill();
}
Wir werden eine Schnittstelle Fillable
mit einer Methode erstellen fill()
. Dementsprechend werden die Autos, die betankt werden müssen, diese Schnittstelle implementieren, andere Autos (z. B. unser Buggy) jedoch nicht. Aber auch diese Option wird uns nicht passen. Unsere Klassenhierarchie könnte in Zukunft sehr groß werden (stellen Sie sich vor, wie viele verschiedene Arten von Autos es auf der Welt gibt). Wir haben die vorherige Vererbungsoption aufgegeben, weil wir die nicht überschreiben wollten fill()
. Hier müssen wir es in jeder Klasse implementieren! Was wäre, wenn wir 50 davon hätten? Und wenn an unserem Programm häufig Änderungen vorgenommen werden (und das wird in echten Programmen fast immer der Fall sein!), müssen wir mit heraushängender Zunge zwischen allen 50 Klassen herumlaufen und das Verhalten jeder einzelnen von ihnen manuell ändern. Was sollen wir also am Ende tun? Um unser Problem zu lösen, wählen wir einen anderen Weg. Trennen wir nämlich das Verhalten unserer Klasse von der Klasse selbst. Was bedeutet das? Wie Sie wissen, hat jedes Objekt einen Zustand (einen Datensatz) und ein Verhalten (einen Methodensatz). Das Verhalten unserer Maschinenklasse besteht aus drei Methoden – gas()
, stop()
und fill()
. Die ersten beiden Methoden sind in Ordnung. Aber wir werden die dritte Methode außerhalb der Klasse verschieben Auto
. Dies wird die Trennung des Verhaltens von der Klasse sein (genauer gesagt, wir trennen nur einen Teil des Verhaltens – die ersten beiden Methoden bleiben bestehen). Wohin sollen wir unsere Methode bewegen fill()
? Da fällt mir sofort nichts ein :/ Er schien völlig an seinem Platz zu sein. Wir werden es auf eine separate Schnittstelle verschieben - FillStrategy
!
public interface FillStrategy {
public void fill();
}
Warum brauchen wir diese Schnittstelle? Es ist einfach. Jetzt können wir mehrere Klassen erstellen, die diese Schnittstelle implementieren:
public class HybridFillStrategy implements FillStrategy {
@Override
public void fill() {
System.out.println("Заправляем бензином oder электричеством на выбор!");
}
}
public class F1PitstopStrategy implements FillStrategy {
@Override
public void fill() {
System.out.println("Заправляем бензин только после всех остальных Verfahren пит-стопа!");
}
}
public class StandartFillStrategy implements FillStrategy {
@Override
public void fill() {
System.out.println("Просто заправляем бензин!");
}
}
Wir haben drei Verhaltensstrategien entwickelt – für konventionelle Autos, für Hybride und für Formel-1-Autos. Jede Strategie implementiert einen separaten Betankungsalgorithmus. In unserem Fall wird dies nur an die Konsole ausgegeben, die Methode enthält jedoch möglicherweise eine komplexe Logik. Was sollen wir als nächstes damit machen?
public class Auto {
FillStrategy fillStrategy;
public void fill() {
fillStrategy.fill();
}
public void gas() {
System.out.println("Едем вперед");
}
public void stop() {
System.out.println("Тормозим!");
}
}
Wir verwenden unsere Schnittstelle FillStrategy
als Feld in der übergeordneten Klasse Auto
. Bitte beachten Sie: Wir geben keine konkrete Implementierung vor, sondern nutzen die Schnittstelle. Und wir benötigen spezifische Implementierungen der Schnittstelle FillStrategy
in den Kinderautoklassen:
public class F1Car extends Auto {
public F1Car() {
this.fillStrategy = new F1PitstopStrategy();
}
}
public class HybridAuto extends Auto {
public HybridAuto() {
this.fillStrategy = new HybridFillStrategy();
}
}
public class Sedan extends Auto {
public Sedan() {
this.fillStrategy = new StandartFillStrategy();
}
}
Mal sehen, was wir haben:
public class Main {
public static void main(String[] args) {
Auto sedan = new Sedan();
Auto hybrid = new HybridAuto();
Auto f1car = new F1Car();
sedan.fill();
hybrid.fill();
f1car.fill();
}
}
Konsolenausgabe:
Просто заправляем бензин!
Заправляем бензином oder электричеством на выбор!
Заправляем бензин только после всех остальных Verfahren пит-стопа!
Super, der Tankvorgang funktioniert wie er soll! Übrigens hindert uns nichts daran, die Strategie als Parameter im Konstruktor zu verwenden! Zum Beispiel so:
public class Auto {
private FillStrategy fillStrategy;
public Auto(FillStrategy fillStrategy) {
this.fillStrategy = fillStrategy;
}
public void fill() {
this.fillStrategy.fill();
}
public void gas() {
System.out.println("Едем вперед");
}
public void stop() {
System.out.println("Тормозим!");
}
}
public class Sedan extends Auto {
public Sedan() {
super(new StandartFillStrategy());
}
}
public class HybridAuto extends Auto {
public HybridAuto() {
super(new HybridFillStrategy());
}
}
public class F1Car extends Auto {
public F1Car() {
super(new F1PitstopStrategy());
}
}
Lassen Sie uns unsere Methode ausführen main()
(sie bleibt unverändert) und das gleiche Ergebnis erhalten! Konsolenausgabe:
Просто заправляем бензин!
Заправляем бензином oder электричеством на выбор!
Заправляем бензин только после всех остальных Verfahren пит-стопа!
Das Strategiemuster definiert eine Familie von Algorithmen, kapselt jeden von ihnen und stellt sicher, dass sie austauschbar sind. Damit ist es möglich, Algorithmen unabhängig von ihrer Verwendung auf der Client-Seite zu modifizieren (diese Definition stammt aus dem Buch „Exploring Design Patterns“ und scheint mir äußerst gelungen). ![Designmuster „Strategie“ – 4](https://cdn.javarush.com/images/article/6d9ab390-1860-4470-ae46-384250d4d119/512.jpeg)
Auto
:
public class Auto {
FillStrategy fillStrategy;
public void fill() {
fillStrategy.fill();
}
public void gas() {
System.out.println("Едем вперед");
}
public void stop() {
System.out.println("Тормозим!");
}
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) {
ChildrenBuggies buggies = new ChildrenBuggies();
buggies.setFillStrategy(new StandartFillStrategy());
buggies.fill();
}
}
Wenn plötzlich Kinder-Buggys mit Benzin gefüllt werden, ist unser Programm auf ein solches Szenario vorbereitet :) Das ist eigentlich alles! Sie haben ein weiteres Designmuster gelernt, das Sie zweifellos brauchen werden und das Ihnen bei der Arbeit an echten Projekten mehr als einmal helfen wird :) Wir sehen uns wieder!
GO TO FULL VERSION