Das Strategy Design Pattern

Studienprojekt von Philipp Hauer. 2009 - 2010. ©

Inhalt

Einführung

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.).

Einführung ins Strategie Entwurfsmuster

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?

Einführung in das Strategy Pattern

Scheint auf den ersten Blick in Ordnung, doch schaut man genauer hin, zeigen sich eine Reihe von Nachteilen:

  • bellen() und laufen() werden immer vererbt (wenn in Hund bereits implementiert) bzw. müssen in der Subklasse implementiert werden (wenn abstrakt deklariert) - auch bei Subklassen, die beispielweise gar nicht laufen können - wie die HundAttrappe.
  • Code Redundanz. Angenommen Bulldogge und Pudel haben gleiches Laufverhalten, so muss nichtsdestotrotz der laufen()-Code doppelt geschrieben werden - einmal für Pudel und einmal für Bulldogge. Dazu kommt, das Änderungen an diesem Verhalten, Codeänderungen in mehreren Klassen bedeutet. Ein Wartungs- und Erweiterungsalptraum mit hoher Fehleranfälligkeit.
  • Ohne Weiteres ist es nicht möglich, das Verhalten der Hunde zur Laufzeit zu ändern.
  • Es können keine allgemeinen Aussagen über das Verhalten von Hunden getroffen werden, da jeder konkrete Hund sie für sich selbst codiert.
  • Wiederverwendbarkeit. Bestehendes Verhalten kann nicht wiederverwendet werden: Erstellt man beispielsweise einen Schäferhund, so muss man wieder bellen() und laufen() neu schreiben - egal ob es sich dabei um neues oder bereits (in einem anderen Hund) implementiertes Verhalten handelt.

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.

Einführung ins Strategie Entwurfsmuster

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.

Einführung ins Strategie Entwurfsmuster

Die Instanzvariablen im Hund auf das Verhalten sind somit vom Interfacetyp "BellVerhalten" (beziehungsweise "LaufVerhalten").

Instanzvariablentyp BellVerhalten:

private BellVerhalten 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 BellVerhalten bellVerhalten = new LeiseBellen(); 

    public void bellen(){ 
        bellVerhalten.bellen(); 
    } 

    public void setBellVerhalten(BellVerhalten bellVerhalten){ 
        this.bellVerhalten = bellVerhalten; 
    } 
}  
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:

Einführung ins Strategie Entwurfsmuster

Laufverhalten (Interface und Implementationen):

interface LaufVerhalten { 
    public void laufen(); 
} 

class NormalLaufen implements LaufVerhalten{ 
    public void laufen() { 
        System.out.println("Normal laufen."); 
    } 
} 

class SchnellLaufen implements LaufVerhalten { 
    public void laufen() { 
        System.out.println("Schnell laufen."); 
    } 
} 

class KannNichtLaufen implements LaufVerhalten{ 
    public void laufen() { 
        System.out.println("Kann doch gar nicht laufen."); 
    } 
} 

class Humpeln implements LaufVerhalten{ 
    public void laufen() { 
        System.out.println("Humpeln."); 
    } 
} 
Bellverhalten (Interface und Implementationen):

interface BellVerhalten { 
    public void bellen(); 
} 

class LeiseBellen implements BellVerhalten { 
    public void bellen() { 
        System.out.println("ganz leise bellen..."); 
    } 
} 

class LautBellen implements BellVerhalten{ 
    public void bellen() { 
        System.out.println("GANZ LAUT BELLEN!!"); 
    } 
}

class ElektronischBellen implements BellVerhalten { 
    public void bellen() { 
        System.out.println("Elekkkkktronisch Bellen!"); 
    } 
}  
Abstrakte Hundklasse und die konkrete Hundeklasse Husky

public abstract class Hund { 

    //Instanzvariablen vom Typ des Interfaces. Defaultverhalten 
    BellVerhalten bellVerhalten = new LautBellen(); 
    LaufVerhalten laufVerhalten = new SchnellLaufen(); 

    public void setBellVerhalten(BellVerhalten bellVerhalten) { 
        this.bellVerhalten = bellVerhalten; 
    } 

    public void setLaufVerhalten(LaufVerhalten laufVerhalten) { 
        this.laufVerhalten = laufVerhalten; 
    } 

    public void bellen(){ 
        //Delegation des Verhaltens an Verhaltensobjekt 
        bellVerhalten.bellen(); 
    } 

    public void laufen(){ 
        //Delegation des Verhaltens an Verhaltensobjekt 
        laufVerhalten.laufen(); 
    } 
}

