TUTORIAL SERIES

Design Patterns in Python: Proxy

The Powerful Surrogate

Amir Lavasani
9 min readOct 16, 2023

Have you encountered recurring coding challenges? Imagine having a toolbox of tried-and-true solutions readily available. That’s precisely what design patterns provide. In this series, we’ll explore what these patterns are and how they can elevate your coding skills.

Understanding the Proxy Pattern

Imagine you’re working on a complex application where you need authorization to access certain objects, services are scattered across different servers, or perhaps, the creation of an object is resource-intensive. This is where the Proxy Design Pattern comes to your rescue.

What is the Proxy Design Pattern?

The Proxy Design Pattern is a structural pattern that provides an interface to another object, acting as an intermediary or surrogate. It is used when we want to control access to an object, add some functionality before or after accessing it, or delay the creation of an object until it is actually needed. Think of it as a virtual representation of the real object, allowing you to manage its lifecycle and interactions.

When to Use the Proxy Pattern

Determining the appropriate scenarios for employing the Proxy pattern is crucial. Here’s a discussion on when to leverage the Proxy pattern:

  1. Expensive Object Loading: When dealing with complex or resource-intensive objects, consider the Virtual Proxy. It acts as a placeholder, loading the full object on demand, and optimizing resource usage.
  2. Remote Object Access: If the original object is in a different address space and you need local-like interaction, opt for the Remote Proxy. It manages connection details, making remote objects appear local.
  3. Enhanced Security: Employ the Proxy pattern for added security. The Protection Proxy enforces controlled access based on client rights, safeguarding sensitive resources.
Circular proxy tunnel accessing a faraway object
Dall-E generated image with the following concept: Circular proxy tunnel accessing a faraway object

The Anatomy of a Proxy

To fully grasp the Proxy pattern, let’s dissect its structure and understand its key components:

  1. Proxy Interface: The Proxy Interface defines the methods that both the Real Subject and the Proxy must implement. This ensures that the Proxy is a true substitute for the Real Subject.
  2. Real Subject: The Real Subject is the actual object that the Proxy represents. It implements the Proxy Interface and performs the real business logic.
  3. Proxy: The Proxy is the intermediary between the client and the Real Subject. It also implements the Proxy Interface, but it doesn’t perform the core logic itself. Instead, it delegates the calls to the Real Subject, potentially adding additional functionality before or after the delegation.
Image from refactoring.guru

Here, Service Class is the Real Subject and the Client Code interacts with the Service through Proxy object.

Usually, proxies manage the full lifecycle of their service objects.

Types of Proxies

The Proxy pattern’s versatility is underscored by its ability to adapt to diverse use cases, leading to various types of proxies. Let’s embark on an expedition to acquaint ourselves with these types:

  1. Virtual Proxy: A Virtual Proxy is all about deferring a resource-intensive object loading until explicitly required. It creates a placeholder for an expensive or resource-intensive object. This object is only instantiated when a client requests it. Until then, the proxy manages the access.
  2. Remote Proxy: Relays communication with objects residing in different address spaces or on remote servers. The proxy handles communication details, such as network connections, serialization, and deserialization, while the client interacts with the proxy as if it were the real object.
  3. Protection Proxy: Takes on the role of a vigilant gatekeeper, applying a robust authentication and authorization layer in front of the real object. This additional layer of security ensures that access to the real object is granted only to authorized entities, safeguarding sensitive resources.
  4. Smart Reference: When you need to dismiss a heavyweight object once it’s no longer in use, the Smart Reference shines. It tracks active clients and periodically checks their status. If no clients are using the object, the proxy releases it, freeing system resources. It also monitors modifications to the object, allowing for reuse when unchanged.

Implementing the Proxy Pattern in Python

To delve into Python’s Proxy implementation, we’ll illustrate it with two real-world examples. The first example showcases a Caching Proxy for database queries, employing lazy initialization.

The second demonstrates a Monitoring Proxy designed to track the runtime of a time-consuming operation performed by the real subject.

Caching Proxy for Database Queries

In this implementation, the CacheProxy serves as an intermediary between the client and the RealDatabaseQuery. It checks whether the result of a query is already cached and, if so, returns the cached result within the specified cache duration.

If the result is not in the cache or has expired, it delegates the query execution to the RealDatabaseQuery, caches the result, and returns it to the client. This way, the CacheProxy reduces the load on the database server and speeds up data retrieval by serving cached results when possible.

import time

# Define the interface for the Real Subject
class DatabaseQuery:
def execute_query(self, query):
pass

