Skip to Content
👆 We offer 1-on-1 classes as well check now

Proxy Pattern

The Proxy Pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. It acts as an intermediary, allowing you to add behavior before or after the request gets to the real object. This pattern is useful when you want to control access to an object, delay its creation, or add additional functionality.

What is Proxy Pattern

The Proxy Pattern involves creating a proxy class that acts as an interface to something else. The proxy could interface to anything: a network connection, a large object in memory, a file, or anything else that is expensive or impossible to duplicate. In essence, a proxy controls access to the original object, allowing you to perform actions such as:

  • Access Control: Restricting access to the real object based on certain conditions or user roles.
  • Lazy Initialization: Delaying the creation of the real object until it is actually needed, which can improve application startup time and reduce memory consumption.
  • Logging: Adding logging functionality before or after the real object’s methods are called.
  • Caching: Caching the results of the real object’s methods to improve performance.
  • Remote Proxy: Providing a local representation of an object that resides in a different address space (e.g., on a remote server).

The Proxy Pattern decouples the client from the real object, allowing you to change the implementation of the real object without affecting the client code. This promotes flexibility and maintainability.

Edge cases to consider include handling exceptions thrown by the real object, ensuring thread safety if the proxy is accessed by multiple threads, and managing the lifetime of the real object. Performance considerations are also important. While the proxy pattern can introduce optimizations like lazy loading and caching, it also adds overhead due to the additional layer of indirection. It’s crucial to analyze the performance impact in your specific use case.

Syntax and Usage

The Proxy Pattern typically involves the following components:

  • Subject: The common interface for both the real object and the proxy. It declares the methods that the client can call.
  • Real Subject: The actual object that the proxy represents. It implements the Subject interface.
  • Proxy: The proxy class that implements the Subject interface. It contains a reference to the real subject and controls access to it.

Here’s a basic example of the structure:

#include <iostream> // Subject interface class Subject { public: virtual void request() = 0; virtual ~Subject() = default; }; // Real Subject class RealSubject : public Subject { public: void request() override { std::cout << "RealSubject: Handling request.\n"; } }; // Proxy class Proxy : public Subject { private: RealSubject* realSubject; public: Proxy() : realSubject(nullptr) {} ~Proxy() { delete realSubject; } void request() override { // Lazy initialization if (realSubject == nullptr) { realSubject = new RealSubject(); } // Additional logic before the request std::cout << "Proxy: Performing security checks.\n"; realSubject->request(); // Additional logic after the request std::cout << "Proxy: Logging the request.\n"; } }; int main() { Proxy* proxy = new Proxy(); proxy->request(); delete proxy; return 0; }

Basic Example

Let’s consider a more practical example of downloading an image from a remote server. The RealImage class represents the actual image, and the ProxyImage class acts as a proxy that downloads the image only when it’s needed.

#include <iostream> #include <string> #include <chrono> #include <thread> // Interface class Image { public: virtual void display() = 0; virtual ~Image() = default; }; // Real implementation class RealImage : public Image { private: std::string filename; public: RealImage(const std::string& filename) : filename(filename) { loadFromDisk(filename); } void display() override { std::cout << "Displaying " << filename << std::endl; } private: void loadFromDisk(const std::string& filename) { std::cout << "Loading " << filename << " from disk" << std::endl; // Simulate a long loading time std::this_thread::sleep_for(std::chrono::seconds(2)); std::cout << "Finished loading " << filename << std::endl; } }; // Proxy class ProxyImage : public Image { private: RealImage* realImage; std::string filename; public: ProxyImage(const std::string& filename) : realImage(nullptr), filename(filename) {} ~ProxyImage() { delete realImage; } void display() override { if (realImage == nullptr) { realImage = new RealImage(filename); // Lazy loading } realImage->display(); } }; int main() { ProxyImage image1("image1.jpg"); // Image will be loaded only when display is called std::cout << "Image not yet loaded." << std::endl; image1.display(); std::cout << "Image already loaded." << std::endl; image1.display(); // Will not load again. return 0; }

In this example, ProxyImage avoids loading the actual image (RealImage) until the display() method is called for the first time. This is a simple form of lazy initialization. The second call to display() reuses the already loaded RealImage, improving performance.

Advanced Example

Consider a scenario where you need to control access to sensitive data. You can use the Proxy Pattern to implement an authentication mechanism.

#include <iostream> #include <string> class SensitiveData { public: virtual std::string getData() = 0; virtual ~SensitiveData() = default; }; class RealSensitiveData : public SensitiveData { public: std::string getData() override { return "Very Sensitive Data"; } }; class ProtectedSensitiveDataProxy : public SensitiveData { private: RealSensitiveData* realData; std::string userRole; public: ProtectedSensitiveDataProxy(const std::string& role) : realData(new RealSensitiveData()), userRole(role) {} ~ProtectedSensitiveDataProxy() { delete realData; } std::string getData() override { if (userRole == "admin") { return realData->getData(); } else { return "Access Denied"; } } }; int main() { ProtectedSensitiveDataProxy user("user"); ProtectedSensitiveDataProxy admin("admin"); std::cout << "User data: " << user.getData() << std::endl; std::cout << "Admin data: " << admin.getData() << std::endl; return 0; }

In this advanced example, the ProtectedSensitiveDataProxy checks the user’s role before allowing access to the RealSensitiveData. This demonstrates how the Proxy Pattern can be used to implement access control.

Common Use Cases

  • Lazy Loading: Loading large objects only when needed.
  • Access Control: Restricting access to certain resources.
  • Remote Proxy: Providing a local representation of a remote object.
  • Caching: Caching expensive operations to improve performance.

Best Practices

  • Keep the Proxy Simple: Avoid adding too much logic to the proxy. Its primary responsibility should be controlling access.
  • Use Interfaces: Define a clear interface for both the real subject and the proxy.
  • Consider Thread Safety: If the proxy is accessed by multiple threads, ensure thread safety.
  • Proper Resource Management: Handle the lifetime of the real subject carefully, especially in dynamically allocated scenarios, to avoid memory leaks. Use smart pointers if appropriate.

Common Pitfalls

  • Overuse: Don’t use the Proxy Pattern unnecessarily.
  • Complexity: The pattern can add complexity if not implemented carefully.
  • Performance Overhead: The additional layer of indirection can introduce performance overhead, especially if the proxy performs complex operations.
  • Incorrect Lifetime Management: Failing to properly manage the lifetime of the real subject can lead to memory leaks or dangling pointers.

Key Takeaways

  • The Proxy Pattern provides a surrogate or placeholder for another object.
  • It can be used to control access, delay object creation, or add additional functionality.
  • It promotes decoupling and flexibility.
  • Consider performance implications and thread safety when implementing the Proxy Pattern.
Last updated on