Das Proxy Pattern ist ein strukturelles Entwurfsmuster, das einen Stellvertreter für ein anderes Objekt bereitstellt. Der Proxy kontrolliert den Zugriff auf das Originalobjekt und kann zusätzliche Funktionalitäten wie Sicherheitsprüfungen, Verzögerung oder Caching implementieren. Dies ermöglicht es, die Interaktion mit dem Originalobjekt zu optimieren oder zu steuern, ohne dessen ursprüngliche Implementierung zu verändern.
Was ist das Proxy Pattern?
Das Proxy Pattern bietet eine Möglichkeit, den Zugriff auf ein Objekt zu kontrollieren, indem ein „Proxy“ als Vermittler dient. Der Proxy agiert als Platzhalter und übernimmt die Verantwortung für Aufgaben wie Authentifizierung, Caching oder Lastverlagerung. Der Proxy hält eine Referenz zum echten Objekt und leitet die Aufrufe an das Originalobjekt weiter. Er kann jedoch auch vor oder nach dem Weiterleiten zusätzliche Logik ausführen.
Ein häufiges Beispiel für den Einsatz eines Proxy Patterns ist der Fall, in dem teure Ressourcen wie Datenbankverbindungen oder Netzwerkzugriffe genutzt werden. Der Proxy kann dann die teure Operation nur dann durchführen, wenn sie wirklich erforderlich ist.
Struktur des Proxy Patterns
- Subject: Die Schnittstelle, die sowohl vom echten Objekt als auch vom Proxy implementiert wird. Sie definiert die Methoden, die sowohl der Proxy als auch das tatsächliche Objekt anbieten.
- RealSubject: Das tatsächliche Objekt, das die Hauptoperationen ausführt. Es implementiert die
Subject
-Schnittstelle und enthält die Kernlogik. - Proxy: Der Proxy implementiert ebenfalls die
Subject
-Schnittstelle und kontrolliert den Zugriff auf dasRealSubject
. Der Proxy kann zusätzliche Logik vor oder nach der Weiterleitung der Anfragen einfügen.
Beispiel des Proxy Patterns in C++
Im folgenden Beispiel simulieren wir einen Proxy für eine Ressource, die teuer in der Erstellung ist, etwa eine Bilddatei. Der Proxy lädt das Bild nur, wenn es wirklich benötigt wird.
#include <iostream>
#include <string>
// Subject: Die gemeinsame Schnittstelle, die sowohl vom Proxy als auch vom RealSubject implementiert wird
class Image {
public:
virtual void display() const = 0;
virtual ~Image() = default;
};
// RealSubject: Das echte Objekt, das die teure Operation ausführt
class RealImage : public Image {
private:
std::string filename;
public:
RealImage(const std::string& filename) : filename(filename) {
loadImageFromDisk(); // Teure Operation: Bild laden
}
void loadImageFromDisk() const {
std::cout << "Lade Bild: " << filename << std::endl;
}
void display() const override {
std::cout << "Anzeige des Bildes: " << filename << std::endl;
}
};
// Proxy: Der Proxy, der den Zugriff auf das echte Objekt kontrolliert
class ProxyImage : public Image {
private:
RealImage* realImage;
std::string filename;
public:
ProxyImage(const std::string& filename) : realImage(nullptr), filename(filename) {}
void display() const override {
if (realImage == nullptr) {
realImage = new RealImage(filename); // Nur bei Bedarf wird das echte Bild geladen
}
realImage->display();
}
~ProxyImage() {
delete realImage;
}
};
// Client-Code
int main() {
Image* image1 = new ProxyImage("bild1.jpg");
Image* image2 = new ProxyImage("bild2.jpg");
image1->display(); // Bild wird nur beim ersten Aufruf geladen
image2->display(); // Bild wird nur beim ersten Aufruf geladen
image1->display(); // Bild wird nicht erneut geladen
delete image1;
delete image2;
return 0;
}
Erklärung des C++-Beispiels
In diesem Beispiel haben wir die folgenden Komponenten:
- Subject: Die
Image
-Schnittstelle stellt die Methodedisplay()
bereit, die von beiden, dem Proxy und dem echten Objekt, implementiert wird. - RealSubject: Die Klasse
RealImage
stellt die Methodedisplay()
zur Verfügung und führt die teure Operation aus, das Bild aus der Datei zu laden. Dies passiert im Konstruktor durch den Aufruf der MethodeloadImageFromDisk()
. Diese Methode ist teuer, da sie das Bild aus dem Dateisystem liest. - Proxy: Die Klasse
ProxyImage
hält eine Referenz zuRealImage
. Sie implementiert die Methodedisplay()
, lädt das Bild jedoch nur beim ersten Aufruf. Wenn der Proxy das erste Mal die Methodedisplay()
aufruft, wird das tatsächliche Bild durch die Instanziierung vonRealImage
geladen.
Der Proxy sorgt dafür, dass das Bild nur einmal geladen wird. Bei weiteren Aufrufen der Methode display()
wird das Bild direkt angezeigt, ohne dass es erneut vom Festplattenspeicher geladen werden muss.
Beispiel des Proxy Patterns in Python
Das Proxy Pattern wird verwendet, um den Zugriff auf ein Objekt zu kontrollieren und oft zusätzliche Funktionalität hinzuzufügen, wie z. B. Caching, Authentifizierung oder Logging. Es gibt zwei Hauptarten von Proxies: den Virtuellen Proxy, der den Zugriff auf ein teures Objekt steuert, und den Schutz-Proxy, der den Zugriff auf ein Objekt basierend auf Berechtigungen kontrolliert.
Hier ist ein einfaches Beispiel des Virtuellen Proxy in Python:
Beispiel: Proxy Pattern in Python
Angenommen, wir haben eine Klasse RealSubject
, die eine teure Operation durchführt (z. B. das Laden von Daten aus einer Datenbank). Der Proxy (Proxy
) kontrolliert den Zugriff und kann dabei zusätzliche Logik (z. B. Caching) einfügen.
class RealSubject:
def request(self):
print("RealSubject: Verarbeitung der Anfrage...")
class Proxy:
def __init__(self, real_subject: RealSubject):
self._real_subject = real_subject
self._cached = False
def request(self):
if not self._cached:
print("Proxy: Vorbereitungen treffen (z.B. Caching)...")
self._real_subject.request() # Anfrage an das reale Objekt weitergeben
self._cached = True
else:
print("Proxy: Rückgabe der gecachten Antwort.")
# Anwendung des Proxy Patterns
real_subject = RealSubject()
proxy = Proxy(real_subject)
# Erstmalige Anfrage führt zur echten Verarbeitung
proxy.request()
# Zweite Anfrage nutzt die gecachte Antwort
proxy.request()
Erklärung:
RealSubject
führt die teure Operation aus, z. B. das Laden von Daten oder das Ausführen einer langwierigen Berechnung.- Der
Proxy
steuert den Zugriff und prüft, ob eine Anfrage bereits bearbeitet wurde, um unnötige Berechnungen zu vermeiden (Caching). - Beim ersten Aufruf von
proxy.request()
wird die Anfrage anRealSubject
weitergeleitet und die Operation ausgeführt. Beim zweiten Aufruf wird die gecachte Antwort zurückgegeben.
Ausgabe:
Proxy: Vorbereitungen treffen (z.B. Caching)...
RealSubject: Verarbeitung der Anfrage...
Proxy: Rückgabe der gecachten Antwort.
In diesem Fall handelt es sich um einen Virtuellen Proxy, der eine Operation verzögert, um Ressourcen zu sparen (z. B. durch Caching). Der Proxy kann jedoch auch auf komplexere Weise erweitert werden, um zusätzliche Funktionalität hinzuzufügen, wie z. B. Authentifizierung oder Berechtigungsprüfung.
Vorteile des Proxy Patterns
- Leistung: Der Proxy kann den Zugriff auf teure Ressourcen optimieren, indem er diese nur bei Bedarf lädt. Dies verringert unnötige Operationen.
- Sicherheit: Der Proxy kann Authentifizierungsprüfungen oder Zugriffssteuerungen einführen, bevor er den Zugriff auf das Originalobjekt gewährt.
- Zugriffskontrolle: Der Proxy ermöglicht die Implementierung zusätzlicher Funktionen wie Caching, Lastverlagerung oder Protokollierung, ohne das eigentliche Objekt zu ändern.
- Flexibilität: Der Proxy kann in verschiedenen Szenarien eingesetzt werden, wie etwa zur Optimierung von Systemressourcen oder zur Bereitstellung zusätzlicher Funktionalitäten wie Lazy Loading.
Nachteile des Proxy Patterns
- Erhöhte Komplexität:
Das Hinzufügen von Proxys kann den Code deutlich komplexer machen. Besonders in großen Systemen, in denen mehrere Proxy-Klassen verwendet werden, kann es zu einer Vielzahl von Abstraktionen kommen, die schwer zu durchschauen sind. Wenn das Proxy Pattern nicht sorgfältig implementiert wird, kann es zu einem komplexen Netz von Interaktionen führen, das die Lesbarkeit und Wartbarkeit des Codes beeinträchtigt. - Leistungseinbußen:
In einigen Szenarien kann der Proxy zusätzliche Verarbeitungszeit erfordern, da er als zusätzliche Abstraktionsebene zwischen dem Client und dem realen Objekt steht. Jede Anfrage muss durch den Proxy gehen, was zu einer Verzögerung führen kann, insbesondere wenn der Proxy selbst komplexe Logik ausführt, wie Caching, Authentifizierung oder Logging. Dies könnte die Gesamtleistung des Systems beeinträchtigen, besonders in zeitkritischen Anwendungen, in denen Geschwindigkeit und Effizienz eine Rolle spielen. - Wartungsaufwand:
Der Einsatz vieler Proxys im System kann den Wartungsaufwand erheblich steigern. Wenn zahlreiche Proxy-Klassen für verschiedene Objekte oder Funktionen implementiert werden, müssen Entwickler bei Änderungen an der Logik oder den Anforderungen dafür sorgen, dass alle Proxys konsistent bleiben. Das Testen und Warten eines Systems mit mehreren Proxys erfordert zusätzliches Fachwissen und kann dazu führen, dass Entwickler versehentlich Fehler in die Proxies einführen. - Versteckte Fehlerursachen:
Ein weiterer Nachteil ist, dass Fehler durch die Proxy-Schicht nicht sofort offensichtlich sind. Wenn ein Proxy fehlerhaft implementiert wird, kann es schwierig sein, die Ursache des Problems zu isolieren, da der Fehler nicht direkt im realen Objekt, sondern in der Proxy-Logik steckt. Dies kann den Debugging-Prozess erheblich erschweren und dazu führen, dass Fehler unentdeckt bleiben. - Erhöhte Anzahl von Klassen:
Da jeder Proxy eine eigene Klasse oder sogar mehrere Unterklassen benötigt, kann der Code schnell wachsen, was zu einer Vielzahl von Klassen führt, die ständig gepflegt und aktualisiert werden müssen. Dies kann nicht nur die Lesbarkeit des Codes beeinträchtigen, sondern auch die Modularität und die Möglichkeit, das System zu erweitern, erschweren. In sehr großen Systemen könnte dies dazu führen, dass die Codebasis unübersichtlich und schwer zu managen wird.
Was ist der Unterschied zwischen Proxy und Repository Pattern?
Der Unterschied zwischen dem Proxy Pattern und dem Repository Pattern liegt in ihrem jeweiligen Ziel und ihrer Funktionsweise. Beide Entwurfsmuster haben unterschiedliche Anwendungsfälle und lösen verschiedene Probleme in der Softwareentwicklung.
1. Proxy Pattern
Das Proxy Pattern dient dazu, den Zugriff auf ein Objekt zu steuern, indem es als Stellvertreter für das eigentliche Objekt fungiert. Der Proxy übernimmt die Aufgabe, das echte Objekt zu verbergen und zusätzliche Logik hinzuzufügen, wie z. B. Caching, Authentifizierung, Zugriffskontrolle oder das Verzögern der Instanziierung eines Objekts (Lazy Loading).
Wichtige Merkmale:
- Verantwortung: Steuert den Zugriff auf ein anderes Objekt und bietet zusätzliche Funktionalität (z. B. Caching, Logging).
- Ziel: Schutz des echten Objekts oder Optimierung der Ressourcen (z. B. verzögertes Laden, Berechtigungsprüfung).
- Beispiel: Ein Proxy könnte verhindern, dass auf eine teure Datenbankabfrage mehrmals zugegriffen wird, indem er die Ergebnisse zwischenzeitlich speichert (Caching).
Beispiel:
Wenn du eine Datenbankverbindung hast, könnte ein Proxy dafür sorgen, dass Daten nur dann abgerufen werden, wenn sie noch nicht im Cache vorhanden sind.
2. Repository Pattern
Das Repository Pattern dient dazu, eine zentrale Schnittstelle zu schaffen, die den Zugriff auf Daten abstrahiert. Es stellt eine Sammlung von Objekten bereit und übernimmt die Verantwortung für das Abrufen, Speichern und Löschen von Entitäten aus der Datenquelle (z. B. einer Datenbank). Ein Repository wird in der Regel verwendet, um die Geschäftslogik von der Datenzugriffsebene zu trennen.
Wichtige Merkmale:
- Verantwortung: Abstraktion der Datenspeicherung und -abfrage. Es liefert eine Sammlung von Objekten, die von der Geschäftslogik verwendet werden können.
- Ziel: Trennung der Datenzugriffsschicht von der Geschäftslogik. Ermöglicht eine einfache Testbarkeit und Flexibilität bei der Auswahl von Datenquellen.
- Beispiel: Ein Repository könnte Methoden bieten, um alle Benutzer aus einer Datenbank zu laden oder einen einzelnen Benutzer zu finden, ohne dass sich die Geschäftslogik mit SQL-Anfragen oder Details der Datenbank beschäftigen muss.
Beispiel:
Angenommen, du hast ein Repository für Benutzerdaten. Es könnte eine Methode wie get_all_users()
anbieten, um alle Benutzer aus der Datenbank zu holen, ohne dass du dich mit den Details der Datenbankinteraktion beschäftigen musst.
Vergleich
Merkmal | Proxy Pattern | Repository Pattern |
---|---|---|
Ziel | Kontrolle des Zugriffs auf ein Objekt, Optimierung oder Schutz des Objekts | Abstraktion des Datenzugriffs und Trennung von Geschäftslogik und Datenhaltung |
Verantwortung | Erweitert die Funktionalität eines Objekts, bevor auf das tatsächliche Objekt zugegriffen wird | Bietet eine Schnittstelle zum Abrufen und Speichern von Entitäten aus einer Datenquelle |
Funktionalität | Verzögertes Laden, Caching, Berechtigungsprüfung, Logging | Bereitstellung von Methoden zum Abrufen, Speichern und Löschen von Entitäten |
Beispiel | Ein Proxy für ein teures System (z. B. Datenbankabfrage) oder ein geschütztes System (z. B. Authentifizierung) | Ein Repository, das eine Sammlung von Benutzern verwaltet und Datenzugriffe abstrahiert |
- Der Proxy ist ein Zugriffsregler, der zwischen dem Client und dem echten Objekt steht und zusätzliche Funktionalitäten (wie Caching oder Schutz) hinzufügt.
- Das Repository ist eine Datenzugriffsabstraktion, die die Interaktion mit der Datenquelle vereinfacht und von der Geschäftslogik trennt.
Beide Muster sind hilfreich, aber sie lösen unterschiedliche Probleme: Der Proxy hilft beim Steuern des Zugriffs auf ein Objekt, während das Repository eine saubere und strukturierte Möglichkeit bietet, mit Daten zu arbeiten und diese zu verwalten.
Fazit
Das Proxy Pattern ist ein nützliches Designmuster, das die Interaktion mit teuren oder schwer zugänglichen Objekten optimiert. Durch die Verwendung eines Proxys können teure Operationen wie das Laden von Daten nur bei Bedarf ausgeführt werden. Dies kann die Leistung eines Systems erheblich verbessern. Der Proxy bietet auch eine flexible Möglichkeit, zusätzliche Funktionalitäten wie Caching oder Sicherheitsprüfungen einzuführen, ohne das Originalobjekt zu ändern.
In C++ lässt sich das Proxy Pattern leicht implementieren, indem eine Proxy-Klasse erstellt wird, die das echte Objekt hält und den Zugriff darauf kontrolliert. Obwohl der Proxy das System flexibler und leistungsfähiger macht, sollte er mit Bedacht eingesetzt werden, um unnötige Komplexität und Leistungseinbußen zu vermeiden.
Zurück zur Liste der Pattern: Liste der Design-Pattern