# Real Subject: Represents the actual database
class RealDatabaseQuery(DatabaseQuery):
def execute_query(self, query):
print(f"Executing query: {query}")
# Simulate a database query and return the results
return f"Results for query: {query}"

# Proxy: Caching Proxy for Database Queries
class CacheProxy(DatabaseQuery):
def __init__(self, real_database_query, cache_duration_seconds):
self._real_database_query = real_database_query
self._cache = {}
self._cache_duration = cache_duration_seconds

def execute_query(self, query):
if query in self._cache and time.time() - self._cache[query]["timestamp"] <= self._cache_duration:
# Return cached result if it's still valid
print(f"CacheProxy: Returning cached result for query: {query}")
return self._cache[query]["result"]
else:
# Execute the query and cache the result
result = self._real_database_query.execute_query(query)
self._cache[query] = {"result": result, "timestamp": time.time()}
return result

# Client code
if __name__ == "__main__":
# Create the Real Subject
real_database_query = RealDatabaseQuery()

# Create the Cache Proxy with a cache duration of 5 seconds
cache_proxy = CacheProxy(real_database_query, cache_duration_seconds=5)

# Perform database queries, some of which will be cached
print(cache_proxy.execute_query("SELECT * FROM table1"))
print(cache_proxy.execute_query("SELECT * FROM table2"))
time.sleep(3) # Sleep for 3 seconds

# Should return cached result
print(cache_proxy.execute_query("SELECT * FROM table1"))

print(cache_proxy.execute_query("SELECT * FROM table3"))

Monitoring Proxy for Performance Metrics

To gather performance metrics or statistics about method calls and object usage, you can use a Monitoring Proxy. It logs relevant data without affecting the primary functionality.

This code demonstrates the implementation of the Proxy pattern, specifically the Monitoring Proxy, to track performance metrics of a costly operation represented by the Real Subject. The Monitoring Proxy is designed to lazily instantiate the Real Subject, ensuring that system resources are conserved until the operation is actually performed.

from abc import ABC, abstractmethod
from datetime import datetime

# Define an interface for the Real Subject
class Subject(ABC):
@abstractmethod
def perform_operation(self):
pass

# Define the Real Subject that we want to monitor
class RealSubject(Subject):
def perform_operation(self):
"""
The Real Subject's method representing a costly operation.
"""
print("RealSubject: Performing a costly operation...")

# Define the Monitoring Proxy that will lazily instantiate the Real Subject
class MonitoringProxy(Subject):
def __init__(self):
self._real_subject = None

def perform_operation(self):
"""
The Proxy's method, which monitors and adds performance metrics.
Lazily instantiates the Real Subject on the first call.
"""
if self._real_subject is None:
print("MonitoringProxy: Lazy loading the RealSubject...")
self._real_subject = RealSubject()

start_time = datetime.now()
print(
f"MonitoringProxy: Recording operation start time - {start_time}"
)

# Delegate the operation to the Real Subject
self._real_subject.perform_operation()

end_time = datetime.now()
execution_time = end_time - start_time
print(f"MonitoringProxy: Recording operation end time - {end_time}")
print(
f"MonitoringProxy: Operation executed in {execution_time} seconds"
)

# Client code
if __name__ == "__main__":
# Create the Monitoring Proxy (lazy loading of Real Subject)
monitoring_proxy = MonitoringProxy()

# Client interacts with the Proxy
monitoring_proxy.perform_operation()

When perform_operation is called, the Monitoring Proxy records the start and end times of the operation and calculates its execution duration, providing valuable performance metrics without altering the Real Subject's behavior.

GitHub Repo 🎉

Explore all code examples and design pattern implementations on GitHub!

A valuable object being protected under high security
Dall-E generated image with the following concept: A valuable object being protected under high security

Real-world Use Cases: Proxy in Action

