Patterns in der Softwareentwicklung: Das Stellvertretermuster
Ein Stellvertreter kontrolliert den Zugriff auf ein anderes Objekt und ermöglicht es, zusätzliche Operationen auf das ursprüngliche Objekt durchzuführen.
- 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 Stellvertreter-Muster (Proxy Pattern) ist eines der sieben Strukturmuster aus dem Buch "Design Patterns: Elements of Reusable Object-Oriented Software".
Ein Stellvertreter kontrolliert den Zugriff auf ein anderes Objekt und ermöglicht es, zusätzliche Operationen vor oder nach dem Zugriff auf das ursprüngliche Objekt durchzuführen.
Welches Idiom ist charakteristisch für C++? RAII (Resource Acquisition Is Initialization)! Und RAII ist die C++-Methode zur Umsetzung des Stellvertretermusters.
Stellvertretermuster
Zweck
- Bietet einen Platzhalter für den Zugriff auf ein anderes Objekt
Auch bekannt als
- Surrogat
Anwendungsfall
- Kontrolliert den Zugriff auf ein anderes Objekt
- Remote-Stellvertreter (fungiert als Vermittler für einen Remote Service)
- Virtueller Stellvertreter (erstellt ein Objekt auf Anfrage)
- Sicherheitsstellvertreter (erweitert eine Anfrage um Sicherheitsattribute)
- Caching-Stellvertreter (stellt zwischengespeicherte Anfragen zu)
Struktur
Proxy
- Kontrolliert den Zugriff und die Lebensdauer des
RealSubject
- Unterstützt die gleiche Schnittstelle wie RealSubject
Subject
- Definiert die Schnittstelle zwischen dem
Proxy
und demRealSubject
RealSubject
- Implementiert die Schnittstelle
Beispiel
Die folgenden Beispiele verwenden zwei generische Stellvertreter: std::unique_ptr
und std::shared_ptr.
// proxy.cpp
#include <iostream>
#include <memory>
class MyInt{
public:
MyInt(int i):i_(i){}
int getValue() const {
return i_;
}
private:
int i_;
};
int main(){
std::cout << '\n';
MyInt* myInt = new MyInt(1998); // (3)
std::cout << "myInt->getValue(): "
<< myInt->getValue() << '\n';
std::unique_ptr<MyInt> uniquePtr{new MyInt(1998)}; // (1)
std::cout << "uniquePtr->getValue(): "
<< uniquePtr->getValue() << '\n';
std::shared_ptr<MyInt> sharedPtr{new MyInt(1998)}; // (2)
std::cout << "sharedPtr->getValue(): "
<< sharedPtr->getValue() << '\n';
std::cout << '\n';
}
Beide Smart Pointer können transparent auf die Member-Funktionen getValue
von MyInt
zugreifen. Es stellt keinen Unterschied dar, ob man die Memberfunktion getValue
über den std::unique_ptr
(1), über den std::shared_ptr
(2) oder direkt über das Objekt aufruft. Alle Aufrufe geben denselben Wert zurück.
Bekannte Verwendungen
Die Smart Pointer modellieren das Stellvertreter-Muster in C++. Außerdem ist das RAII-Idiom die C++-Adaption des Stellvertretermusters. RAII ist das sehr häufig verwendete Idiom in C++. Ich werde in ein paar Zeilen mehr darüber schreiben.
Verwandte Muster
- Das Adaptor-Muster passt eine bestehende Schnittstelle an, während das Facade-Muster eine neue, vereinfachte Schnittstelle schafft.
- Das Decorator-Muster ist dem Stellvertreter strukturell ähnlich, aber der Decorator hat einen anderen Zweck. Ein Decorator erweitert ein Objekt mit zusätzlichen Aufgaben. Ein Stellvertreter kontrolliert den Zugriff auf ein Objekt.
- Die Fassade ähnelt dem Stellvertreter, weil es den Zugriff auf ein Objekt kapselt. Die Facade unterstützt nicht die gleiche Schnittstelle wie der Stellvertreter, sondern eine vereinfachte.
Vor- und Nachteile
Vorteile
- Das zugrundeliegende Objekt ist für den Client völlig transparent,
- der Stellvertreter kann Anfragen direkt beantworten, ohne den Client zu benutzen und
- der Stellvertreter kann transparent erweitert oder durch einen anderen Proxy ersetzt werden.
Nachteile
- Die Trennung von Proxy und Objekt macht den Code schwieriger
- Die weitergeleiteten Proxy-Aufrufe können die Performance negativ beeinflussen
Das RAII-Idiom
RAII steht für Resource Acquisition Is Initialization. Das wahrscheinlich wichtigste Idiom in C++ besagt, dass eine Ressource im Konstruktor angefordert und im Destruktor des Objekts freigegeben werden sollte. Der Kerngedanke ist, dass der Destruktor automatisch aufgerufen wird, wenn das Objekt seinen Gültigkeitsbereich verlässt. Anders ausgedrückt: Die Lebensdauer einer Ressource ist an die Lebensdauer einer lokalen Variablen gebunden, und C++ verwaltet die Lebensdauer von lokalen Variablen automatisch.
Es gibt einen großen Unterschied zwischen dem Stellvertretermuster und dem RAII-Idiom in C++. Im klassischen Stellvertretermuster implementieren der Stellvertreter und das Objekt dieselbe Schnittstelle. Daher ruft man eine Mitgliedsfunktion des Stellvertreters auf, und dieser Aufruf wird an das Objekt delegiert. Im Gegensatz dazu ist es typisch für RAII, dass die Operation am Objekt implizit ausgeführt wird.
Das folgende Beispiel zeigt das deterministische Verhalten von RAII in C++
// raii.cpp
#include <iostream>
#include <new>
#include <string>
class ResourceGuard{
private:
const std::string resource;
public:
ResourceGuard(const std::string& res):resource(res){
std::cout << "Acquire the " << resource << "." << '\n';
}
~ResourceGuard(){
std::cout << "Release the "<< resource << "." << '\n';
}
};
int main() { // (2)
std::cout << '\n';
ResourceGuard resGuard1{"memoryBlock1"}; // (1)
std::cout << "\nBefore local scope" << '\n';
{
ResourceGuard resGuard2{"memoryBlock2"}; // (3)
} // (4)
std::cout << "After local scope" << '\n';
std::cout << '\n';
std::cout << "\nBefore try-catch block" << '\n';
try{
ResourceGuard resGuard3{"memoryBlock3"};
throw std::bad_alloc(); // (5)
}
catch (std::bad_alloc& e){ // (6)
std::cout << e.what();
}
std::cout << "\nAfter try-catch block" << '\n';
std::cout << '\n';
}
ResourceGuard
ist ein Guard, der seine Ressource verwaltet. In diesem Fall steht der String für die Ressource. ResourceGuard
erstellt in seinem Konstruktor die Ressource und gibt sie in seinem Destruktor wieder frei. Er erledigt seine Aufgabe sehr zuverlässig.
Der Destruktor von resGuard1
(1) wird am Ende der main
-Funktion (2) aufgerufen. Die Lebensdauer von resGuard2
(3) endet bereits in (4). Daher wird der Destruktor automatisch ausgeführt. Auch das Auslösen einer Ausnahme hat keinen Einfluss auf die Zuverlässigkeit von resGuard3
(5). Der Destruktor wird am Ende des try
-Blocks (Zeile 6) aufgerufen.
Der Screenshot zeigt die Lebensdauern der Objekte.
Es ist ziemlich einfach, dank des RAII-Idioms den ResourceGuard
in einen LockGuard
zu transformieren.
class LockGuard{
private:
static inline std::mutex m;
public:
LockGuard() {
m.lock();
}
~LockGuard() {
m.unlock();
}
};
Alle Instanzen von LockGuard
teilen sich denselben Mutex m
. Wenn eine Instanz den Gültigkeitsbereich verlässt, geben sie automatisch ihren Mutex m
frei.
Ich habe behauptet, dass das RAII-Idiom das wichtigste Idiom in C++ ist. Als Beleg führe ich ein paar prominente Beispiele auf:
Anwendungen von RAII
- Container der STL, einschließlich
std::string
: Sie allokieren automatisch in ihrem Konstruktor Speicher und geben diesen auch wieder automatisch in ihrem Destruktor frei - Smart Pointers:
std::unqiue_ptr
undstd::shared_ptr
verwalten den zugrundeliegenden Raw Pointer; sie geben den Speicher des zugrundeliegenden Raw Pointer automatisch wieder frei, wenn er nicht mehr benötigt wird. - Locks:
std::lock_guard, std::unique_lock, std::shared_lock
undstd::scoped_lock
sperren in ihrem Konstruktor den zugrundeliegenden Mutex und entsperren ihn in ihrem Destruktor automatisch std::jthread: std::jthread
in C++20 ist ein verbesserterstd::thread
aus C++11;std::jthread
ruft automatischjoin
in seinem Destruktor auf, falls dies nötig ist
Wie geht's weiter?
In meinem nächsten Artikel werde ich meine Reise durch die Muster aus dem Buch "Design Patterns: Elements of Reusable Object-Oriented Software fortsetzen. Das Beobachtermuster ist ein Verhaltensmuster, das in der Werkzeugkiste eines jeden professionellen Programmierers sein sollte. (rme)