public class Husky extends Hund { 
	public Husky(){
		setBellVerhalten(new LeiseBellen());
		setLaufVerhalten(new SchnellLaufen());
	}
}  
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:

  • Alternative zur Unterklassenbildung. Neue Hundearten können kreiert werden, ohne die abstrakte Hundklasse ableiten zu müssen. Sie entstehen stattdessen durch die Erstellung neuer Verhaltensklassen oder durch die neue Kombination bestehender Verhaltensobjekte.
  • Hund ist von seinem Verhalten entkoppelt und kennt nur noch die Schnittstelle seines Verhaltens. Dadurch können wir das Verhalten eines Hundes beliebig ändern, ohne seinen Code ändern zu müssen. Das Hinzufügen von neuem Verhalten ist somit mit minimalen Änderungen verbunden.
  • Keine Code Redundanz. Es existiert für jedes Verhalten eine Klasse. Jeder Hund, der dieses Verhalten nutzen möchte, kann eine entsprechende Instanz dieser Klasse nutzen. Dadurch vermeidet man doppelte Implementierung identischen Verhaltens in verschiedenen Klassen. Neue Hundeklassen können bestehendes Verhalten nutzen und müssen nicht zwangsläufig ihr eigenes Verhalten implementieren. Dadurch wird die Entwicklungszeit beschleunigt, da eine hohe Wiederverwendbarkeit des Verhaltens möglich ist.
  • Durch die Zentralisierung des Verhaltens muss bei Änderungen am konkreten Verhalten (z. B. LautBellen soll jetzt noch lauter sein) nur noch eine Klasse (LautBellen) angefasst werden. Vorher mussten alle laut bellenden Hundeklassen anzupassen werden.
  • Nicht nur zur Designzeit, sondern auch zur Laufzeit kann das Verhalten eines Hundes geändert werden.
  • Es lassen sich sofort allgemeine Aussagen über die verschiedenen möglichen Verhalten von Hunden treffen.
  • Durch die Kapselung des Verhaltens können nun auch andere Klassen, außer Hund, das Verhalten wiederverwenden, beispielsweise Wölfe, Füchse oder Katzen.

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.

Das Hundebeispiel mit Strategy Pattern Termini

Klasse Strategy Teilnehmer
Hund Context
BellVerhalten Strategy-Interface
LautBellen, LeiseBellen Konkrete Strategy
bellen()-Methoden Algorithmen

Analyse und Diskussion

Gang Of Four-Definition

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)

Beschreibung

Schematische Darstellung des Strategy Design Pattern

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.

  • Verhalten wird contextunabhängig
  • Context wird implementierungsunabhängig.

Das Strategy Pattern

Context:

public class Context { 

    // Instanzvariable für die Strategy (Komposition) 
    // vom Typ des Interfaces -> Implementierungunabhängigkeit 
    // Defaultverhalten: ConcreteStrategyA 
    private Strategy strategy = new ConcreteStrategyA(); 

    public void execute() { 
        //delegiert Verhalten an Strategy-Objekt 
        strategy.executeAlgorithm(); 
    } 

    public void setStrategy(Strategy strategy) { 
        strategy = strategy; 
    } 

    public Strategy getStrategy() { 
        return strategy; 
    } 
} 
Strategy, ConcreteStrategyA, ConcreteStrategyB:

interface Strategy { 

    public void executeAlgorithm(); 

} 

class ConcreteStrategyA implements Strategy { 

    public void executeAlgorithm() { 
        System.out.println("Concrete Strategy A"); 
    } 

} 

class ConcreteStrategyB implements Strategy { 

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

}  

Anwendungsfälle

  • Alternative zur Unterklassenbildung. Objekte mit neuen Verhalten entstehen nicht durch Vererbung, sondern durch neue Strategieobjekte, mit dem das Objekt konfiguriert wird.
  • Bei vielen ähnlichen Klassen, die sich nur im Verhalten unterscheiden, können die verschiedenen Verhaltensweisen als Strategy-Klassen gekapselt werden.
    • In Eingabemasken kann die Logik zur Validierung und Plausibilitätsprüfung von Benutzereingaben gekapselt werden ([GoF], Seite 384). Statt vieler Validator-Klassen existiert somit nur noch eine Klasse, dessen Validierungslogik entsprechend dem Strategy Pattern entkoppelt wird.
  • Verhalten sollte auch dann vom Context entkoppelt werden, wenn unterschiedliche Ausformungen ein und der selben Funktion benötigt werden.
    • Sortierung einer Collection (Array, List), wobei die konkreten Strategys verschiedene Sortierverfahren repräsentieren. ([PK], Seite 48)
    • Speicherung in verschiedenen Dateiformaten ([WS], "Beispiel")
    • Packer mit verschiedenen Kompressionsalgorithmen ([WS], "Beispiel")
    • Kapselung verschiedener Speicherbelegungsstrategien ([GoF], Seite 384).
  • Entkopplung und Verstecken von komplizierten Algorithmusdetails (spezifische Daten und Strukturen) vor dem Context.
    • Die von den verschiedenen Sortierverfahren benötigten Datenstrukturen (zusätzliche Arrays und Zeiger) werden in der konkreten Strategy-Klasse verborgen und entschlanken somit den Context.
  • Wenn in einer Kontextklasse verschiedene Verhaltensweisen implementiert sind und die gewünschte mit zahlreichen Bedingungen (if ()... else if()... else ...) ausgewählt wird, kann das Strategy Pattern genutzt werden, um die Verhaltensweisen aus dem Kontext auszulagern.

