Patterns in der Softwareentwicklung: Das Strategiemuster
Das Strategiemuster aus dem Buch "Design Patterns" definiert eine Familie von Algorithmen und kapselt sie in Objekten.
- Rainer Grimm
Patterns sind eine wichtige Abstraktion in der modernen Softwareentwicklung. Sie bieten eine klar definierte Terminologie, eine saubere Dokumentation und das Lernen von den Besten. Das Strategiemuster ist ein verhaltensorientiertes Entwurfsmuster aus dem Buch "Design Patterns: Elements of Reusable Object-Oriented Software". Es definiert eine Familie von Algorithmen und kapselt sie in Objekten.
Das Strategiemuster wird häufig in der Standard Template Library verwendet.
Das Strategiemuster
Zweck
- Es definiert eine Familie von Algorithmen und kapselt sie in Objekten.
Auch bekannt als
- Policy
Anwendungsfall
- Verschiedene Variationen eines Algorithmus werden benötigt,
- der Client soll den Algorithmus nicht kennen,
- der Algorithmus sollte während der Laufzeit eines Programms austauschbar sein.
Struktur
Strategy
- Definiert die Schnittstelle fĂĽr die Familie der Algorithmen.
ConcreteStrategy
- Implementiert einen Algorithmus.
Context
- Verwaltet einen Verweis auf eine
ConcreteStrategy,
- besitzt eine
ConcreteStrategy.
Der Context
verfĂĽgt ĂĽber eine ConcreteStrategy
, die in einem Objekt gekapselt ist. Die ConcreteStrategy
implementiert die Schnittstelle von Strategy
. Typischerweise kann die ConcreteStrategy
während der Laufzeit angepasst werden.
Beispiel
Das folgende Beispiel strategy.cpp
verwendet drei konkrete Strategien und folgt den Namenskonventionen im vorherige Bild.
// strategy.cpp
#include <iostream>
#include <memory>
#include <utility>
class Strategy {
public:
virtual void execute() = 0; // (4)
virtual ~Strategy() {}
};
class Context {
std::unique_ptr<Strategy> strat{}; // (1)
public:
void setStrategy(std::unique_ptr<Strategy> strat_) { // (2)
strat = std::move(strat_); }
}
void strategy() { if (strat) strat->execute(); } // (3)
};
class Strategy1 : public Strategy {
public:
void execute() { std::cout << "Strategy1 executed\n"; }
};
class Strategy2 : public Strategy {
public:
void execute() { std::cout << "Strategy2 executed\n"; }
};
class Strategy3 : public Strategy {
public:
void execute() { std::cout << "Strategy3 executed\n"; }
};
int main() {
std::cout << '\n';
Context k;
k.setStrategy(std::make_unique<Strategy1>());
k.strategy();
k.setStrategy(std::make_unique<Strategy2>());
k.strategy();
k.setStrategy(std::make_unique<Strategy3>());
k.strategy();
std::cout << '\n';
}
Context
hat eine Strategie (1), die gesetzt (2) und ausgefĂĽhrt werden kann (3). Jede Strategie muss die Mitgliedsfunktion execute
(4) unterstĂĽtzen. Das Hauptprogramm verwendet drei konkrete Strategien, setzt sie und fĂĽhrt sie aus. Hier ist die Ausgabe des Programms:
Verwendung in C++
Das Strategiemuster wird in der Standard Template Library häufig verwendet. Im Fall von Templates nennen wir es Policy.
Eine Policy ist eine generische Funktion oder Klasse, deren Verhalten konfiguriert werden kann. In der Regel gibt es Default-Werte fĂĽr die Policy-Parameter. std::vector
und std::unordered_map
sind Beispiele fĂĽr Policies in C++.
template<class T, class Allocator = std::allocator<T>> // (1)
class vector;
template<class Key,
class T,
class Hash = std::hash<Key>, // (3)
class KeyEqual = std::equal_to<Key>, // (4)
class allocator =
std::allocator<std::pair<const Key, T>> // (2)
class unordered_map;
Das bedeutet, dass jeder Container einen Standard-Allokator fĂĽr seine Elemente hat, der von T
(1) oder von std::pair<const Key, T>
(2) abhängt. Außerdem verfügt std::unorderd_map
ĂĽber eine Standard-Hash-Funktion (3) und eine Standard-Gleichheitsfunktion (4). Die Hash-Funktion berechnet den Hash-Wert auf der Grundlage des SchlĂĽssels, und die Equal-Funktion kĂĽmmert sich um Kollisionen in den Buckets. In meinem vorherigen Artikel "Hashfunktionen" finden sich weitere Informationen zu std::unordered_map.
Folglich man einen benutzerdefinierten Datentyp wie MyInt
als SchlĂĽssel in einer std::unordered_map
verwenden.
// templatesPolicy.cpp
#include <iostream>
#include <unordered_map>
struct MyInt{
explicit MyInt(int v):val(v){}
int val;
};
struct MyHash{ // (1)
std::size_t operator()(MyInt m) const {
std::hash<int> hashVal;
return hashVal(m.val);
}
};
struct MyEqual{
bool operator () (const MyInt& fir,
const MyInt& sec) const { // (2)
return fir.val == sec.val;
}
};
std::ostream& operator << (std::ostream& strm,
const MyInt& myIn){ // (3)
strm << "MyInt(" << myIn.val << ")";
return strm;
}
int main(){
std::cout << '\n';
using MyIntMap =
std::unordered_map<MyInt, int,
MyHash, MyEqual>; // (4)
std::cout << "MyIntMap: ";
MyIntMap myMap{{MyInt(-2), -2}, {MyInt(-1), -1},
{MyInt(0), 0}, {MyInt(1), 1}};
for(auto m : myMap) std::cout << '{' << m.first
<< ", " << m.second << '}';
std::cout << "\n\n";
}
Ich habe die Hash-Funktion (1) und die Gleichheitsfunktion (2) als Funktionsobjekt implementiert und der Einfachheit halber den Ausgabeoperator ĂĽberladen (3). In (4) wird aus allen Komponenten ein neuer Typ MyIntMap
erstellt, der MyInt
als SchlĂĽssel verwendet. Der folgende Screenshot zeigt die Ausgabe der Instanz myMap
:
Verwandte Muster
- Die Anwendungsfälle der Schablonenmethode und des Strategiemusters sind ziemlich ähnlich. Beide Muster ermöglichen es, Variationen eines Algorithmus bereitzustellen. Die Schablonenmethode basiert auf einer Klassenebene durch Subklassifizierung, das Strategiemuster auf einer Objektebene durch Komposition. Das Strategiemuster erhält seine verschiedenen Strategien als Objekte und kann daher seine Strategien zur Laufzeit austauschen. Die Schablonenmethode kehrt den Kontrollfluss um und folgt damit dem Hollywood-Prinzip: "Don't call us, we will call you". Das Strategiemuster ist oft eine Blackbox. Es ermöglicht es dir, eine Strategie durch eine andere zu ersetzen, ohne ihre Details zu kennen.
- Das Brückenmuster und das Strategiemuster haben die gleiche Struktur, aber unterschiedliche Absichten. Das Bridge Pattern ist ein strukturelles Muster und zielt darauf ab, physische Abhängigkeiten zu entkoppeln, während das Strategiemuster ein Verhaltensmuster ist, das logische Abhängigkeiten entkoppelt und die Injektion von externen Implementierungen ermöglicht.
Vor- und Nachteile
Vorteile
- Algorithmen sind in Objekten gekapselt und können während der Laufzeit ausgetauscht werden.
- Das HinzufĂĽgen neuer Strategien ist einfach. Man muss nur eine neue Strategie implementieren.
- Das Strategiemuster ersetzt die bedingte AusfĂĽhrung auf der Grundlage von
if/else
- oderswitch
-Anweisungen. - Callables sind oft schlanke Implementierungen fĂĽr Strategien in C++:
// sortVariations.cpp
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
bool greater(const std::string& first, const std::string& second) {
return first > second;
}
int main(){
std::vector<std::string>
myStrings{"Only", "for", "testing", "purpose", "."};
// sort ascending
std::sort(myStrings.begin(), myStrings.end());
// sort descending // (1)
std::sort(myStrings.begin(), myStrings.end(), greater);
std::sort(myStrings.begin(), myStrings.end(),
std::greater<std::string>());
std::sort(myStrings.begin(), myStrings.end(),
[](const std::string& first,
const std::string& second) {
return first > second;
});
// sort based on the length of the strings
std::sort(myStrings.begin(), myStrings.end(),
[](const std::string& first,
const std::string& second) {
return first.length() < second.length();
});
}
Das Programm verwendet die Funktion greater
, das vordefinierte Funktionsobjekt std::greater<std::string>
und einen Lambda-Ausdruck (1), um einen std::vector<std::string>
in absteigender Reihenfolge zu sortieren. Die Callables sind in diesem Beispiel binäre Prädikate.
Nachteile
- Anwender müssen die richtige Strategie kennen und auswählen.
- Die Anzahl der Objekte (Strategien) steigt stark.
Wie geht's weiter?
In meinen letzten Artikeln habe ich die folgenden Design Patterns aus dem bahnbrechenden Buch "Design Patterns: Elements of Reusable Object-Oriented Software" vorgestellt:
Die hier aufgelisteten Muster sind die, die mir in der Vergangenheit am wichtigsten waren. In meinem nächsten Artikel werde ich über Idiome in C++ schreiben. Ein Idiom ist eine Implementierung einer Architektur oder eines Entwurfsmusters in einer konkreten Programmiersprache.
In eigener Sache: Modernes C++ Mentoring
- Fundamentals for C++ Professionals (open)
- Design Patterns and Architectural Patterns with C++ (starts in February 2023)
Stay informed about my mentoring programs: Subscribe Here.
(rme)