Everyone talks about design patterns. But what are these elusive entities?
Today, we’ll be learning more about design patterns, these blueprints, these design best practices that solve common problems in software engineering. If you’ve ever wrestled with a convoluted piece of software, desperately wishing for a little bit of structure, then you’ve probably unknowingly cried out to the design pattern deities in despair.
We’ll delve specifically into the Proxy pattern: a structural design pattern that operates as a surrogate a sort of placeholder, even in some instances a bodyguard that stands between the object we are proxying and the business logic.
Don’t worry if this sounds confusing. We’ll peel back the layers of this concept in the sections to come.
Understanding the Proxy Pattern
Picture yourself as a high-profile celebrity, incessantly bombarded by all sorts of characters – some savory, others not so much. There’s no way you can personally sift through this crowd to determine who gets access to you.
So, what’s your solution?
You hire a personal assistant, a surrogate who regulates access to you. This stand-in could be a bodyguard, a secretary, an investment agent, or a mix of all three.
In the realm of programming, the Proxy Pattern is our stand-in. It stands in as a placeholder for another object, controlling access to it. But not only does it act as a substitute for the real object, it also permits us to perform certain actions before or after the real object is referenced.
Think of the Proxy Pattern as our metaphorical bodyguard-cum-personal-assistant. It’s a handy tool for managing complex or heavyweight objects that involve heavy-duty business logic or network-intensive resources.
Instead of loading hefty objects or making multiple network calls every time, we can use the proxy to streamline these operations, executing them only when necessary.
The Proxy Pattern is a jack-of-all-trades, capable of lazy loading, caching, access control, blacklisting, logging, and much more. It’s a versatile design pattern and a darling among developers, particularly when dealing with resource-intensive objects.
Still with me? If not, don’t sweat it. You haven’t seen or written a single line of code yet. `
In fact, feel free to skip right ahead to that section if you wish.
But if you’re up for diving deeper into the technical trenches, there’s still more theory to explore – let’s delve into the various types of proxies.
Types of Proxy Patterns
At this point, we’re familiar with the concept of the proxy pattern and have an inkling of how it operates. Let’s dig a little deeper into the different types of Proxy Patterns. Each type has unique characteristics that makes it particularly effective at solving specific problems. We’ll cover the the Virtual Proxy, Protection Proxy, Remote Proxy, Logging Proxy, and Smart Reference Proxy types.
Virtual Proxy
The virtual proxy is an efficiency-enhancing pattern. Its main function is to control access to resource-heavy objects, ensuring they’re initialized only when absolutely necessary. The resource-intensive object could be stored in a cache or serialized and saved to disc.
Or perhaps we’re only interested in a partial version of an object obtained from an internal API.
Consider a webpage with several high-resolution images. Re-downloading all these images with each visit would bog down the server. A virtual proxy could be set up to download them only when needed.
Protection Proxy
Sometimes referred to as a guardian proxy, the Protection Proxy controls access to an object. It acts as a bouncer at a club, only admitting VIPs and keeping out the undesirables.
Access control might be user-specific, where only certain users can access your proxied object. Or maybe you want to impose a blocklist, rejecting certain parameters for your object methods.
A Protection Proxy can handle that.
Remote Proxy
The Remote Proxy serves as a local stand-in for an object existing in a separate address space. This proxy acts as a diplomat, a bridge, an interface between two systems located in different spaces.
For example, if you were using some cloud-based services, you could set up a Remote Proxy to manage interactions between your business logic and your remote service.
Logging Proxy
A Logging Proxy generates logs when the proxied object is referenced. Picture a secretary who not only fields all your calls but also logs each one, adding vital information.
When did the call occur? How long was it? Who was on the other end? Were there any events during the call? You get the picture.
Scroll all the way to the bottom for an example of a logging proxy.
Smart Reference Proxy
The Smart Reference Proxy incorporates smart logic that activates when the proxied object is referenced. It’s like an autonomous agent performing actions either before or after your proxied object is referenced.
This pattern is incredibly versatile, as it entails adding logic pre-or post-method reference, a common task in software engineering environments.
And voila! Now we’ve added five different proxy types to our toolbelt and seen some of the software design challenges they can tackle. Up next, we’re diving into the Python side of things. I don’t know about you, but I’m chomping at the bit to get coding.
Let’s dive in!
The Proxy Pattern in Python
Python, as you’re likely aware, is dynamically typed. With its versatile attribute handling and robust class structure, the language excels at prototyping and implementing design patterns. So let’s think of a proxy we can prototype in Python.
Picture this scenario: you are tasked with developing a system that, among other functions, retrieves user data from a server. The hitch is that this process is slow since the target server is halfway across the world. It’s also expensive because the user data is frequently requested, causing these long-running retrieval requests to hog bandwidth.
Here, the Proxy pattern can streamline the process by tapping the server only when necessary.
So, what would that look like in Python? Something like this, possibly:
'''
Sample code explaining the implementation of a Virtual Proxy
'''
class UserData:
"""Fetches user data from the server."""
def get_user_data(self, user_criteria:dict) -> dict:
request_params = self._build_request_params(user_criteria)
user_data = self._make_network_request(request_params)
return user_data
def _build_request_params(self, user_criteria:dict) -> dict:
"""Builds request params from the user criteria.
This method should be implemented in a subclass.
"""
# Dummy implementation.
return {}
def _make_network_request(self, request_params) -> dict:
"""Makes a network request to get user data.
This method should be implemented in a subclass.
"""
# Dummy implementation.
return {}
class UserDataProxy:
"""Fetches user data from the cache if possible, otherwise from the server."""
def __init__(self, user_data: UserData):
self.user_data = user_data
def get_user_data(self, user_criteria:dict) -> UserData:
user_data = self._check_cache_for_user_data(user_criteria)
if user_data:
return user_data
else:
return self.user_data.get_user_data(user_criteria)
def _check_cache_for_user_data(self, user_criteria:dict) -> dict|None:
"""Checks the cache for user data based on the given criteria.
This method should be implemented in a subclass.
"""
# Dummy implementation.
return None
In the above example, the UserDataProxy
stands in for the UserData
object. It trims unnecessary network operations, enhancing our software’s efficiency.
Note that in the strictest implementation of the proxy, you would have an interface from which both the UserData
and UserDataProxy
classes would subclass.
And there you have it!
That’s the Proxy pattern in Python. Keep in mind, like any tool, the Proxy pattern truly shines when applied in the appropriate situation. Knowing how to use it with Python is a potent skill that will amp up your coding prowess and make your software design more modular, efficient, and adaptable.
Step-By-Step Implementation of the Proxy Pattern in Python
With an abstract understanding of the Proxy pattern under our belts, it’s time to roll up our sleeves for a more systematic, step-by-step implementation. Let’s tackle a real-world scenario that we touched on earlier in this article: lazy loading!
Suppose you’re developing an application that involves loading high-resolution images. These massive images could bog down your application if loaded all at once. That’s where your trusty Virtual Proxy comes in – with some bonus logging functionality to keep track of its performance.
Defining the interface
First we must define the interface, the contract that will define the methods to be implemented by our Proxy and our Proxied class. For this example, we only need to define the display_image
interface.
from abc import ABC, abstractmethod
class ImageInterface(ABC):
@abstractmethod
def display_image(self):
pass
In the example above, we define an ImageInterface
with an abstractmethod called display_image
. Any class that will inherit from the ImageInterface will be forced to define an implementation of the display_image
method.
Defining the proxied object
Let’s kick this section off by defining the HighResImage
class, our proxied object. This class symbolizes the resource-heavy, high-resolution image that takes a toll on your resources to load.
import logging
# Uncomment the next line if you're going to use PIL
# from PIL import Image
logging.basicConfig(level=logging.INFO)
class HighResImage(ImageInterface):
def __init__(self, filename):
self.filename = filename
print(f"Loading image {filename}")
self.pillow_image = self._load_image(filename)
def display_image(self):
"""Display the high-resolution image."""
print(f"Displaying image {self.filename}")
# Code to display the image would go here. Not implemented in this example.
# For example, with PIL you could use:
# self.pillow_image.show()
def _load_image(self, filename:str): # -> PIL.image.image
"""Load an image from a file."""
# Uncomment and fill in this method if you're going to use PIL
# try:
# image = Image.open(filename)
# return image
# except IOError:
# logging.error(f"Cannot open image {filename}")
# return None
pass
Proxy Definition
Next, let’s turn our attention to the proxy itself, diving into how it executes the lazy loading logic.
import datetime
class ImageProxy(ImageInterface):
def __init__(self, filename:str):
self.filename = filename
self.image = None
def load_image(self):
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
if self.image is None:
self.image = HighResImage(self.filename)
print(f'ImageProxy loaded {self.filename} at {now}')
else:
print(f'ImageProxy saved one call on {self.filename} at {now}')
print("Image is ready for display")
def display_image(self):
self.load_image()
self.image.display_image()
In this proxy, the ImageProxy maintains a reference to the HighResImage object. However, it only loads the object when the display_image method is called on the ImageProxy. It also has a check in place to make sure the image isn’t already loaded, preventing costly reloads. Lastly, it injects some logging logic for us to glean information about the context of the method reference.
Implementing the ImageProxy in Business Logic
We’ve got our HighResImage and our ImageProxy. Now, we need to employ them in our business logic. Here’s how that might look:
# main.py
proxy_image = ImageProxy('huge_image.jpg')
# The image isn't loaded yet, so you can perform other tasks
proxy_image.display_image()
Now the image is loaded and displayed, but only when needed
This code demonstrates the Proxy pattern in action. Instead of loading the HighResImage immediately when the ProxyImage is created, it postpones initialization until the image needs to be displayed. This approach can conserve a substantial amount of memory and give your application a speed boost when instant image loading isn’t crucial. Moreover, it logs both network call events made by the proxy, and when the proxy saves us a network call. That’s valuable data to have since it allows you to measure your proxy’s efficacy!
To see this in action, run the above code snippet and watch the image loading process get deferred.
And voila! You’ve just implemented the Proxy design pattern in a Python application. Armed with this pattern, you can now craft more efficient and resource-friendly Python applications.
When and Where to Use the Proxy Pattern
Okay, nice! Let’s take a moment to reflect upon our progress. We’ve now learned the ropes and even flexed our skills a little.
But the big question remains: when should we use this pattern?
The Proxy Pattern really comes into its own when you need to control and manage access to an object. It’s worth considering when:
- You need to bolster the security of the underlying real object.
The Protection Proxy can verify a user’s access level before forwarding a request. - Your objects are resource-hungry, and you want to employ lazy-loading or caching.
- You want to execute additional actions when an object is referenced, like reference counting, locking, or loading data from memory.
There’s a good chance you’ve already been using the Proxy pattern without realizing it. For example, it’s very common in networking to add a layer between the client and the server. The concept of a ‘proxy server‘ is an advanced implementation of the proxy pattern.
Additionally, if you’ve used an Object-Relational Mapping tool like sqlalchemy, you’ve likely already benefited from the pattern.
Advantages and Drawbacks of the Proxy Pattern
Design patterns provide us with a blueprint, not a rulebook. Each one entails a trade-off between conferring benefits and introducing challenges. So let’s examine the perks and pitfalls of the Proxy pattern.
Advantages of the Proxy Pattern
The Proxy pattern is a mighty tool when you need to manage or control complex or resource-intensive objects. Some of its perks are:
- Resource optimization: Handy when interacting with resource-intensive objects.
- Access control: Useful for handling sensitive data or controlling object access.
- Separation of concerns: A well-implemented proxy abstracts ancillary logic from the proxied object, enhancing maintainability and ease of use.
Drawbacks of the Proxy Pattern
I’ve harped long and heavy on how useful the proxy pattern is, but I would also like to explicitly call attention to some of its drawbacks. There are no magic bullets in life, and the proxy pattern isn’t a cure-all. A couple of con-leaning considerations around the pattern follow:
- Added complexity: If it’s uncalled-for, you’re introducing unnecessary complexity.
- Debugging difficulty: A shoddily implemented proxy might become a black hole that swallows up propagated exceptions, leaving them untraceable.
Weighing the Pros and Cons
As with any design pattern, there are situations where it could be deemed inappropriate or overkill.
For instance, if you’re not dealing with resource-intensive objects or objects that demand access control, you probably don’t need a proxy. Ultimately, it’s about weighing the additional complexity of the Proxy pattern against the value it brings to your specific scenario.
Understanding design patterns is fantastic, but possessing the intuition and judgement about ‘why’, ‘when’, and ‘how’ to implement them truly sets apart skilled programmers.
The key is to think critically whether the Proxy pattern (or any pattern) is the right tool for your job. Remember, patterns are meant to simplify your life. If a pattern doesn’t fit your needs, trying to force it to could do more harm than good.
Keep exploring, keep learning, but remember that design patterns should be your allies, not your overlords!
Conclusion
We’ve come a long way, dear reader, wandering through the labyrinths of design patterns, delving into the depths of proxies, and emerging armed with an arsenal of wisdom and code. Design patterns, these blueprints, are a beacon of order in an often chaotic world of software engineering.
The Proxy pattern, our protagonist for the day, is an excellent solution to managing access and controlling resource consumption, making our software a tad bit more efficient and maintainable.
Yet, like any tool, it bears two edges. It can make your software glow or plunge it into an abyss of complexity when wielded without discernment. The Proxy pattern brings along a host of benefits: resource optimization, access control, separation of concerns, and more.
But these boons come with a price tag: added complexity, potential black holes for exceptions, and unnecessary usage if your software doesn’t call for its unique advantages.
The real trick, then, lies in deploying this tool when the task demands it and shelving it when it’s uncalled for. This judicious application of your knowledge is what will truly elevate you as a programmer. Keep learning, keep investigating, but remember, not all problems call for the same remedy.
Pursue this path of self-enhancement, keep expanding your abilities, and let your insatiable curiosity chart your course. Maintain a well-stocked toolbox, but keep your mind even sharper, for the mind is the most formidable tool you possess.
Happy coding!
PostScript:
For those esteemed connoisseurs of the design pattern world who might want more, I shall share one more implementation of the pattern. Enjoy!
'''
Sample code discussing the implementation of a Logging Proxy
'''
import logging
from abc import ABC, abstractmethod
logging.basicConfig(level=logging.DEBUG) # Ensures log messages are shown.
class User:
def __init__(self, id:int, name:str):
self.id = id
self.name = name
def __str__(self) -> str:
'''Overriding the __str__ methods allows you to enforce a string representation of the object.
The return value of this function will provide the value for str(self).
'''
return self.name
class OrganizationInterface(ABC):
@abstractmethod
def deduct_balance(self):
pass
class Organization(OrganizationInterface):
def __init__(self, name:str, balance:int):
self.name = name
self.balance = balance
def deduct_balance(self, amount:int) -> int:
self.balance -= amount
return self.balance
def __str__(self):
return self.name
class OrganizationLoggingProxy(OrganizationInterface):
AMOUNT_WARNING_THRESHOLD = 2000
def __init__(self, org:Organization):
self.org = org
def deduct_balance(self, amount:int, user:User) -> int:
balance = self.org.deduct_balance(amount)
if amount > self.AMOUNT_WARNING_THRESHOLD:
logging.warning(f'{user} deducted {amount} from the Organization {self.org}. Remaining balance: {balance}')
return balance
user = User(1, "John Doe")
organization = Organization("Walmart", 50000)
organization.deduct_balance(3000)
organization_proxy = OrganizationLoggingProxy(organization)
organization_proxy.deduct_balance(3000, user)
print("END")