Weitere:

  • Bundeslandübergreifender Ferienkalender, in dem der Algorithmus zur Definition von bundesländerspezifischen Schulferien gekapselt wird.
  • Berechnung von länderspezifischen Steuersätzen ([WS], "Beispiel")

Vorteile

  • Alternative zur Unterklassenbildung. Neuartige Contextklassen mit neuem Verhalten können kreiert werden, ohne Vererbung verwenden zu müssen. Sie entstehen stattdessen durch die Erstellung neuer Verhaltensklassen oder durch die neue Kombination bestehender Verhaltensobjekte.
  • Wiederverwendbarkeit und Entkopplung von Context und Verhalten. Es wird eine Familie von Algorithmen erstellt, welche contextunabhängig verwendet werden kann. Auf der einen Seite können neue Contextobjekte die bestehenden Strategien nutzen und andersrum können neue Strategien erstellt und im Client eingebaut werden, ohne dass der Contextcode bricht (Implementierungsunabhängigkeit des Contexts).
  • Komposition statt Vererbung und Code Recycling. Erweitert man eine Contextklasse mehrmals (ohne Strategy Pattern), um spezifisches Verhalten zu implementieren, so wird zum einen die Verhaltenslogik fest in der jeweiligen Contextklasse codiert und zum anderen Contextcode mit Verhaltenscode vermischt. Es entsteht viel redundanter Code. Wartung und Erweiterungen würden enorm erschwert und zeitintensiv. Kapselt man allerdings das Verhalten in eine separate Strategieklasse und delegiert das Verhalten an das Strategieobjekt, so wird das Verhalten contextunabhängig, (zur Laufzeit) auswechselbar, wiederverwendbar und erweiterbar.
    In Sprache ohne Mehrfachvererbung vergibt man sich damit nicht den wertvollen Platz der Superklasse für das Verhalten und kann diese anderweitig verwenden.
  • Dynamisches Verhalten. Das Verhalten des Contexts kann durch entsprechende Setter zur Laufzeit geändert werden.
  • Vermeidung von Bedingungen. Das Strategy Pattern löst Situationen, in denen verschiedene Algorithmen in einer Klasse definiert sind und zwischen ihnen mittels Bedingungen (if-else) ausgewählt wird. Sie können in Strategieklassen ausgelagert und vom Client gesetzt werden. Damit ist der Context von der Mehrfachimplementierung einer Funktion und der Auswahllogik dazu befreit. Die Kohäsion des Contexts steigt. Hierbei sei auf das State Design Pattern verwiesen.
  • Alternativimplementierung. Auch kann die selbe Funktion (z.B. Sortieren) durch verschiedene Implementierungen angeboten werden, die sich aber in nichtfunktionaler Hinsicht unterscheiden (Performance, Speicherbedarf).

Nachteile

  • Enge Kopplung zwischen Client und Strategieimplementierungen. Der Client setzt die passende Strategie des Contexts und muss folglich die Implementierungen kennen. Diese enge Kopplung des Clients an die konkreten Strategien ist nachteilig. Das Strategy Pattern sollte nur genutzt werden, wenn das Variieren der Strategien für den Client wichtig ist.
    Aber das Problem kann mit einer Factory behoben werden (siehe Borderbeispiel unten). Damit lässt sich die Erstellungslogik (mit den konrekten Implementierungen) aus dem Client in eine Factory auslagern.
  • Unnötige Context-Strategie-Kommunikation möglich. Es kann zu unnötiger Kommunikation und Datentransfer zwischen Context und Strategie kommen, wenn die im Interface definierte Methode eine Reihe von Parametern entgegennimmt, die aber gar nicht von allen Strategien verarbeitet werden. Der Context übergibt, erstellt und/oder initialisiert diese trotzdem, da er nur das Interface kennt.

Anwendung in der Java Standardbibliothek

In der Java API wird das Strategy Design Pattern an zahlreichen Stellen angewandt.

Swing Border Classes:

Das Strategy Design Pattern in der Swing Border Architektur

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.

LayoutManager

Das Strategy Pattern wird ebenso bei dem Layout Managern von AWT, Swing und SWT verwendet. Hier am Beispiel von AWT/Swing:

Das Strategy Design Pattern in der LayoutManager Architektur

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... 
} 

Swing Look & Feel

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.

Exkurs: Vergleich zwischen State und Strategy Design Pattern

Der Vergleich zwischen den strukturidentischen Entwurfsmustern State und Strategy erfolgt im Artikel zum State Design Pattern.