Proxy
Proxies provide a substitute or placeholder for another object. The proxy itself controls access to the original object, which can be used for security, lazy loading, remote access, logging, etc. without interacting directly with the primary object.
TL;DR
Using the proxy pattern we can create a class that represents the functionality of another class.
https://refactoring.guru/design-patterns/proxy
Problem
Suppose you have an application that needs to access a remote SQL server, and the spin up time for queries is rather long. If access to this database is required, but not constantly required, clients can implement lazy initialization; however, this might create a lot of code duplication or the class may be a 3rd-party library that we cannot modify.
Solution
Rather than let the clients access the database directly, the Proxy pattern suggests that we implement a new proxy class with the same interface as the original database. With this, we can get all the clients to communicate with the Proxy, rather than directly with the database, allowing us to execute something before or after the primary logic of the class.
For instance, we can use the Proxy to cache queries before sending them in bulk, or to block dangerous queries (allowing the Database class itself to follow Single Responsibility Principle (SRP)).
from abc import ABC, abstractmethod
import time
# Subject interface
class DatabaseInterface(ABC):
@abstractmethod
def query(self, sql):
pass
# Real Subject - the actual database connection
class Database(DatabaseInterface):
def __init__(self):
print("Connecting to database...")
time.sleep(1) # Simulate connection time
print("Database connected!")
def query(self, sql):
print(f"Executing query: {sql}")
return f"Results for: {sql}"
# Proxy - controls access to the real database
class DatabaseProxy(DatabaseInterface):
def __init__(self):
self._database = None
self._cache = {}
def query(self, sql):
# Lazy initialization - only create database when needed
if self._database is None:
print("Proxy: Creating database connection...")
self._database = Database()
# Caching - return cached results if available
if sql in self._cache:
print("Proxy: Returning cached result")
return self._cache[sql]
# Access control - block dangerous queries
if "DROP" in sql.upper() or "DELETE" in sql.upper():
print("Proxy: Blocking dangerous query!")
return "Access denied: Dangerous operation"
# Forward request to real database
result = self._database.query(sql)
self._cache[sql] = result
return result
# Client code
def main():
print("=== Using Database Proxy ===")
# Create proxy (no database connection yet)
db_proxy = DatabaseProxy()
# First query - will create connection and cache result
print("\n1. First query:")
result1 = db_proxy.query("SELECT * FROM users")
print(f"Result: {result1}")
# Same query - will return cached result
print("\n2. Same query again:")
result2 = db_proxy.query("SELECT * FROM users")
print(f"Result: {result2}")
# Dangerous query - will be blocked
print("\n3. Dangerous query:")
result3 = db_proxy.query("DROP TABLE users")
print(f"Result: {result3}")
# New query - will execute normally
print("\n4. New query:")
result4 = db_proxy.query("SELECT * FROM products")
print(f"Result: {result4}")
if __name__ == "__main__":
main()