Table of Contents
- What is the Proxy Pattern?
- Key Participants in the Proxy Pattern
- Use Cases with Python Examples
- Step-by-Step Implementation Guide
- Pros and Cons of the Proxy Pattern
- Proxy vs. Other Patterns: Clearing Confusion
- Real-World Applications
- Conclusion
- References
What is the Proxy Pattern?
The Proxy Pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. According to the Gang of Four (GoF)—the authors of the seminal book Design Patterns: Elements of Reusable Object-Oriented Software—the intent of the Proxy Pattern is:
“Provide a surrogate or placeholder for another object to control access to it.”
In simpler terms, a proxy acts as an intermediary between a client and a “real subject” (the object being proxied). The proxy implements the same interface as the real subject, ensuring the client interacts with the proxy transparently—the client doesn’t know (or care) whether it’s working with the proxy or the real object.
Real-World Analogy
Imagine you want to buy a rare book from an overseas seller. Instead of contacting the seller directly (which might involve language barriers, currency issues, or logistics), you use a local bookstore (the proxy). The bookstore handles communication, payment, and shipping on your behalf. You interact with the bookstore exactly as you would with the seller, but the proxy simplifies the process and adds value (e.g., resolving logistics).
Key Participants in the Proxy Pattern
To implement the Proxy Pattern, we typically define three core components:
1. Subject
An interface (or abstract class) that declares the common methods for both the RealSubject and the Proxy. This ensures the proxy is interchangeable with the real subject.
2. RealSubject
The actual object that the proxy represents. It contains the core business logic or resource-intensive operations (e.g., loading a large file, querying a database).
3. Proxy
A class that implements the Subject interface and maintains a reference to the RealSubject. The proxy controls access to the RealSubject, delegating calls to it when appropriate. It may also add extra logic (e.g., lazy initialization, logging, or access checks) before or after delegating.
Use Cases with Python Examples
The Proxy Pattern shines in scenarios where you need to control access to an object. Let’s explore four common use cases with practical Python implementations.
Virtual Proxy: Lazy Initialization
Problem: Loading resource-heavy objects (e.g., large images, database connections) upfront can waste memory and slow down initialization, especially if the object is never used.
Solution: Use a virtual proxy to defer initialization of the real subject until it’s actually needed (lazy initialization).
Example: Lazy-Loaded Image
Suppose we have an HeavyImage class that loads a high-resolution image from disk—a slow operation. A ProxyImage will act as a placeholder, loading the real image only when the user requests to display it.
from abc import ABC, abstractmethod
import time # To simulate slow loading
# Subject Interface
class Image(ABC):
@abstractmethod
def display(self) -> None:
pass
# Real Subject: Heavy resource
class HeavyImage(Image):
def __init__(self, filename: str) -> None:
# Simulate slow image loading (e.g., reading from disk/network)
print(f"Loading high-res image: {filename}...")
time.sleep(2) # Simulate delay
self.filename = filename
def display(self) -> None:
print(f"Displaying image: {self.filename}")
# Proxy: Lazy initialization
class ProxyImage(Image):
def __init__(self, filename: str) -> None:
self.filename = filename
self._real_image: HeavyImage | None = None # Defer initialization
def display(self) -> None:
# Load real image only when display() is called
if self._real_image is None:
self._real_image = HeavyImage(self.filename)
self._real_image.display()
# Client Code
if __name__ == "__main__":
# Image is not loaded yet
image = ProxyImage("vacation.jpg")
print("Image created, but not loaded.")
# Image loads only when display() is called
print("\nRequesting display...")
image.display() # Loads and displays
# Subsequent calls reuse the already loaded image
print("\nRequesting display again...")
image.display() # No delay—already loaded
Output:
Image created, but not loaded.
Requesting display...
Loading high-res image: vacation.jpg...
Displaying image: vacation.jpg
Requesting display again...
Displaying image: vacation.jpg
Why It Works: The ProxyImage delays creating the HeavyImage until display() is called, saving resources if the image is never used.
Remote Proxy: Transparent Remote Access
Problem: Interacting with objects in a remote system (e.g., a web service, database) requires handling low-level details like network calls, serialization, and error handling, which complicates client code.
Solution: Use a remote proxy to act as a local representative for a remote object. The proxy handles network communication, making the remote object appear local to the client.
Example: Weather Service Proxy
Let’s simulate a remote weather service that returns temperature data. A WeatherServiceProxy will handle HTTP requests, so the client can call get_temperature() without worrying about network logic.
from abc import ABC, abstractmethod
import requests # For actual HTTP requests (install with `pip install requests`)
# Subject Interface
class WeatherService(ABC):
@abstractmethod
def get_temperature(self, city: str) -> float:
pass
# Real Subject: Remote service (simulated with a public API)
class RemoteWeatherService(WeatherService):
def get_temperature(self, city: str) -> float:
# Call a real weather API (e.g., OpenWeatherMap; replace with your API key)
url = f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid=YOUR_API_KEY&units=metric"
response = requests.get(url)
data = response.json()
return data["main"]["temp"]
# Proxy: Handles remote communication details
class WeatherServiceProxy(WeatherService):
def __init__(self) -> None:
self._remote_service = RemoteWeatherService() # Reference to remote subject
def get_temperature(self, city: str) -> float:
try:
print(f"Proxy: Fetching temperature for {city}...")
return self._remote_service.get_temperature(city)
except requests.exceptions.RequestException as e:
print(f"Proxy: Error accessing remote service: {e}")
return -999.9 # Fallback value
# Client Code
if __name__ == "__main__":
weather_proxy = WeatherServiceProxy()
temp = weather_proxy.get_temperature("London")
print(f"Temperature in London: {temp}°C")
Note: Replace YOUR_API_KEY with a valid OpenWeatherMap API key (free tier available). The proxy handles network errors, providing a fallback, so the client remains unaware of remote details.
Protection Proxy: Access Control
Problem: You need to restrict access to sensitive methods or data (e.g., admin-only operations, paid features).
Solution: Use a protection proxy to enforce access control rules before delegating to the real subject.
Example: Admin-Only Document Editing
A Document class has an edit() method that should only be accessible to admins. A ProtectedDocumentProxy will check the user’s role before allowing edits.
from abc import ABC, abstractmethod
from typing import Literal
# Subject Interface
class Document(ABC):
@abstractmethod
def view(self) -> None:
pass
@abstractmethod
def edit(self) -> None:
pass
# Real Subject: Sensitive document
class RealDocument(Document):
def __init__(self, title: str) -> None:
self.title = title
def view(self) -> None:
print(f"Viewing document: {self.title}")
def edit(self) -> None:
print(f"Editing document: {self.title}")
# Proxy: Enforces access control
class ProtectedDocumentProxy(Document):
def __init__(self, document: RealDocument, user_role: Literal["user", "admin"]) -> None:
self._document = document # Real subject
self._user_role = user_role # User's role for access checks
def view(self) -> None:
# Anyone can view
self._document.view()
def edit(self) -> None:
# Only admins can edit
if self._user_role == "admin":
self._document.edit()
else:
print("Access denied: Only admins can edit documents.")
# Client Code
if __name__ == "__main__":
# Create a real document
report = RealDocument("Q3 Financial Report")
# User tries to access (non-admin)
user_proxy = ProtectedDocumentProxy(report, user_role="user")
user_proxy.view() # Allowed
user_proxy.edit() # Denied
# Admin tries to access
admin_proxy = ProtectedDocumentProxy(report, user_role="admin")
admin_proxy.edit() # Allowed
Output:
Viewing document: Q3 Financial Report
Access denied: Only admins can edit documents.
Editing document: Q3 Financial Report
Logging Proxy: Monitoring Interactions
Problem: You want to log method calls, arguments, or return values for debugging or auditing, without modifying the real subject (open/closed principle).
Solution: Use a logging proxy to wrap the real subject and inject logging logic around method calls.
Example: Logging Calculator
A Calculator class performs arithmetic operations. A LoggingCalculatorProxy will log each method call, input arguments, and results.
from abc import ABC, abstractmethod
import logging
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Subject Interface
class Calculator(ABC):
@abstractmethod
def add(self, a: float, b: float) -> float:
pass
# Real Subject: Core logic
class BasicCalculator(Calculator):
def add(self, a: float, b: float) -> float:
return a + b
# Proxy: Adds logging
class LoggingCalculatorProxy(Calculator):
def __init__(self, calculator: Calculator) -> None:
self._calculator = calculator # Real subject
def add(self, a: float, b: float) -> float:
# Log before method call
logger.info(f"Calling add(a={a}, b={b})")
# Delegate to real subject
result = self._calculator.add(a, b)
# Log after method call
logger.info(f"add() returned {result}")
return result
# Client Code
if __name__ == "__main__":
calc = BasicCalculator()
logging_calc = LoggingCalculatorProxy(calc)
sum_result = logging_calc.add(5, 3)
print(f"Result: {sum_result}")
Output:
INFO:__main__:Calling add(a=5, b=3)
INFO:__main__:add() returned 8
Result: 8
The proxy adds logging without modifying BasicCalculator, adhering to the open/closed principle.
Implementation Steps
To implement the Proxy Pattern in Python, follow these steps:
- Define the Subject Interface: Use an abstract base class (ABC) with abstract methods to enforce a common interface for
RealSubjectandProxy. - Implement the Real Subject: Create a class that implements the
Subjectinterface and contains the core logic. - Create the Proxy Class: Implement the
Subjectinterface, hold a reference to theRealSubject, and add control logic (lazy init, logging, etc.) before/after delegating. - Client Interaction: The client uses the proxy exactly as it would the real subject, thanks to the common interface.
Pros and Cons of the Proxy Pattern
Pros
- Separation of Concerns: Proxies isolate control logic (e.g., logging, access checks) from the real subject’s core logic, improving maintainability.
- Lazy Initialization: Virtual proxies reduce memory usage by deferring heavy object creation.
- Security: Protection proxies enforce access rules, preventing unauthorized use.
- Transparency: Clients interact with proxies as if they were real subjects, simplifying code.
Cons
- Increased Complexity: Adding a proxy introduces an extra layer, which can make debugging harder.
- Potential Overhead: Proxies may add latency (e.g., network calls in remote proxies) or memory usage.
- Overuse Risk: Applying proxies unnecessarily can complicate the codebase without benefit.
Proxy vs. Other Patterns: Clearing Confusion
Proxies are often confused with decorators or adapters. Here’s how they differ:
| Pattern | Purpose | Key Difference |
|---|---|---|
| Proxy | Control access to an object | Implements the same interface as the real subject; may not always delegate (e.g., lazy init). |
| Decorator | Add/modify behavior dynamically | Always delegates to the wrapped object; focuses on extending functionality, not control. |
| Adapter | Convert one interface to another | Changes the interface of the wrapped object; proxy preserves it. |
Real-World Applications
- ORM Lazy Loading: Libraries like SQLAlchemy use virtual proxies to load related database records only when accessed (e.g.,
relationship(lazy="select")). - Caching Proxies: Tools like
requests-cachewraprequests.Sessionto cache HTTP responses, acting as a caching proxy. - AOP (Aspect-Oriented Programming): Frameworks like Spring (Java) or
aspectlib(Python) use proxies to inject cross-cutting concerns (logging, transactions) without modifying core code. - Remote Services: gRPC and REST clients use remote proxies to handle network communication, making remote services appear local.
Conclusion
The Proxy Pattern is a powerful tool for controlling access to objects, enhancing flexibility, and improving resource management. By acting as a surrogate, proxies enable lazy initialization, access control, logging, and remote transparency—all while keeping clients decoupled from implementation details.
When using proxies, prioritize thoughtful design: apply them only when they solve a specific problem (e.g., reducing memory usage, securing sensitive operations). Avoid overengineering—sometimes a simple direct call is better than adding a proxy layer.
With the examples and principles covered here, you’re ready to leverage the Proxy Pattern to build more efficient, secure, and maintainable Python applications.
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Python Software Foundation. (n.d.). Abstract Base Classes (ABCs). https://docs.python.org/3/library/abc.html
- OpenWeatherMap API. (n.d.). https://openweathermap.org/api
- requests-cache. (n.d.). https://requests-cache.readthedocs.io/
- SQLAlchemy. (n.d.). Relationship Configuration. https://docs.sqlalchemy.org/en/20/orm/relationship_api.html