Strategy Pattern: Software-Entwurfsmuster für variable Verhaltensstrategien

In der objektorientierten Programmierung unterstützen Design Patterns (Entwurfsmuster) die Entwickler mit bewährten Lösungsansätzen und -schablonen. Ist das passende Lösungsschema gefunden, müssen nur noch individuelle Anpassungen vorgenommen werden. Derzeit gibt es insgesamt 70 Entwurfsmuster, die auf bestimmte Einsatzgebiete zugeschnitten sind. Strategy Design Patterns fokussieren das Verhalten von Software.

Was ist das Strategy Pattern?

Das Strategy Pattern gehört zu den Behavioral Patterns (Verhaltensmustern), die eine Software mit verschiedenen Lösungsmethoden ausstatten. Hinter den Strategien steht eine Familie von Algorithmen, die vom eigentlichen Programm abgegrenzt werden und autonom (= austauschbar) sind. Zu einem Strategie-Entwurfsmuster gehören auch gewisse Vorgaben und Hilfestellungen für Entwickler. So beschreiben Strategy Patterns, wie man Klassen aufbaut, eine Gruppe von Klassen arrangiert und Objekte erstellt. Eine Besonderheit des Strategy Design Patterns ist, dass ein variables Programm- und Objektverhalten auch zur Laufzeit einer Software realisiert werden kann.

Wie sieht die UML-Darstellung eines Strategy Patterns aus?

Strategy Patterns werden normalerweise mit der grafischen Modellierungssprache UML (Unified Modelling Language) entworfen. Sie visualisiert Entwurfsmuster mit einer standardisierten Notation und verwendet dabei spezielle Zeichen und Symbole. Die UML stellt für die objektorientierte Programmierung verschiedene Diagrammtypen zur Verfügung. Für die Darstellung eines Strategie-Entwurfsmusters wird in der Regel ein Klassendiagramm mit mindestens drei Grundkomponenten gewählt:

  • Context (Kontext bzw. Kontext-Klasse)
  • Strategy (Strategie bzw. Strategie-Klasse)
  • ConcreteStrategy (Konkrete Strategie)

Im Strategy Design Pattern übernehmen die Grundkomponenten spezielle Funktionen: Die Verhaltensmuster der Context-Klasse werden in verschiedene Strategy-Klassen ausgelagert. Diese separaten Klassen beherbergen die Algorithmen, die als ConcreteStrategies bezeichnet werden. Über eine Referenz (also einen internen Verweis) kann der Context bei Bedarf auf die ausgelagerten Berechnungsvarianten (ConcreteStrategyA, ConcreteStrategyB etc.) zugreifen. Dabei interagiert er nicht direkt mit den Algorithmen, sondern mit einer Schnittstelle.

Das Strategy-Interface kapselt die Berechnungsvarianten und kann zugleich von allen Algorithmen implementiert werden. Für die Interaktion mit dem Context stellt die generische Schnittstelle nur eine einzige Methode zum Auslösen von ConcreteStrategy-Algorithmen zur Verfügung. Zu den Interaktionen mit dem Context gehört neben dem Strategieaufruf auch der Austausch von Daten. An Strategiewechseln, die auch zur Laufzeit eines Programms stattfinden können, ist das Strategy-Interface ebenfalls beteiligt.

Fakt

Mit einer Kapselung wird der direkte Zugriff auf Algorithmen und interne Datenstrukturen unterbunden. Ein externe Instanz (Client, Context) kann Berechnungen und Funktionen ausschließlich über definierte Schnittstellen in Anspruch nehmen. Dabei sind nur diejenigen Methoden und Datenelemente eines Objekts zugänglich, die für die externe Instanz relevant sind.

Wie das Entwurfsmuster in einem praxisnahen Projekt umgesetzt wird, erklären wir nun anhand eines Strategy-Pattern-Beispiels.

Das Strategy Pattern am Beispiel erklärt

