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.
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 [1]" und "Pattern-Oriented Software Architecture, Volume 1 [2]".
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 [3] genannt: "A pattern can be called a pattern only if it has been applied to a real world solution at least three times."
Struktur eines Musters
Die beiden BĂŒcher "Design Patterns: Elements of Reusable Object-Oriented Software [4]" und "Pattern-Oriented Software Architecture, Volume 1 [5]" 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 [6]" möglichst kurz und prĂ€gnant darzustellen â und wende sie direkt auf das Strategiemuster [7]an. Die Absicht jedes Schritts ist kursiv dargestellt. Die nicht kursiven Inhalte beziehen sich auf das Strategiemuster.
Name
Ein prÀgnanter Name, der leicht zu merken ist.
Strategiemuster
Zweck
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.
Auch bekannt als
Alternative Namen fĂŒr das Muster, falls bekannt.
Policy
Motivation
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.
Anwendbarkeit
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.
Teilnehmer
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.
Interaktionen
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.
Konsequenzen
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.
Implementierung
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.
Beispielcode
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 [8] 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.
Bekannte Anwendungen
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 [9]" ausgefĂŒhrt.
Verwandte Verwendungen
Patterns, die eng mit diesem Pattern verwandt sind.
Strategieobjekte sollten leichtgewichtige Objekte sein. Folglich sind Lambda-AusdrĂŒcke ideal geeignet.
Wie geht's weiter?
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 [10])
URL dieses Artikels:
https://www.heise.de/-7215710
Links in diesem Artikel:
[1] https://en.wikipedia.org/wiki/Design_Patterns
[2] https://en.wikipedia.org/wiki/Pattern-Oriented_Software_Architecture
[3] https://wiki.c2.com/?RuleOfThree
[4] https://en.wikipedia.org/wiki/Design_Patterns
[5] https://en.wikipedia.org/wiki/Pattern-Oriented_Software_Architecture
[6] https://en.wikipedia.org/wiki/Design_Patterns
[7] https://de.wikipedia.org/wiki/Strategie_(Entwurfsmuster)
[8] https://en.cppreference.com/w/cpp/utility/functional
[9] https://www.heise.de/blog/Projektionen-mit-Ranges-7123335.html
[10] mailto:map@ix.de
Copyright © 2022 Heise Medien