Die Struktur von Patterns in der Softwareentwicklung

Die Buchklassiker "Design Patterns" und "Pattern-Oriented Software Architecture" folgen einer ähnlichen Struktur, um ihr Muster zu präsentieren.

Lesezeit: 7 Min.
In Pocket speichern
vorlesen Druckansicht Kommentare lesen

(Bild: B.Forenius/shutterstock.com)

Von
  • 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. Bekannte Bücher zum Thema sind "Design Patterns: Elements of Reusable Object-Oriented Software" und "Pattern-Oriented Software Architecture, Volume 1".

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

In diesem Beitrag wende ich mich den Strukturen der Muster zu und schaue mir die Schritte genau an, denen die Autoren beim Präsentieren ihrer Muster folgen. Die Schritte ähneln einander deutlich.

Bevor ich näher auf die Struktur eines Musters eingehe, möchte ich alle Leser auf den gleichen Wissensstand bringen und mit der Definition eines Musters nach Christopher Alexander beginnen.

Pattern: "Each pattern is a three part rule, which expresses a relation between a certain context, a problem, and a solution."

Das bedeutet, dass ein Muster eine generische Lösung für ein Designproblem beschreibt, das in einem bestimmten Kontext immer wieder auftritt.

  • Der Kontext ist die Designsituation.
  • Das Problem sind die Kräfte, die in diesem Kontext wirken.
  • Die Lösung ist eine Konfiguration, die diese Kräfte ausgleicht.

Um die Vorteile von Mustern zu beschreiben, greift Christopher Alexander auf die drei Begriffe "nützlich", "brauchbar" und "verwendet" zurück.

  • Nützlich: Ein Muster muss nützlich sein.
  • Brauchbar: Ein Muster muss umsetzbar sein.
  • Verwendet: Muster werden entdeckt, aber nicht erfunden. Diese Regel wird die Dreierregel genannt: "A pattern can be called a pattern only if it has been applied to a real world solution at least three times."

Die beiden Bücher "Design Patterns: Elements of Reusable Object-Oriented Software" und "Pattern-Oriented Software Architecture, Volume 1" gehören zweifellos zu den einflussreichsten, die je über Softwareentwicklung geschrieben wurden. Beide Werke wirken allerdings auch etwas einschläfernd. Diesen Effekt führe ich vor allem darauf zurück, dass beide Bücher ihre Muster in sich monoton wiederholenden 13 Schritten präsentieren.

Um nicht in die gleiche Falle zu tappen, bemühe ich mich, die Schritte aus dem Buch "Design Patterns: Elements of Reusable Object-Oriented Software" möglichst kurz und prägnant darzustellen – und wende sie direkt auf das Strategiemuster an. Die Absicht jedes Schritts ist kursiv dargestellt. Die nicht kursiven Inhalte beziehen sich auf das Strategiemuster.

Ein prägnanter Name, der leicht zu merken ist.

Strategiemuster

Eine Antwort auf die Frage: Wie lautet der Zweck des Musters?

Definiere eine Familie von Algorithmen, kapsle sie in Objekten und mache sie während der Laufzeit deines Programms austauschbar.

Alternative Namen für das Muster, falls bekannt.

Policy

Ein motivierendes Beispiel für das Muster.

Ein Container mit Strings kann auf verschiedene Arten sortiert werden. Sie lassen sich lexikografisch, ohne Berücksichtigung der Groß- und Kleinschreibung, umgekehrt, und so weiter sortieren. Es wäre ein Alptraum, müsste man seine Sortierkriterien in seinem Sortieralgorithmus fest codieren. Deutlich flexibler ist es, das Sortierkriterium in einem Objekt zu kapseln und den Sortieralgorithmus mit dem Objekt zu konfigurieren.

Situationen, in denen sich das Muster anwenden lässt.

Das Strategiemuster ist anwendbar, wenn

  • viele verwandte Klassen sich nur in ihrem Verhalten unterscheiden.
  • verschiedene Varianten eines Algorithmus benötigt werden.
  • die Algorithmen für den Nutzer transparent sein sollen.

Struktur

Eine grafische Darstellung des Musters.

Klassen und Objekte, die an diesem Muster teilnehmen.

  • Context: Verwendet eine konkrete Strategie, die die Strategie-Schnittstelle implementiert.
  • Strategy: Deklariert die Schnittstelle für die verschiedenen Strategien.
  • ConcreteStrategyA, ConcreteStrategyB: Implementieren die Strategie.

Kollaboration mit den Teilnehmern.

Der Kontext und die konkrete Strategie implementieren den gewählten Algorithmus. Der Kontext leitet die Client-Anfrage an die verwendete konkrete Strategie weiter.

Wie sehen die Vor- und Nachteile des Musters aus?

Die Vorteile des Strategiemusters sind:

  • Familien von verwandten Algorithmen können einheitlich verwendet werden.
  • Die Implementierungsdetails werden vor dem Benutzer verborgen.
  • Die Algorithmen können während der Laufzeit ausgetauscht werden.

