The Proxy Design Pattern: Mastering Controlled Access to Objects

Tobias Lang
9 min readMay 27, 2023

Software development practices entail a myriad of challenges, one of which is the efficient management of resources. As developers, we often find ourselves in a position where we need to control access to an object, perhaps due to the cost of creating and maintaining the object, or because of access restrictions. In this scenario, the Proxy Design Pattern comes to the rescue. Let’s delve into the world of the Proxy Design Pattern, learn how to use it in Python, discover its pros and cons, and look at its usage in the Python standard library.

The article is part of my series on Software Design Patterns. For a more general introduction to design patterns, refer to this overview article, which is an excellent starting point for exploring the fascinating world of software design patterns.

As usual, the sample code shown in the article can be found on my Github repository.

Billard table
Photo by Pavel Danilyuk: https://www.pexels.com/de-de/foto/hand-spielen-bunt-finger-7403831/

Understanding the Proxy Design Pattern

In essence, the Proxy Design Pattern involves creating a new proxy class as an interface for the actual class. This pattern is a type of structural design pattern as it’s concerned with how classes and objects can be composed to form larger structures. The Proxy Design Pattern introduces a layer of protection to the actual object from the outside world. This is particularly useful when the original object is vulnerable, or complex and heavy.

Proxy Design Pattern UML
Traced by User:Stannered, created by en:User:TravisHein, CC BY-SA 3.0

As we can see, the pattern is not that complicated:

  • Subject is an interface that declares common operations for RealSubject and Proxy.
  • RealSubject is a class that defines the real object that the proxy represents.
  • Proxy is a class that maintains a reference to a RealSubject, and can control access to it. The proxy often does some additional operations before and after forwarding the request to the RealSubjectsuch as access checks and logging.
  • The Client accesses the RealSubject via the Proxy by means of the defined interface Subject.

Now, let’s translate the UML diagram for the Proxy Pattern over into actual Python code:

import logging
from abc import ABC, abstractmethod


class Subject(ABC):
"""
The Subject interface declares common operations for both
RealSubject and the Proxy.
"""

@abstractmethod
def do_action(self) -> str:
"""
The Subject interface declares a common method for both
RealSubject and the Proxy.
"""


class RealSubject(Subject):
"""
The RealSubject contains some core business logic.
"""

def do_action(self) -> str:
"""
Work done by the RealSubject.
"""
return "RealSubject: Handling request."


class Proxy(Subject):
"""
The Proxy has an interface identical to the RealSubject.
"""

def __init__(self, real_object: RealSubject) -> None:
"""
The Proxy maintains a reference to an object of the RealSubject class.
"""
self._real_object = real_object

def do_action(self) -> str:
"""
Work done via the Proxy.
"""
if self.check_access():
self._real_object.do_action()
self.log_access()

return "Proxy: Handling request."

def check_access(self) -> bool:
"""
Helper function to check access rights before firing a real request.
"""
logging.info("Proxy: Checking access prior to firing a real request.")
return True

def log_access(self) -> None:
"""
Helper function to log access to the real subject.
"""
logging.info(
"Proxy: Logged the time of request.",
)

def client_code(subject: Subject) -> str:
"""
The client code is supposed to work with all objects (both subjects and
proxies) via the Subject interface in order to support both real subjects
and proxies. In real life, however, clients mostly work with their real
subjects directly. In this case, to implement the pattern more easily, you
can extend your proxy from the real subject's class.
"""
return subject.do_action()

# Executing the client code with a real subject.
real_subject = RealObject()
client_code(real_subject) # --> RealSubject: Handling request.

# Executing the same client code with a proxy.
proxy = Proxy(real_subject)
client_code(proxy) # --> Proxy: Handling request.

In this example, the Client interacts with the Subject interface, which is common to both RealObject and Proxy. The RealSubject contains the actual business logic, and the Proxy manages access to the RealSubject, performing additional tasks such as checking access rights before and logging after forwarding requests to the RealSubject.

Advantages and Drawbacks