In unserem Beispiel (wir orientieren uns an dem Studienprojekt zum Strategy Pattern von Philipp Hauer) soll eine Navigations-App mithilfe eines Strategie-Entwurfsmusters realisiert werden. Die App soll eine Routenberechnung durchführen, die sich an den üblichen Transportmitteln orientiert. Der Nutzer kann zwischen drei Optionen wählen:

  • Fußgänger (ConcreteStrategyA)
  • Auto (ConcreteStrategyB)
  • Nahverkehr (ConcreteStrategyC)

Überträgt man diese Vorgaben in eine UML-Grafik, werden Aufbau und Funktionsweise des benötigten Strategy Patterns deutlich:

Der Client ist in unserem Beispiel die Bedienoberfläche (Graphical User Inferface, GUI) einer Navigations-App mit Schaltflächen für die Routenberechnung. Trifft der Anwender eine Auswahl und tippt auf eine Schaltfläche, wird eine konkrete Route kalkuliert. Der Context (Navigator-Klasse) hat die Aufgabe, eine Reihe von Kontrollpunkten auf der Karte zu errechnen und darzustellen. Die Navigator-Klasse verfügt über eine Methode zum Umschalten der aktiven Routing-Strategie. So kann über die Client-Schaltflächen zwischen den Transportmitteln problemlos gewechselt werden.

Löst man etwa mit der Fußgänger-Schaltfläche des Clients einen entsprechenden Befehl aus, wird der Service „Berechne die Fußgänger-Route“ (ConcreteStrategyA) angefordert. Die Methode executeAlgorithm() (in unserem Beispiel die Methode: berechneRoute (A, B)) akzeptiert einen Ursprung und ein Ziel und gibt eine Sammlung der Kontrollpunkte der Route zurück. Der Context nimmt den Client-Befehl entgegen und entscheidet auf der Basis von vorher definierten Richtlinien (Policy) über die passende Strategie (setStrategy: Fußgänger). Per Call delegiert er die Anfrage an das Strategy-Objekt und dessen Schnittstelle.

Durch getStrategy() wird die aktuell gewählte Strategie im Context (Navigator-Klasse) hinterlegt. Die Ergebnisse der ConcreteStrategy-Berechnungen fließen in die weitere Aufbereitung sowie in die grafische Routendarstellung in der Navigations-App ein. Entscheidet sich der Anwender für eine andere Route, indem er beispielsweise danach auf die Schaltfläche „Auto“ klickt, wechselt der Context auf die angefragte Strategie (ConcreteStrategyB) und veranlasst über einen weiteren Call eine neue Berechnung. Am Ende der Prozedur wird eine modifizierte Wegbeschreibung für das Transportmittel Auto ausgegeben.

In unserem Beispiel kann die Pattern-Mechanik mit relativ übersichtlichem Code umgesetzt werden:

Context:

public class Context {

    //vorgegebener Standardwert (Default-Verhalten): ConcreteStrategyA
    private Strategy strategy = new ConcreteStrategyA(); 

    public void execute() { 
        //delegiert das Verhalten an ein 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"); 
    } 

}  

Client:

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

}

Was sind die Vor- und Nachteile des Strategy Patterns?

Die Vorteile eines Strategy Patterns werden erkennbar, wenn man die Perspektive eines Programmierers und Systemadministrators einnimmt. Generell führt die Zerlegung in autonome Module und Klassen zu einer besseren Strukturierung des Programmcodes. In den abgegrenzten Teilbereichen hat es der Programmierer unserer Beispiel-App mit schlankeren Code-Segmenten zu tun. So lässt sich der Umfang der Navigator-Klasse durch die Auslagerung der Strategien verringern, und auf eine Bildung von Unterklassen kann im Bereich des Kontextes verzichtet werden.

Da im schlankeren und sauber abgegrenzten Code die internen Abhängigkeiten von Segmenten im Rahmen bleiben, haben Änderungen geringere Auswirkungen. Sie ziehen also seltener weitere – möglicherweise langwierige – Umprogrammierungen nach sich; teils können diese sogar ganz ausgeschlossen werden. Übersichtlichere Code-Segmente lassen sich auch langfristig besser pflegen, zudem werden die Problemdiagnose und die Fehlersuche erleichtert.

