Gegeben sei folgendes Szenario: Es sollen verschiedene Hunde modelliert werden. Jeder Hund soll bellen und laufen können.
Da liegt es nahe, Vererbung einzusetzen: Eine abstrakte Superklasse Hund von der alle konkreten Subklassen (Husky, Bulldogge, Pudel) ableiten und damit das Bell- und Laufverhalten erben (oder gezwungen sind, bellen() und laufen() zu implementieren, wenn diese Methoden abstrakt deklariert sind. So oder so: Man erreicht Polymorphie.).

Soweit so gut, jedoch ändern sich nun die Anforderungen (wie immer...): Ein Husky soll nun anders laufen als eine Bulldogge oder ein Pudel. Und ein Pudel bellt definitiv anders als die anderen beiden Hundearten. Dazu müssen die Subklassen bellen() und laufen() überschreiben. Weiterhin soll auch eine weitere Klasse modelliert werden: Hundeattrappen. Wie würde dann das UML-Diagramm aussehen?

Scheint auf den ersten Blick in Ordnung, doch schaut man genauer hin, zeigen sich eine Reihe von Nachteilen:
Der Entwurf ist somit eher suboptimal, denn wenn man sich auf eins in der Softwareentwicklung verlassen kann, dann ist es: Veränderung. Es gilt Entwürfe so zu gestalten, dass Änderungen minimale Auswirkungen auf den bestehenden Code haben (Änderungsstabilität). Dazu sei ein essenzielles OO-Entwurfsprinzip rezitiert:
Identifiziere jene Aspekte, die sich ändern und trenne sie von jenen, die konstant bleiben.
Was ändert sich bei unserer Hunde-Modellierung? Das Verhalten bellen() und laufen(). Was bleibt konstant? Der Rest der Hundeklassen. Also liegt es nahe, den Verhaltenscode aus den Hundeklassen herauszuziehen und in separaten Klassen zu kapseln.

Danach ist es nur noch nötig, dass jeder Hund sein Verhaltensobjekt mit einer Instanzvariable kennt. Soll der Husky dann bellen, so lässt er sein Bell-Verhaltensobjekt für ihn bellen.
Delegation von Verhalten an das Verhaltensobjekt:
public void bellen(){
bellVerhalten.bellen();
}
Doch welchen Typ hat die Instanzvariable, die das Objekt mit dem Bellverhalten referenziert? Das konkrete LautBellen? Nein, in diesem Fall würden wir uns auf ein Verhalten festlegen und hätten keine Chance das Verhalten zu ändern, ohne den Husky-Code zu brechen, da der Typ festgecodiert ist. Wir müssen auf eine Schnittstelle programmieren, nicht auf eine Implementierung. Wir definieren also ein Interface für unser Verhalten und lassen die konkreten Verhaltensklassen dieses Interface implementieren.

