(Credits: Blog von Vishal Chovatiya)
„Das Proxy Design Pattern stellt ein Stellvertreterobjekt für ein anderes Objekt bereit, das dieselbe Schnittstelle wie das andere Objekt implementiert und ermöglicht es, den Zugriff auf dieses Objekt kontrolliert zu steuern.”
Das Proxy Design Pattern gehört zu den strukturellen Entwurfsmustern und führt ein Objekt ein, das als Platzhalter oder Vermittler für ein anderes Objekt fungiert.
Der Proxy implementiert dieselbe Schnittstelle wie das reale Objekt (das sogenannte „Real Subject”), sodass er für den Client transparent an dessen Stelle verwendet werden kann, ohne dass dieser den Unterschied bemerkt. Dadurch lässt sich der Zugriff auf das reale Objekt kontrollieren, verzögern oder um zusätzliche Funktionalität erweitern, ohne den Code des Clients oder des realen Objekts selbst zu verändern.
Typische Einsatzzwecke sind Lazy Initialization (das reale Objekt wird erst bei tatsächlichem Bedarf erzeugt, etwa bei teuren Ressourcen), Zugriffskontrolle (Protection Proxy, z. B. Prüfung von Berechtigungen vor der Weiterleitung eines Aufrufs), Remote Proxies (Stellvertreter für Objekte, die sich in einem anderen Adressraum oder auf einem anderen Rechner befinden) sowie Caching oder Logging von Aufrufen.
In C++ wird dies häufig über eine gemeinsame abstrakte Basisklasse oder ein Interface realisiert, das sowohl vom Real Subject als auch vom Proxy implementiert wird; der Proxy hält dabei intern einen Zeiger oder eine Referenz auf das reale Objekt. Der Client arbeitet ausschließlich mit der Proxy-Schnittstelle und bleibt so von der konkreten Implementierung entkoppelt.
Auf diese Weise verbindet das Pattern das Open/Closed-Prinzip mit einer sauberen Trennung von Verantwortlichkeiten: Steuerungslogik (Proxy) und eigentliche Funktionalität (Real Subject) bleiben getrennt.
Das folgende UML-Diagramm beschreibt eine Implementierung des Proxy Patterns. Es besteht im Wesentlichen aus drei Teilen:
- SubjectBase: Schnittstelle (oder abstrakte Klasse), die von der
RealSubject-Klasse implementiert wird und deren Dienste in Form von abstrakten (virtuellen) Methoden beschreibt. Die Schnittstelle muss auch von der Proxy-Klasse implementiert werden, so dass das Proxy-Objekt überall dort verwendet werden kann, wo dasRealSubject-Objekt sonst in Erscheinung treten würde. - RealSubject: Repräsentiert eine aus welchen Gründen auch schwer zugängliche oder ressourcensensitive Klasse, die man einfacher oder effektiver verwenden möchte.
- Proxy: Dreh- und Angelpunkt dieses Entwurfsmusters: Es kapselt eine Referenz (Zeiger) auf das
RealSubject-Objekt. Die Client-Anwendung ruft Methoden an derProxy-Klasse auf, die an entsprechende Methoden desRealSubject-Objekts transferiert werden.
Abbildung 1: Schematische Darstellung des Proxy Patterns.
Bemerkung:
Das Proxy Pattern hat zwei Beziehungen:
- Nach außen (Polymorphie):
Der Client soll nicht unterscheiden können, ob er mit dem echten Objekt oder dem Proxy arbeitet.
Deshalb müssen beide Klassen dieselbe Schnittstelle SubjectBase implementieren.
- Nach innen (Implementierung):
Das Proxy-Objekt delegiert seine Arbeit an das „echte” Objekt, also an ein RealSubject-Objekt.
Dies hat nichts mit Polymorphie zu tun, der Proxy weiß genau, welches Objekt er repräsentiert.
Deshalb besitzt er eine Variable m_realSubject des Typs std::unique_ptr<RealSubject>.
Ein sehr ansprechendes Beispiel aus der STL für das Proxy Design Pattern
sind die beiden Smart Pointer Klassen std::unique_ptr<> und std::shared_ptr<>:
std::unique_ptr<int> ptr{ std::make_unique<int>(123) };
*ptr = 5; oder
std::unique_ptr<MyClass> ptr{ std::make_unique<MyClass>() };
ptr->doSomething();Wenn wir die beiden Code-Snippets sehen, können wir nicht entscheiden,
ob ptr ein Raw-Zeiger oder ein Smart Pointer ist.
In anderen Programmiersprachen wie C# gibt es das sprachliche Konstrukt der Properties: Hierunter versteht man – in C# – eine private Instanzvariable inklusive getter- und setter-Methode für diese Variable.
01: template<typename T>
02: class Property
03: {
04: private:
05: T m_value;
06:
07: public:
08: Property(const T initialValue) { *this = initialValue; }
09: operator T() { return m_value; }
10: T operator= (T newValue) { return m_value = newValue; }
11: };
12:
13: struct Rectangle
14: {
15: Property<size_t> m_top{ 10 };
16: Property<size_t> m_left{ 5 };
17: Property<size_t> m_width{ 20 };
18: Property<size_t> m_height{ 30 };
19: };Der Vorteil dieser Properties – samt der vorgestellten Realisierung in C++ – liegt darin, dass man auf diese Weise den (schreibenden und/oder lesenden) Zugriff auf derartige Instanzvariable „abfangen” bzw. „mitloggen” kann.
Ein sogenanntes virtuelles Proxy-Objekt vermittelt den Anschein, als würden Sie mit einem Objekt arbeiten, so wie Sie es gewohnt sind, obwohl das Objekt möglicherweise noch nicht einmal erstellt wurde.
Studieren Sie zu diesem Zweck den Beispielcode genau:
01: struct Image {
02: virtual void draw() = 0;
03: };
04:
05: class EagerBitmap : public Image
06: {
07: private:
08: std::string m_filename;
09:
10: public:
11: EagerBitmap(const std::string& filename) : m_filename{ filename } {
12: std::cout << "loading image from " << m_filename << std::endl;
13: // steps to load the image ...
14: }
15:
16: void draw() {
17: std::cout << "drawing image " << m_filename << std::endl;
18: }
19: };
20:
21: class LazyBitmap : public Image
22: {
23: private:
24: std::unique_ptr<EagerBitmap> m_bmp{ nullptr };
25: std::string m_filename;
26:
27: public:
28: LazyBitmap(const std::string& filename) : m_filename{ filename } {}
29:
30: void draw() {
31: if (! m_bmp) {
32: m_bmp = std::make_unique<EagerBitmap>(m_filename);
33: }
34:
35: m_bmp->draw();
36: }
37: };Es gibt dabei zwei Varianten zu betrachten: Klasse EagerBitmap und Klasse LazyBitmap.
Vergleichen Sie die Ausgaben in der Konsole:
loading image from image_1.png
loading image from image_2.png
drawing image image_1.png
versus
loading image from image_1.png
drawing image image_1.png
- Das zugrunde liegende Objekt ist für den Client vollständig transparent.
- Der Proxy kann Anfragen direkt beantworten, ohne sich an den Client zu wenden.
- Der Proxy kann transparent erweitert oder durch einen anderen Proxy ersetzt werden.
- Die Trennung von Proxy-Objekt und Ziel-Objekt trägt nicht zur Vereinfachung des Quellcodes bei.
- Die weitergeleiteten Proxy-Aufrufe können leistungskritisch sein (Performanz).
Frage: Unterschied zwischen Proxy- und Decorator-Pattern?
-
Das Proxy-Pattern stellt im Wesentlichen dieselbe oder sogar eine verfeinfachte Schnittstelle dar.
-
Das Decorator-Pattern bewirkt eine Verfeinerung in der Ausgestaltung einer Schnittstelle.
Die Anregungen zum konzeptionellen Beispiel finden Sie unter
https://refactoring.guru/design-patterns
und
vor.
Die beiden Beispiele „Property Proxy” und „Virtual Proxy” stammen aus dem Blog von Vishal Chovatiya.