Let’s delve into the advantages and potential pitfalls associated with the application of the Proxy pattern.

Advantages

  • Controlled Access: The Proxy Design Pattern is primarily lauded for the controlled access it offers. By interposing a proxy or surrogate for an actual object, the pattern permits requests to be processed or relayed as required. For instance, in the case of a large and resource-heavy object, the proxy can delay the instantiation until necessary, thus saving system resources. In some cases, it can also limit the number of instances created for a particular object, which can prove useful in the context of network connections or database access.
  • Reduced Complexity: Another significant advantage of the Proxy Design Pattern is the reduction in system complexity it brings about. It achieves this by abstracting the underlying complexities of the real object. This means that the client can interact with the proxy without needing to understand the intricacies of how the actual object works, its lifecycle, or how it’s implemented. This makes the system as a whole more user-friendly and manageable, as the client needs only to interact with the straightforward interface provided by the proxy.
  • Security Enhancement: The Proxy Design Pattern can add a layer of security to the real object. By interposing itself between the client and the actual object, the proxy can control and monitor the access to the real object. For instance, it can check if the client has the necessary permissions or credentials before forwarding the request to the real object. This is particularly useful in situations where the real object contains sensitive data or functionality that should only be accessible under specific conditions or by certain clients.

Drawbacks

  • Code Complexity: One of the main drawbacks of the Proxy Design Pattern is that it can lead to increased code complexity. This is primarily because the pattern necessitates the introduction of additional classes, namely the Proxy class itself. Moreover, maintaining coherence between the interface of the Proxy class and the Real object class can add to the overhead, especially when the system evolves and the interface needs to change. Furthermore, the use of proxies may not be immediately obvious to other developers, making the code harder to understand and maintain.
  • Response Time: Another potential drawback is the impact on response time. Since the proxy sits between the client and the actual object, all requests and responses must pass through the proxy. This added layer can cause a slight delay in the response time. Although this latency might be negligible in many scenarios, it could become significant if the proxy performs complex checks or the interactions with the real object are particularly time-sensitive. This is especially true in high-performance systems where even minor delays can be a major drawback. Consequently, the Proxy Design Pattern should be employed judiciously, taking into account the system’s performance requirements and the potential latency introduced by the proxy.

Combining Proxy with Other Patterns

Design Patterns can be combined effectively with other design patterns for more optimized solutions. We might not have explored all the patterns, but I will start to incorporate this section — just skip over the unknown patterns for now and come back later.

  • The Adapter Design Pattern, for instance, can be used with the Proxy Pattern when the proxy class needs to change the interface of the real object without altering its behavior.
    The Adapter Pattern is another valuable structural design pattern that allows objects with incompatible interfaces to collaborate. This pattern is often used when you want to make your existing class work with others without modifying their source code. The Adapter acts as a wrapper between two objects. It catches calls for one object and transforms them into calls for another, adapting the interface of one class (the adaptee) to be used from another interface, the one that the client expects to work with.
import logging

from patterns.proxy.proxy import Subject, RealSubject

class Adapter:
"""
Adapter to change RealSubject's interface.

We are using the Proxy pattern implementation from above.
"""

def __init__(self, proxy: Subject) -> None:
"""
Initialize Adapter with proxy.

Args:
proxy: Proxy interface to change.

Returns:
None
"""
self._proxy = proxy

def request(self) -> int:
"""
Change the interface of RealSubject via Proxy.

Returns:
Integer
"""
logging.info("Adapter: Changing the interface of RealSubject via Proxy.")
self._proxy.do_action()
return 1

def client_code(adapter: Adapter) -> str:
"""
Mock client code that works with all objects implementing the Adapter.
"""
return adapter.request()

# Executing client code with Adapter and Proxy.
real_subject = RealSubject()
proxy = Proxy(real_subject)
adapter = Adapter(proxy)

client_code(adapter) # --> 1
from patterns.proxy.proxy import Subject, RealSubject

class Decorator:
"""
Decorator to add new responsibilities to the proxy object without changing its interface.

We are using the Proxy pattern implementation from above.
"""