The Proxy Design Pattern is practical and applicable in various real-world scenarios. Here are programming examples showcasing its effectiveness:

  1. Virtual Proxy for Image Loading: In photo editing apps, a Virtual Proxy represents high-resolution images, loading them on demand, optimizing memory, and enhancing startup times.
  2. Remote Proxy for Web Services: When dealing with remote web services or APIs, a Remote Proxy simplifies communication, handling authentication, encryption, and network connectivity details.
  3. Caching Proxy for Database Queries: Database apps benefit from a Cache Proxy storing frequently accessed query results, reducing server load and speeding up data retrieval.
  4. Lazy Loading for Expensive Objects: In large datasets or complex objects, use a Proxy to delay object creation until necessary, common in CAD software.
  5. Access Control with Security Proxy: Ensure secure data access by employing a Security Proxy, enforcing permissions and authentication checks.
  6. Logging Proxy for Debugging: Debugging is streamlined with a Logging Proxy recording method calls and parameters, aiding issue diagnosis without code modifications.
  7. Counting Proxy for Resource Management: In resource-critical systems like concurrent programming, a Counting Proxy tracks object references, releasing them when no longer required.
  8. Monitoring Proxy for Performance Metrics: Gather performance data and usage statistics with a Monitoring Proxy, enhancing insights without impacting primary functionality.
  9. Load Balancing with Proxy Servers: Proxy servers in web apps distribute network requests across backend servers, ensuring availability and load balancing.
  10. Transactional Proxy for Database Operations: Database apps benefit from a Transactional Proxy handling transactions, ensuring data integrity through rollback and commit mechanisms.

These examples highlight the Proxy Design Pattern’s versatility, improving resource management, security, and performance across diverse software domains, ultimately enhancing code robustness and maintainability.

Advantages of the Adapter Pattern

The Proxy pattern offers several advantages:

  1. Open/Closed Principle: Introduce new proxies without altering client code, adhering to the Open/Closed Principle.
  2. Smooth Service: Proxies operate even when the service object is unavailable or unready, ensuring uninterrupted service.
  3. Security: Proxy methods enhance system security by facilitating access control mechanisms.
  4. Performance: Proxies improve application performance by avoiding the duplication of memory-intensive objects.

Considerations and Potential Drawbacks

While proxies are valuable, they come with some downsides:

  1. Complexity: Introducing proxies can add complexity to the codebase, especially when dealing with multiple proxy types.
  2. Performance Overhead: Depending on the proxy’s functionality, there can be a performance overhead, such as network communication or caching synchronization.
  3. Maintenance: Managing proxies and their interactions with the real objects can require extra effort in terms of maintenance and testing.

Adapter provides a different interface to its subject. Proxy provides the same interface. Decorator provides an enhanced interface.

Dall-E generated image with the following concept: A protective shield enveloping an object, emphasizing the Proxy’s capacity to safeguard and control access

Relations with Other Patterns — TL;DR;

The Proxy pattern shares similarities and contrasts with several other design patterns, namely the Adapter pattern, Decorator pattern, and Facade pattern. Let’s explore these comparisons and distinctions:

Proxy vs. Decorator Pattern

Decorator and Proxy Patterns both wrap objects, yet their intentions and usage vary.

  • Decorator: Dynamically augments an object’s behavior, often with client awareness and intent.
  • Proxy: Focuses on controlling access to an object or enhancing its functionality, often transparently to the client.

Proxy vs. Facade Pattern

Facade and Proxy Patterns both simplify interactions with complex systems, but they differ in scope and intent.

  • Facade: Provides a unified, simplified interface to a complex system or subsystem.
  • Proxy: Primarily manages access to individual objects, adding control or functionality without altering the system’s structure and interface.

In summary, while these patterns share a common goal of improving object interactions, they do so through distinct strategies tailored to specific use cases and requirements. The choice among them depends on the specific design goals and complexities of the system at hand.

Conclusion

The Proxy Design Pattern is a powerful tool in a software engineer’s arsenal, enabling you to control access to objects, add functionality, and optimize resource usage. Whether you’re dealing with lazy loading, remote access, caching, or other scenarios, proxies can make your code more efficient and maintainable. Understanding when and how to use proxies, along with their relationships to other design patterns, empowers you to design robust and scalable software systems.

In this article, we’ve covered the basics of the Proxy pattern, its types, structure, use cases, advantages, and disadvantages. We’ve also explored its connections with the Adapter and Decorator patterns. Armed with this knowledge, you can apply the Proxy pattern effectively in your Python projects, improving both performance and maintainability.

Hope you enjoyed the Proxy pattern exploration 🙌 Happy coding! 👨‍💻

Next on the Series 🚀

Read More 📜

The Series 🧭

References

  1. Design Patterns: Elements of Reusable Object-Oriented Software (Book)
  2. refactoring.guru Proxy Pattern
  3. Head First Design Patterns (Book)
  4. Proxy Method — Python Design Patterns
  5. Proxy Design Pattern
  6. The Proxy Pattern in Java

--

--

Amir Lavasani

I delve into machine learning 🤖 and software architecture 🏰 to enhance my expertise while sharing insights with Medium readers. 📃