Darüber hinaus profitiert die Bedienung, da die Beispiel-App mit einer benutzerfreundlichen Oberfläche ausgestattet werden kann. Über deren Schaltflächen können Anwender das Programmverhalten (Routenberechnung) auf einfache Weise variabel steuern und bequem zwischen Optionen auswählen.

Da der Context der Navigations-App durch die Kapselung der Algorithmen nur mit einer Schnittstelle interagiert, ist er unabhängig von der konkreten Implementierung einzelner Algorithmen. Werden zu einem späteren Zeitpunkt die Algorithmen geändert oder neue Strategien eingeführt, muss der Code des Contexts nicht geändert werden. So könnte man die Routenberechnung schnell und unkompliziert mit zusätzlichen ConcreteStrategies für Flugzeugrouten, Schiffs- und Fernverkehr ergänzen. Die neuen Strategien müssen lediglich das Strategy-Interface korrekt implementieren.

Strategy Patterns erleichtern die ohnehin schon schwierige Programmierung objektorientierter Software durch eine weitere positive Eigenschaft. Sie ermöglichen den Entwurf mehrfach- und wiederverwendbarer Software(-Module), deren Entwicklung als besonders anspruchsvoll gilt. So könnten auch verwandte Context-Klassen die ausgelagerten Strategien zur Routenberechnung via Interface nutzen und müssen sie nicht mehr selbst implementieren.

Trotz der zahlreichen Vorteile hat das Strategy Pattern auch einige Nachteile. Der Software-Entwurf erzeugt durch seinen komplexeren Aufbau möglicherweise Redundanzen und Ineffizienzen in der internen Kommunikation. So kann die generische Strategy-Schnittstelle, die ja alle Algorithmen gleichermaßen implementieren müssen, im Einzelfall überdimensioniert sein.

Ein Beispiel: Nachdem der Context gewisse Parameter erstellt und initialisiert hat, übergibt er sie an die generische Schnittstelle und die darin definierte Methode. Die letztlich implementierte Strategie benötigt aber nicht unbedingt alle kommunizierten Context-Parameter und verarbeitet sie demzufolge auch nicht. Eine bereitgestellte Schnittstelle wird im Strategy Pattern also nicht immer optimal genutzt und ein erhöhter Kommunikationsaufwand mit überflüssigen Datentransfers lässt sich nicht immer vermeiden.

Bei der Implementierung gibt es zudem eine enge interne Abhängigkeit zwischen Client und Strategien. Da der Client die Auswahl trifft und die konkrete Strategie per Auslösebefehl anfordert (in unserem Beispiel die Berechnung der Fußgängerroute), muss er die ConcreteStrategies kennen. Man sollte daher dieses Entwurfsmuster nur verwenden, wenn Strategie- und Verhaltenswechsel wichtig bzw. elementar für die Verwendung und die Funktionsweise einer Software sind.

Die aufgeführten Nachteile können teilweise umgangen oder ausgeglichen werden. So wird die Zahl der Objektinstanzen, die im Strategy Pattern in größerer Zahl anfallen können, häufig durch eine Implementierung in ein Flyweight Pattern reduziert. Die Maßnahme wirkt sich auch positiv auf die Effizienz und den Speicherbedarf einer Anwendung aus.

Wo kommt das Strategy Pattern zum Einsatz?

Das Strategy Design Pattern ist als grundlegendes Entwurfsmuster in der Software-Entwicklung nicht auf ein bestimmtes Einsatzgebiet beschränkt. Vielmehr ist die Art der Problemstellung ausschlaggebend für die Verwendung des Design Patterns. Software, die anstehende Aufgaben und Probleme mit Variabilität, Verhaltensoptionen und -änderungen lösen muss, ist prädestiniert für das Entwurfsmuster.

