Patterns in der Softwareentwicklung: Das Strategiemuster

Das Strategiemuster aus dem Buch "Design Patterns" definiert eine Familie von Algorithmen und kapselt sie in Objekten.

In Pocket speichern vorlesen Druckansicht 11 Kommentare lesen

(Bild: Shutterstock.com / Impact Photography)

Lesezeit: 5 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

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.

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

Das Strategiemuster wird häufig in der Standard Template Library verwendet.

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.

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:

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

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- oder switch-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.

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.

Stay informed about my mentoring programs: Subscribe Here.

(rme)