def __init__(self, proxy: Subject) -> None:
"""
Initialize Decorator with proxy.
"""
self._proxy = proxy

def do_action(self) -> str:
"""
Add new responsibilities to the proxy object without changing its interface.
"""
print("Decorator: Before request.")
result = self._proxy.do_action()
print("Decorator: After request.")

return result

def client_code(decorator: Decorator) -> str:
"""
Mock client code that works with all objects implementing the Decorator.
"""
return decorator.do_action()

# Executing client code with Decorator and Proxy.
real_subject = RealSubject()
proxy = Proxy(real_subject)
decorator = Decorator(proxy)
result = client_code(decorator) # --> Proxy: Handling request.
  • The Facade Pattern can also be combined with the Proxy pattern when you want to provide a simplified interface to a complex subsystem.
    The Facade Pattern is a structural design pattern that provides a simplified interface to a complex subsystem. This pattern involves a single class that represents an entire subsystem, hiding its complexity from the client. The goal of the Facade Pattern is to improve the readability and usability of a software library, framework, or any other complex set of classes by providing a simpler, high-level interface. It does not alter the subsystem interface like an Adapter does; instead, it abstracts the subsystem’s complexity, providing a user-friendly layer for clients.
import logging

from patterns.proxy.proxy import Subject, RealSubject


class Facade:
"""
Facade to simplify access to the subsystem.

We are using the Proxy pattern implementation from above.
"""

def __init__(self, proxy: Subject) -> None:
"""
Initialize Facade with proxy.
"""
self._proxy = proxy

def operation(self) -> bool:
"""
Simplify access to the subsystem.
"""
logging.info("Accessing the subsystem..")

result = self._proxy.do_action()
if result:
return True
return False

def client_code(facade: Facade) -> bool:
"""
Mock client code that works with all objects implementing the Facade pattern.
"""
return facade.operation()

# Executing client code with Facade and Proxy.
real_subject = RealSubject()
proxy = Proxy(real_subject)
facade = Facade(proxy)
result = client_code(facade) # --> True

Real-World Examples in the Python Standard Library

An excellent real-world example of the Proxy Design Pattern in the Python Standard Library is the weakref module. A weak reference is a pointer to an object that doesn't prevent the object from being garbage collected. Hence, it's a kind of proxy that references the original object without preventing it from dying.

import weakref

class HeavyObject:
def __init__(self, name):
self.name = name
heavy = HeavyObject('My Heavy Object')
weak_proxy = weakref.proxy(heavy)
print(weak_proxy.name)

In this case, the weakref.proxy acts as a proxy to the HeavyObject without preserving its lifetime. This allows for memory optimization, especially when dealing with large objects.

Another example is the lru_cache decorator from the functools module in the Python standard library. It provides a caching mechanism for function results, reducing the need to recompute them. The lru_cache decorator acts as a proxy by intercepting function calls, caching the results, and returning the cached result for subsequent calls with the same arguments. It simplifies access to the underlying expensive computation and enhances performance.

And last but not least, the multiprocessing module in Python includes the Pool class, which provides a high-level interface for distributing tasks across multiple processes. The Pool class acts as a proxy, managing access to a pool of worker processes and delegating tasks to them. The proxy hides the complexity of process creation, management, and communication, allowing the client code to focus on submitting and retrieving results from the worker processes.

Conclusion

The Proxy Design Pattern serves as a powerful tool in a developer’s toolkit, providing an efficient mechanism for controlled and simplified access to objects. It elegantly separates the responsibilities of object usage and object management, enabling higher code maintainability, security, and effective resource management.

Though the implementation of this pattern can lead to increased complexity and potential latency, understanding your application’s needs and resources will guide whether the Proxy Pattern is a good fit. A well-implemented Proxy Pattern can significantly enhance the structure and reliability of large systems.

The art of software design involves understanding the problem at hand and applying the most suitable pattern or mix of patterns to solve it. While Proxy is just one among many design patterns, its relevance and value in particular situations are unquestionable. Happy coding!

--

--