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()