(Credits: Blog von Vishal Chovatiya)
„Das Strategy Pattern kapselt unterschiedliche Algorithmen bzw. Verhaltensweisen hinter einer gemeinsamen Schnittstelle, sodass sie unabhängig voneinander ausgetauscht werden können.”
Das Strategy-Pattern gehört zu den Verhaltensmustern (Behavioral Patterns) und dient dazu, eine Familie von Algorithmen zu definieren, jeden davon zu kapseln und sie austauschbar zu machen.
Statt eine bestimmte Logik fest in eine Klasse zu programmieren, wird diese Logik in eine eigene Strategie-Klasse ausgelagert, die ein gemeinsames Interface implementiert. Der sogenannte Kontext (Context) hält lediglich eine Referenz oder einen Zeiger auf dieses Interface und delegiert die eigentliche Arbeit an das konkret zugewiesene Strategie-Objekt.
Dadurch lässt sich das Verhalten eines Objekts zur Laufzeit ändern, indem einfach eine andere Strategie-Instanz gesetzt wird. Neue Algorithmen können hinzugefügt werden, ohne bestehenden Code zu verändern – ein klassisches Beispiel für das Open-Closed-Prinzip.
Gleichzeitig wird die Verzweigungslogik (z. B. lange if-else- oder switch-Ketten) vermieden,
die sonst nötig wäre, um zwischen verschiedenen Verhaltensweisen zu unterscheiden.
In C++ bietet sich das Pattern besonders an, da es sowohl klassisch über abstrakte Basisklassen mit virtuellen Funktionen
als auch moderner über std::function oder Templates umgesetzt werden kann.
Insgesamt fördert das Strategy-Pattern lose Kopplung, bessere Testbarkeit einzelner Algorithmen und eine klare Trennung von Verantwortlichkeiten.
Das folgende UML-Diagramm beschreibt eine Implementierung des Strategy Patterns. Es besteht im Wesentlichen aus drei Teilen:
- Client: Diese Klasse verwendet austauschbare Algorithmen. Sie besitzt eine Referenz eines
StrategyBase-Objekts. - StrategyBase: Definiert eine Schnittstelle, die allen unterstützten Algorithmen gemeinsam ist.
Der Client verwendet diese Schnittstelle, um den über eine
ConcreteStrategy-Schnittstelle definierten Algorithmus aufzurufen. - ConcreteStrategy: Konkrete Strategieklasse, die von der
StrategyBase-Klasse erbt. Jede Instanz implementiert einen anderen Algorithmus, der (über dieStrategyBase-Schnittstelle) vom Client verwendet werden kann.
Abbildung 1: Schematische Darstellung des Strategy Patterns.
Wir betrachten die Anwendung des Strategy Design Patterns in einer Hierarchie von Klassen.
In Abbildung 2 kann man zwei Hierarchien erkennen:
-
Eine Hierarchie von anwendungsorientierten Klassen. Hier werden in einer Vaterklasse zentrale Methoden virtuell definiert (z.B. Methode
draw), diese sind in den Kindklassen geeignet zu überschreiben. -
Die überschriebenen Methoden könnten eine Realisierung umsetzen, wollen dies aber vermeiden, um für unterschiedliche Technologien (Strategien) anpassbar zu sein. Dazu benötigt man eine zweite Hierarchie von Klassen, die unterschiedliche Implementierungen bereitstellen.
Abbildung 2: Anwendung des Strategy Design Patterns in einer Klassenhierarchie.
In einer Umsetzung des Strategy Design Patterns mit Modern C++ Sprachmitteln
kommt das Klassentemplate std::function zum Einsatz.
std::function ist ein universeller polymorpher Funktionswrapper, der zum Beispiel Lambda Objekte, freie Funktionen
und andere aufrufbare Objekte kapseln kann.
Mit Hilfe von std::function fallen in dieser Variante alle Klassen weg, die mit Strategien zu tun haben.
Abbildung 3: Umsetzung des Strategy Design Patterns in Modern C++ mit std::function.
std::sort: Im optionalen dritten Parameter kann ein Callable übergeben werden, dass die Strategie des Sortierens implementiert.
Man kann in der Implementierung zwischen dynamischer und statischer Ausprägung unterscheiden.
Die statische Implementierung basiert in C++ auf der Verwendung von Schablonen (template),
die dynamische Implementierung auf dynamisch allokierten Strategy-Objekten (new),
die zur Laufzeit austauschbar sind.
„Real-World” Beispiel
Im Verzeichnis ECommerceApp ist ein Beispiel gezeigt, dass Bezahlvorgänge in einer ECommerce-Anwendung auf Strategien umsetzt.
Das Decorator Design Pattern konnte man sowohl dynamisch als auch statisch implementieren, und dies kann man beim Strategy Design Pattern genauso umsetzen.
Im Prinzip gibt es nichts Besonderes an dieser Umsetzung zu beobachten,
außer dass man den Algorithmus (hier im Beispiel: Methode add_list_item)
nicht über die vtable anspricht (indirekter Methodenaufruf),
sondern als C++–Template-Parameter übergibt.
Dies bedeutet,
dass der Algorithmus/die Strategie zur Laufzeit nicht änderbar ist.
Der Methodenaufruf selbst erfolgt dann direkt, was eine verbesserte Laufzeit zur Folge hat!
Quellcode – Statische Umsetzung des Strategy Design Patterns (Templates)
Quellcode 1 – Einfaches konzeptionelles Beispiel
Quellcode 2 – Einfaches konzeptionelles Beispiel mit std::function
Quellcode 3 – Einfaches Beispiel zu einem Warenkorb (normale Preise, Black Friday Preise)
Quellcode 4 – Einfaches Beispiel mit einer Hierarchie von Klassen
Quellcode 5 – Einfaches Beispiel mit einer Hierarchie von Klassen und std::function
Die Anregungen zum konzeptionellen Beispiel finden Sie unter
https://refactoring.guru/design-patterns
und
vor.
Das Beispiel zu der Auswahl der Bezahlungsmethode in einer E-Commerce App orierentiert sich an
https://refactoring.guru/design-patterns/strategy
Ein weiteres interessantes Beispiel zum Strategy Pattern widmet sich der Suche nach Knoten in einem gerichteten Graphen. Die Art der Suche (Depth First Search / DFS, Breadth First Search / BFS) wird als Strategie realisiert.
