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.

Lesezeit: 6 Min.
In Pocket speichern
vorlesen Druckansicht Kommentare lesen 1 Beitrag

(Bild: Blackboard/Shutterstock.com)

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 Stellvertreter-Muster (Proxy Pattern) ist eines der sieben Strukturmuster aus dem Buch "Design Patterns: Elements of Reusable Object-Oriented Software".

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

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.

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)

Proxy

  • Kontrolliert den Zugriff und die Lebensdauer des RealSubject
  • Unterstützt die gleiche Schnittstelle wie RealSubject

Subject

  • Definiert die Schnittstelle zwischen dem Proxy und dem RealSubject

RealSubject

  • Implementiert die Schnittstelle

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.

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

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 und std::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 und std::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 verbesserter std::thread aus C++11; std::jthread ruft automatisch join in seinem Destruktor auf, falls dies nötig ist

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)