Implementierungstechniken des Musters.

  • Definiere den Kontext und die Strategie-Schnittstelle.
  • Implementiere die konkrete Strategie.
  • Der Kontext kann seine Argumente zur Laufzeit oder zur Kompilierzeit als Template-Parameter erhalten.

Codeschnipsel zur Veranschaulichung der Umsetzung des Musters. Im Buch werden Smalltalk und C++ verwendet.

Das Strategiemuster ist so fest in das Design der Standard Template Library (STL) integriert, dass wir es fast nicht mehr wahrnehmen. Außerdem kommt in der STL oft eine simple Variante des Strategiemusters zum Einsatz.

Hier sind zwei von vielen Beispielen:

  • STL-Algorithmen

std::sort kann mit einem Sortierkriterium parametrisiert werden. Das Sortierkriterium muss ein binäres Prädikat sein. Lambdas sind perfekt für solche binären Prädikate geeignet.

// strategySorting.cpp

#include <algorithm>
#include <functional>
#include <iostream>
#include <string>
#include <vector>

void showMe(const std::vector<std::string>& myVec) {
    for (const auto& v: myVec) std::cout << v << " ";
    std::cout << "\n\n";
}


int main(){

    std::cout << '\n';

    // initializing with an initializer lists
    std::vector<std::string> myStrVec = {"Only", "for", "Testing", "Purpose", "!!!!!"};
    showMe(myStrVec);     // Only for Testing Purpose !!!!! 

    // lexicographic sorting
    std::sort(myStrVec.begin(), myStrVec.end());
    showMe(myStrVec);    // !!!!! Only Purpose Testing for 

    // case insensitive first character
    std::sort(myStrVec.begin(), myStrVec.end(), 
              [](const std::string& f, const std::string& s){ return std::tolower(f[0]) < std::tolower(s[0]); });
    showMe(myStrVec);   // !!!!! for Only Purpose Testing 

    // sorting ascending based on the length of the strings
    std::sort(myStrVec.begin(), myStrVec.end(), 
              [](const std::string& f, const std::string& s){ return f.length() < s.length(); });
    showMe(myStrVec);   // for Only !!!!! Purpose Testing 

    // reverse 
    std::sort(myStrVec.begin(), myStrVec.end(), std::greater<std::string>() );
    showMe(myStrVec);   // for Testing Purpose Only !!!!! 

    std::cout << "\n\n";

}

Das Programm strategySorting.cpp sortiert den Vektor lexikografisch, unabhängig von der Groß- und Kleinschreibung, aufsteigend nach der Länge der Strings und in umgekehrter Reihenfolge. Für die umgekehrte Sortierung verwende ich das vordefinierte Funktionsobjekt std::greater. Die Ausgabe der Applikation zeigt das Programm im Quellcode an.

  • STL-Container

Eine Policy ist eine generische Funktion oder Klasse, deren Verhalten konfiguriert werden kann. In der Regel gibt es Standardwerte für die Policy-Parameter. std::vector und std::unordered_map sind Beispiele für die Umsetzung von Policies in C++. Natürlich ist eine Policy eine Strategie, die zur Kompilierzeit über Template-Parameter konfiguriert wird.

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 (Zeile 1) oder von std::pair<const Key, T> (Zeile 2) abhängt. Außerdem verfügt std::unorderd_map über eine Standard-Hash-Funktion (Zeile 3) und eine Standard-Gleichheitsfunktion (Zeile 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.

Mindestens zwei bekannte Beispiele für die Verwendung des Musters.

Es gibt weitaus mehr Anwendungsfälle von Strategien in modernem C++.

  • In C++17 lassen sich etwa 70 der STL-Algorithmen mit einer Execution Policy konfigurieren. Hier ist eine Überladung von std::sort: zu sehen:
template< class ExecutionPolicy, class RandomIt >
void sort( ExecutionPolicy&& policy,
           RandomIt first, RandomIt last );

Dank der Execution Policy ist es möglich, sequenziell (std::execution::seq), parallel (std::execution::par) oder parallel und vektorisiert (std::execution::par_unseq) zu sortieren.

  • In C++20 haben die meisten der klassischen STL-Algorithmen ein Ranges-Pendant. Diese Ranges-Pendants unterstützen zusätzliche Erweiterungspunkte wie Projektionen. Detaillierter habe ich das bereits in meinem Artikel "Projektionen mit Ranges" ausgeführt.

Patterns, die eng mit diesem Pattern verwandt sind.

Strategieobjekte sollten leichtgewichtige Objekte sein. Folglich sind Lambda-Ausdrücke ideal geeignet.

Wie sich ein Muster, ein Algorithmus oder ein Framework voneinander unterscheiden, möchte ich in meinem nächsten Artikel klären und dabei auch Begriffe wie Pattern-Sequenzen und Pattern-Sprachen einführen. (map)