So greifen Programme, die unterschiedliche Speicherformate für Dateien oder diverse Sortier- und Suchfunktionen anbieten, auf Strategie-Entwurfsmuster zurück. Auch im Bereich der Datenkompression kommen Programme zum Einsatz, die auf der Basis des Entwurfsmusters unterschiedliche Kompressionsalgorithmen implementieren. Sie können dadurch z. B. variabel Videos in ein gewünschtes platzsparendes Dateiformat konvertieren oder komprimierte Archivdateien (z. B. ZIP- oder RAR-Dateien) mit speziellen Entpackerstrategien wieder in den Ursprungszustand zurückversetzen. Ein anderes Beispiel wäre die Speicherung eines Dokuments oder einer Grafik in verschiedenen Dateiformaten.

Das Design Pattern ist zudem an der Entwicklung und Implementierung von Spielesoftware beteiligt, die z. B. zur Laufzeit flexibel auf wechselnde Spielsituationen reagieren muss. Unterschiedliche Charaktere, spezielle Ausrüstungen, Verhaltensmuster von Figuren oder verschiedene Moves (besondere Bewegungen einer Spielfigur) können in Form von ConcreteStrategies hinterlegt werden.

Ein weiteres Einsatzgebiet von Strategy Patterns ist Steuersoftware. Durch den Austausch von ConcreteStrategies können Berechnungssätze problemlos an Berufsgruppen, Länder und Regionen angepasst werden kann. Des Weiteren nutzen Programme, die Daten in verschiedene Grafikformate übersetzen (z. B. als Linien-, Kreis- oder Säulendiagramm), Strategy Patterns.

Speziellere Anwendungen von Strategy Patterns finden sich in der Java Standardbibliothek (Java API) und bei Java GUI-Toolkits (z. B. AWT, Swing und SWT), die bei der Entwicklung und Erzeugung von grafischen Benutzerschnittstellen einen Layout-Manager nutzen. Dieser kann bei der Interface-Entwicklung unterschiedliche Strategien für die Anordnung von Komponenten implementieren. Weitere Anwendungen von Strategy Design Patterns finden sich bei Datenbanksystemen, Gerätetreibern und Serverprogrammen.

Die wichtigsten Eigenschaften des Strategy Patterns im Überblick

Das Strategie-Entwurfsmuster zeichnet sich im umfangreichen Spektrum der Design Patterns durch folgende Eigenschaften aus:

  • verhaltensorientiert (Verhaltensweisen und -änderungen werden leichter programmier- und implementierbar, auch zur Laufzeit eines Programms sind Änderungen möglich)
  • effizienzorientiert (Auslagerungen vereinfachen und optimieren Code und dessen Pflege)
  • zukunftsorientiert (Änderungen und Optimierungen sind auch mittel- und langfristig leicht zu realisieren)
  • zielt auf Erweiterbarkeit (begünstigt durch die modulare Anlage und die Unabhängigkeit von Objekten und Klassen)
  • zielt auf Wiederverwendbarkeit (z. B. Mehrfachnutzung von Strategien)
  • zielt auf optimierte Bedienbarkeit, Steuerbarkeit und Konfigurierbarkeit von Software
  • erfordert gründliche konzeptionelle Vorüberlegungen (was kann wie an welchen Stellen in Strategie-Klassen ausgelagert werden)
Fazit

Strategy Patterns ermöglichen in der objektorientieren Programmierung mit maßgeschneiderten Problemlösungen eine effiziente und wirtschaftliche Software-Entwicklung. Schon in der Entwurfsphase werden möglicherweise anstehende Veränderungen und Verbesserungen optimal vorbereitet. Das auf Variabilität und Dynamik ausgerichtete System kann insgesamt besser gesteuert und kontrolliert werden. Fehler und Ungereimtheiten werden schneller behoben. Durch wiederverwendbare und austauschbare Komponenten werden besonders in komplexen Projekten mit langfristiger Perspektive Entwicklungskosten eingespart. Allerdings gilt es, das richtige Maß zu finden. Nicht selten werden Entwurfsmuster entweder zu sparsam oder zu häufig eingesetzt.