Die Instanzvariablen im Hund auf das Verhalten sind somit vom Interfacetyp "IBellVerhalten" (beziehungsweise "ILaufVerhalten").
Instanzvariablentyp IBellVerhalten:
private IBellVerhalten bellVerhalten;
Dank Polymorphie erlangen wir somit Flexibilität, denn der Husky weiß nun nicht mehr, welches Verhalten er konkret besitzt. Er weiß nur, dass er damit bellen() bzw. laufen() kann. Und wir können nun durch entsprechende Setter oder Konstruktoren das Verhalten der Hunde zur Design- und (!) Laufzeit dynamisch setzen, ohne ihren Code anzufassen.
Fertiger Husky-Code mit gekapselten Bellverhalten:
public class Husky extends Hund {
//Defaultverhalten: LeiseBellen
private IBellVerhalten bellVerhalten = new LeiseBellen();
public void bellen(){
bellVerhalten.bellen();
}
public void setBellVerhalten(IBellVerhalten pBellVerhalten){
bellVerhalten = pBellVerhalten;
}
}
Der Husky kann nun mit variierenden Verhalten genutzt werden, da das Verhalten entkoppelt wurde:
public static void main(String[] args) {
Husky husky = new Husky(); //Defaultverhalten
husky.bellen(); //ganz leise bellen...
husky.setBellVerhalten(new LautBellen()); //Verhalten dynamisch setzen
husky.bellen(); //GANZ LAUT BELLEN!!!
}
In der Zusammenschau sieht unsere flexible Hundemodellierung wie folgt aus:
Laufverhalten (Interface und Implementationen):
interface ILaufVerhalten {
public void laufen();
}
class NormalLaufen implements ILaufVerhalten{
public void laufen() {
System.out.println("Normal laufen.");
}
}
class SchnellLaufen implements ILaufVerhalten {
public void laufen() {
System.out.println("Schnell laufen.");
}
}
class KannNichtLaufen implements ILaufVerhalten{
public void laufen() {
System.out.println("Kann doch gar nicht laufen.");
}
}
class Humpeln implements ILaufVerhalten{
public void laufen() {
System.out.println("Humpeln.");
}
}
Bellverhalten (Interface und Implementationen):
interface IBellVerhalten {
public void bellen();
}
class LeiseBellen implements IBellVerhalten {
public void bellen() {
System.out.println("ganz leise bellen...");
}
}
class LautBellen implements IBellVerhalten{
public void bellen() {
System.out.println("GANZ LAUT BELLEN!!");
}
}
class ElektronischBellen implements IBellVerhalten {
public void bellen() {
System.out.println("Elekkkkktronisch Bellen!");
}
}
Abstrakte Hundklasse und die konkreten Hundeklassen
abstract class Hund {
//Instanzvariablen vom Typ des Interfaces. Defaultverhalten
IBellVerhalten bellVerhalten = new LautBellen();
ILaufVerhalten laufVerhalten = new SchnellLaufen();
public void setBellVerhalten(IBellVerhalten pBellVerhalten) {
bellVerhalten = pBellVerhalten;
}
public void setLaufVerhalten(ILaufVerhalten pLaufVerhalten) {
laufVerhalten = pLaufVerhalten;
}
public void bellen(){
//Delegation des Verhaltens an Verhaltensobjekt
bellVerhalten.bellen();
}
public void laufen(){
//Delegation des Verhaltens an Verhaltensobjekt
laufVerhalten.laufen();
}
}
Beispielclient:
public class Client {
public static void main(String[] args) {
Husky husky = new Husky();
husky.bellen(); //ganz leises bellen...
husky.laufen(); //Schnelles laufen
husky.setLaufVerhalten(new Humpeln());
husky.laufen(); //Humpeln
//...
}
}
Nun lehnen wir uns zurück und analysieren unseren neuen Entwurf:
Es zeigt sich, dass durch den Einsatz des Strategy Patterns, ein hohes Maß an Flexibilität und Dynamik gewonnen wird, während zeitgleich die Wartung erleichtert wird und Erweiterungen schnell und unkompliziert möglich sind.
Nach dieser Einführung wird im folgenden Abschnitt das Strategy Design Pattern formalisiert, näher analysiert und diskutiert.
| Klasse | Strategy Teilnehmer |
|---|---|
| Hund | Context |
| IBellVerhalten | Strategy-Interface |
| LautBellen, LeiseBellen | Konkrete Strategy |
| bellen()-Methoden | Algorithmen |
Strategy:
"Definiere eine Familie von Algorithmen, kapsele jeden einzelnen und mach sie austauschbar. Das Strategiemuster ermöglicht es, den Algorithmus unabhängig von ihn nutzenden Klienten zu variieren."
([GoF], Seite 373)
Das Verhalten (Funktionalität, Algorithmus) eines Objekts (dem Context) in eine eigene Strategieklasse (Strategie = gekapselter Algorithmus) ausgelagert. Der Context hält eine Referenz auf sein Strategieobjekt und wenn er das ausgelagerte Verhalten ausführen soll, so delegiert er den Aufruf an sein referenziertes Strategieobjekt. Der Context arbeitet dabei nicht mit einer konkreten Implementierung, sondern mit einer Schnittstelle. Er ist damit implementierungsunabhängig und kann somit mit neuen Verhalten ausgestattet werden, ohne dass sein Code dafür geändert werden muss. Einzige Bedingung ist, dass die neue Strategie das Strategyinterface korrekt implementiert.
Weiterhin kann durch die Auslagerung des Verhaltens auch andere (verwandte) Contextklassen die Strategien wiederverwenden und müssen sie nicht selbst implementieren.
Der Client kann folglich das Verhalten eines Contextobjekts sowohl zur Designzeit als auch zur Laufzeit dynamisch ändern.
Context:
public class Context {
// Instanzvariable für die Strategy (Komposition)
// vom Typ des Interfaces -> Implementierungunabhängigkeit
// Defaultverhalten: ConcreteStrategyA
private IStrategy _strategy = new ConcreteStrategyA();
public void execute() {
//delegiert Verhalten an Strategy-Objekt
_strategy.executeAlgorithm();
}
public void setStrategy(IStrategy strategy) {
_strategy = strategy;
}
public IStrategy getStrategy() {
return _strategy;
}
}
IStrategy, ConreteStrategyA, ConreteStrategyB:
interface IStrategy {
public void executeAlgorithm();
}
class ConcreteStrategyA implements IStrategy {
public void executeAlgorithm() {
System.out.println("Concrete Strategy A");
}
}
class ConcreteStrategyB implements IStrategy {
public void executeAlgorithm() {
System.out.println("Concrete Strategy B");
}
}
Beispielclient:
public class Client {
public static void main(String[] args) {
//Default Verhalten
Context context = new Context();
context.execute();
//Verhalten ändern
context.setStrategy(new ConcreteStrategyB());
context.execute();
}
}
Weitere:
In der Java API wird das Strategy Design Pattern an zahlreichen Stellen angewandt.
JComponent enthält sehr viele grundlegende Methoden um GUI-Componenten zu zeichnen. Darunter auch welche zum Zeichnen von Bordern (Rahmen). Wie der Border gezeichnet wird, entscheiden die 3 Methoden des Border-Interfaces. In seinen Methoden delegiert JComponent borderspezifisches Verhalten an sein Borderobjekt. Welches das konkret ist (LineBorder, TitledBorder etc.), ist für ihn nicht interessant. Ein Blick in den Quellcode von JComponent lohnt sich:
Ausschnitt aus dem Sourcecode von JComponent:
import java.awt.Container;
import java.awt.Graphics;
import java.awt.Insets;
import java.io.Serializable;
import javax.swing.RepaintManager;
import javax.swing.SwingUtilities;
import javax.swing.TransferHandler;
import javax.swing.border.AbstractBorder;
import javax.swing.border.Border;
public class JComponent extends Container implements Serializable, TransferHandler.HasGetTransferHandler {
private Border border;
//viel Code...
public void setBorder(Border border) {
Border oldBorder = this.border;
this.border = border;
firePropertyChange("border", oldBorder, border);
if (border != oldBorder) {
if (border == null || oldBorder == null || !(border.getBorderInsets(this).equals(oldBorder.getBorderInsets(this)))) {
revalidate();
}
repaint();
}
}
public Border getBorder() {
return border;
}
public Insets getInsets() {
if (border != null) {
return border.getBorderInsets(this);
}
return super.getInsets();
}
protected void paintBorder(Graphics g) {
Border border = getBorder();
if (border != null) {
border.paintBorder(this, g, 0, 0, getWidth(), getHeight());
}
}
//viel Code...
}
Wie bereits oben angesprochen, wird hier ein weiteres Problem des Strategy Pattern gelöst: Die enge Kopplung zwischen Client und den Borderimplementierungen. Es wird eine Factory vom Client genutzt, die den Erstellungcode mit den konkreten Bordern kapselt. Somit kennt der Client keine Implementierungsdetails (konkrete Borderklassen) mehr.
Das Strategy Pattern wird ebenso bei dem Layout Managern von AWT, Swing und SWT verwendet. Hier am Beispiel von AWT/Swing:
Die Klasse Container, der Context, übernimmt das Zeichnen seiner Elemente, dazu nutzt er einen LayoutManager, an den er alle Layoutingfragen delegiert. Je nach Implementierung werden so die Elemente angeordnet. Durch das Strategy Pattern erlangt das Design ein hohes Maß an Flexibilität, da nun auch eigene LayoutManager geschrieben oder Core-API-externe Manager (z.B. FormsLayout, MiGLayout) genutzt werden können. Der Client legt schließtlich fest, welcher Container, welches Layout erhält:
Der Client verbindet Container mit dem LayoutManager:
JFrame frame = new JFrame();
frame.setLayout(new GridLayout(4, 2));
// ...
public Insets getInsets() {
if (border != null) {
return border.getBorderInsets(this);
}
return super.getInsets();
}
protected void paintBorder(Graphics g) {
Border border = getBorder();
if (border != null) {
border.paintBorder(this, g, 0, 0, getWidth(), getHeight());
}
}
//viel Code...
}
Auch das Look & Feel von Swing nutzt das Strategy Pattern, um die jeweiligen Look&Feels (Java-Default, Window, Mac OS X etc.) hinter Interfaces zu kapseln und damit unabhängig von der jeweiligen Implementierung bzw. des aktuellen Betriebssystems zu werden.
Kommentare
So eine gute und ausführliche Erklärung findet man selten.
Vielen Dank
vielen Dank für deine Hinweise.
Habe aber noch ein paar kleine Fehler im Code entdeckt:
Jeweils in der Klasse Husky,
bei der Methode bellen muss es heißen: bellVerhalten.bellen();
und nicht bellenVerhalten....
und
statt
private IBellVerhalten bellVerhalten = new LeiseBellen();
muss es
private IBellVerhalten bellVerhalten = new LautBellen();
heißen, damit der Kommentar beim Client passt, damit auch GANZ LAUT BELLEN ausgegben wird.
Viele Grüße aus Franken
Danke Dir!
Seite: 1 - nächste Seite