zurück zum Artikel

Patterns in der Softwareentwicklung: Das Strategiemuster

Rainer Grimm

(Bild: Shutterstock.com / Impact Photography)

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

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 [1]". 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

Auch bekannt als

Anwendungsfall

Struktur

Strategy

ConcreteStrategy

Context

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 [2]" 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:

Vorteile

// 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

In meinen letzten Artikeln habe ich die folgenden Design Patterns aus dem bahnbrechenden Buch "Design Patterns: Elements of Reusable Object-Oriented Software [5]" 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 [8].

(rme [9])


URL dieses Artikels:
https://www.heise.de/-7372515

Links in diesem Artikel:
[1] https://en.wikipedia.org/wiki/Design_Patterns
[2] https://www.grimm-jaud.de/index.php/blog/hashfunktionen
[3] https://www.heise.de/blog/Patterns-in-der-Softwareentwicklung-Die-Template-Methode-7365112.html
[4] https://www.heise.de/blog/Patterns-in-der-Softwareentwicklung-Das-Brueckenmuster-7308881.html
[5] https://en.wikipedia.org/wiki/Design_Patterns
[6] https://www.modernescpp.org/fundamentals-for-c-professionals/
[7] https://www.modernescpp.org/design-patterns-and-architectural-patterns-with-c/
[8] https://dashboard.mailerlite.com/forms/241414/73224903163119423/share
[9] mailto:rme@ix.de