Proxy Pattern
Proxy Pattern: Controlling Object Access
Section titled “Proxy Pattern: Controlling Object Access”Now let’s explore the Proxy Pattern - a powerful structural design pattern that provides a placeholder or surrogate for another object to control access to it.
Why Proxy Pattern?
Section titled “Why Proxy Pattern?”Imagine you’re accessing a large file on a remote server. Instead of loading the entire file immediately (which is slow and expensive), you use a proxy that loads the file only when you actually need it. The Proxy Pattern works the same way - it provides a placeholder that controls access to the real object, adding functionality like lazy loading, access control, or caching.
The Proxy Pattern provides a surrogate or placeholder for another object to control access to it. The proxy has the same interface as the real object, allowing it to be used interchangeably.
What’s the Use of Proxy Pattern?
Section titled “What’s the Use of Proxy Pattern?”The Proxy Pattern is useful when:
- You want lazy loading - Load expensive objects only when needed
- You want access control - Control who can access the object
- You want caching - Cache results to avoid expensive operations
- You want logging/monitoring - Track access to objects
- You want remote access - Access objects on remote servers
What Happens If We Don’t Use Proxy Pattern?
Section titled “What Happens If We Don’t Use Proxy Pattern?”Without the Proxy Pattern, you might:
- Load everything upfront - Expensive objects loaded even if not used
- No access control - Direct access to sensitive objects
- No caching - Expensive operations repeated unnecessarily
- Tight coupling - Clients directly depend on real objects
- Hard to add cross-cutting concerns - Logging, security scattered everywhere
Simple Example: The Image Loader
Section titled “Simple Example: The Image Loader”Let’s start with a super simple example that anyone can understand!
Visual Representation
Section titled “Visual Representation”Interaction Flow
Section titled “Interaction Flow”Here’s how the Proxy Pattern works in practice - showing how proxy controls access:
sequenceDiagram
participant Client
participant Proxy as ImageProxy
participant RealImage
Client->>Proxy: display()
activate Proxy
Proxy->>Proxy: Check if image loaded
alt Image not loaded
Proxy->>RealImage: load_from_disk()
activate RealImage
RealImage->>RealImage: Expensive loading operation
RealImage-->>Proxy: Image loaded
deactivate RealImage
Proxy->>Proxy: Cache reference
end
Proxy->>RealImage: display()
activate RealImage
RealImage-->>Proxy: Image displayed
deactivate RealImage
Proxy-->>Client: Displayed
deactivate Proxy
Note over Client,RealImage: Proxy loads image only\nwhen needed (lazy loading)!
The Problem
Section titled “The Problem”You’re building an image viewer. Loading images from disk is expensive. Without Proxy Pattern:
# ❌ Without Proxy Pattern - Loads everything upfront!
class RealImage: """Real image - expensive to load""" def __init__(self, filename: str): self.filename = filename self._load_from_disk() # Bad: Loads immediately!
def _load_from_disk(self): """Expensive operation - loads image from disk""" print(f"Loading {self.filename} from disk... (expensive operation)") # Simulate expensive loading import time time.sleep(1) # Simulate disk I/O
def display(self): """Display the image""" print(f"Displaying {self.filename}")
# Problem: All images loaded immediately, even if not displayed!def main(): # Bad: Loads all images upfront, even if not used! images = [ RealImage("photo1.jpg"), # Loaded immediately RealImage("photo2.jpg"), # Loaded immediately RealImage("photo3.jpg"), # Loaded immediately ]
# Only display first image, but all were loaded! images[0].display()
# Problems:# - All images loaded upfront (wasteful!)# - Slow startup time# - High memory usage# - No lazy loading// ❌ Without Proxy Pattern - Loads everything upfront!
public class RealImage { // Real image - expensive to load private String filename;
public RealImage(String filename) { this.filename = filename; loadFromDisk(); // Bad: Loads immediately! }
private void loadFromDisk() { // Expensive operation - loads image from disk System.out.println("Loading " + filename + " from disk... (expensive operation)"); // Simulate expensive loading try { Thread.sleep(1000); // Simulate disk I/O } catch (InterruptedException e) { e.printStackTrace(); } }
public void display() { // Display the image System.out.println("Displaying " + filename); }}
// Problem: All images loaded immediately, even if not displayed!public class Main { public static void main(String[] args) { // Bad: Loads all images upfront, even if not used! RealImage[] images = { new RealImage("photo1.jpg"), // Loaded immediately new RealImage("photo2.jpg"), // Loaded immediately new RealImage("photo3.jpg") // Loaded immediately };
// Only display first image, but all were loaded! images[0].display(); }}
// Problems:// - All images loaded upfront (wasteful!)// - Slow startup time// - High memory usage// - No lazy loadingProblems:
- Loads everything upfront - All images loaded even if not used
- Slow startup - Expensive operations happen immediately
- High memory usage - All objects in memory at once
- No lazy loading - Can’t defer expensive operations
The Solution: Proxy Pattern
Section titled “The Solution: Proxy Pattern”Class Structure
Section titled “Class Structure”classDiagram
class Subject {
<<interface>>
+request() void
}
class RealSubject {
+request() void
}
class Proxy {
-real_subject: RealSubject
+request() void
-check_access() bool
-lazy_load() void
}
class Client {
+operation() void
}
Client --> Subject : uses
Proxy --> Subject : implements
Proxy --> RealSubject : controls access
RealSubject --> Subject : implements
note for Proxy "Controls access to RealSubject"
note for RealSubject "Expensive object"
from abc import ABC, abstractmethod
# Step 1: Define the Subject interfaceclass Image(ABC): """Subject interface - what images should do"""
@abstractmethod def display(self) -> None: """Display the image""" pass
# Step 2: Implement Real Subjectclass RealImage(Image): """Real image - expensive to load"""
def __init__(self, filename: str): self.filename = filename self._load_from_disk()
def _load_from_disk(self): """Expensive operation - loads image from disk""" print(f"Loading {self.filename} from disk... (expensive operation)") # Simulate expensive loading import time time.sleep(1) # Simulate disk I/O
def display(self): """Display the image""" print(f"Displaying {self.filename}")
# Step 3: Create Proxyclass ImageProxy(Image): """Proxy - controls access to real image"""
def __init__(self, filename: str): self.filename = filename self._real_image: RealImage = None # Lazy loading - not loaded yet!
def display(self): """Display image - loads only when needed""" if self._real_image is None: # Lazy loading - load only when actually needed! print(f"Proxy: Loading {self.filename} on demand...") self._real_image = RealImage(self.filename)
# Delegate to real image self._real_image.display()
# Usage - Lazy loading!def main(): print("Creating image proxies (no loading yet)...\n")
# Good: Only creates proxies, doesn't load images! images = [ ImageProxy("photo1.jpg"), # Proxy created, image not loaded ImageProxy("photo2.jpg"), # Proxy created, image not loaded ImageProxy("photo3.jpg"), # Proxy created, image not loaded ]
print("Proxies created. No images loaded yet!\n")
# Only load when actually needed! print("Displaying first image (loads on demand):") images[0].display()
print("\nDisplaying first image again (uses cached):") images[0].display() # Uses cached real image
print("\n✅ Proxy Pattern: Images loaded only when needed (lazy loading)!")
if __name__ == "__main__": main()// Step 1: Define the Subject interfaceinterface Image { // Subject interface - what images should do void display();}
// Step 2: Implement Real Subjectclass RealImage implements Image { // Real image - expensive to load private String filename;
public RealImage(String filename) { this.filename = filename; loadFromDisk(); }
private void loadFromDisk() { // Expensive operation - loads image from disk System.out.println("Loading " + filename + " from disk... (expensive operation)"); // Simulate expensive loading try { Thread.sleep(1000); // Simulate disk I/O } catch (InterruptedException e) { e.printStackTrace(); } }
@Override public void display() { // Display the image System.out.println("Displaying " + filename); }}
// Step 3: Create Proxyclass ImageProxy implements Image { // Proxy - controls access to real image private String filename; private RealImage realImage; // Lazy loading - not loaded yet!
public ImageProxy(String filename) { this.filename = filename; }
@Override public void display() { // Display image - loads only when needed if (realImage == null) { // Lazy loading - load only when actually needed! System.out.println("Proxy: Loading " + filename + " on demand..."); realImage = new RealImage(filename); }
// Delegate to real image realImage.display(); }}
// Usage - Lazy loading!public class Main { public static void main(String[] args) { System.out.println("Creating image proxies (no loading yet)...\n");
// Good: Only creates proxies, doesn't load images! Image[] images = { new ImageProxy("photo1.jpg"), // Proxy created, image not loaded new ImageProxy("photo2.jpg"), // Proxy created, image not loaded new ImageProxy("photo3.jpg") // Proxy created, image not loaded };
System.out.println("Proxies created. No images loaded yet!\n");
// Only load when actually needed! System.out.println("Displaying first image (loads on demand):"); images[0].display();
System.out.println("\nDisplaying first image again (uses cached):"); images[0].display(); // Uses cached real image
System.out.println("\n✅ Proxy Pattern: Images loaded only when needed (lazy loading)!"); }}Real-World Software Example: Database Query Proxy
Section titled “Real-World Software Example: Database Query Proxy”Now let’s see a realistic software example - a database system that needs to cache query results to avoid expensive database calls.
The Problem
Section titled “The Problem”You’re building a database access layer. Database queries are expensive. Without Proxy Pattern:
# ❌ Without Proxy Pattern - No caching!
class Database: """Real database - expensive queries"""
def execute_query(self, query: str) -> dict: """Expensive database query""" print(f"Executing query: {query} (expensive operation)") # Simulate expensive database operation import time time.sleep(2) # Simulate database I/O
# Return mock result return {"result": f"Data for {query}", "rows": 100}
def get_user(self, user_id: int) -> dict: """Get user by ID""" return self.execute_query(f"SELECT * FROM users WHERE id = {user_id}")
# Problem: Every query hits the database, even if same query!def main(): db = Database()
# Same query executed multiple times - wasteful! result1 = db.get_user(1) # Hits database result2 = db.get_user(1) # Hits database again (same query!) result3 = db.get_user(1) # Hits database again (same query!)
print("❌ No caching - same query executed 3 times!")
# Problems:# - No caching - same queries executed repeatedly# - Slow performance# - Unnecessary database load// ❌ Without Proxy Pattern - No caching!
public class Database { // Real database - expensive queries public Map<String, Object> executeQuery(String query) { // Expensive database query System.out.println("Executing query: " + query + " (expensive operation)"); // Simulate expensive database operation try { Thread.sleep(2000); // Simulate database I/O } catch (InterruptedException e) { e.printStackTrace(); }
// Return mock result Map<String, Object> result = new HashMap<>(); result.put("result", "Data for " + query); result.put("rows", 100); return result; }
public Map<String, Object> getUser(int userId) { return executeQuery("SELECT * FROM users WHERE id = " + userId); }}
// Problem: Every query hits the database, even if same query!public class Main { public static void main(String[] args) { Database db = new Database();
// Same query executed multiple times - wasteful! db.getUser(1); // Hits database db.getUser(1); // Hits database again (same query!) db.getUser(1); // Hits database again (same query!)
System.out.println("❌ No caching - same query executed 3 times!"); }}
// Problems:// - No caching - same queries executed repeatedly// - Slow performance// - Unnecessary database loadProblems:
- No caching - Same queries executed repeatedly
- Slow performance - Every query hits the database
- Unnecessary load - Database accessed even for cached queries
The Solution: Proxy Pattern
Section titled “The Solution: Proxy Pattern”from abc import ABC, abstractmethodfrom typing import Dict, Anyimport hashlibimport json
# Step 1: Define the Subject interfaceclass DatabaseInterface(ABC): """Subject interface - database operations"""
@abstractmethod def get_user(self, user_id: int) -> Dict[str, Any]: """Get user by ID""" pass
# Step 2: Implement Real Subjectclass Database(DatabaseInterface): """Real database - expensive queries"""
def execute_query(self, query: str) -> Dict[str, Any]: """Expensive database query""" print(f"Database: Executing query: {query} (expensive operation)") # Simulate expensive database operation import time time.sleep(2) # Simulate database I/O
# Return mock result return {"result": f"Data for {query}", "rows": 100}
def get_user(self, user_id: int) -> Dict[str, Any]: """Get user by ID""" return self.execute_query(f"SELECT * FROM users WHERE id = {user_id}")
# Step 3: Create Proxy with Cachingclass DatabaseProxy(DatabaseInterface): """Proxy - adds caching to database access"""
def __init__(self, database: Database): self._database = database self._cache: Dict[str, Dict[str, Any]] = {} # Cache for query results
def _get_cache_key(self, method: str, *args) -> str: """Generate cache key from method and arguments""" key_data = {"method": method, "args": args} key_string = json.dumps(key_data, sort_keys=True) return hashlib.md5(key_string.encode()).hexdigest()
def get_user(self, user_id: int) -> Dict[str, Any]: """Get user by ID - with caching""" cache_key = self._get_cache_key("get_user", user_id)
# Check cache first if cache_key in self._cache: print(f"Proxy: Cache hit for user {user_id} (fast!)") return self._cache[cache_key]
# Cache miss - query database print(f"Proxy: Cache miss for user {user_id}, querying database...") result = self._database.get_user(user_id)
# Store in cache self._cache[cache_key] = result print(f"Proxy: Cached result for user {user_id}")
return result
def clear_cache(self): """Clear the cache""" self._cache.clear() print("Proxy: Cache cleared")
# Usage - With caching!def main(): database = Database() proxy = DatabaseProxy(database)
print("=== First call (cache miss) ===\n") result1 = proxy.get_user(1) # Cache miss - queries database
print("\n=== Second call (cache hit) ===\n") result2 = proxy.get_user(1) # Cache hit - returns cached result
print("\n=== Third call (cache hit) ===\n") result3 = proxy.get_user(1) # Cache hit - returns cached result
print("\n✅ Proxy Pattern: Caching reduces database load!")
if __name__ == "__main__": main()import java.util.*;import java.security.MessageDigest;
// Step 1: Define the Subject interfaceinterface DatabaseInterface { // Subject interface - database operations Map<String, Object> getUser(int userId);}
// Step 2: Implement Real Subjectclass Database implements DatabaseInterface { // Real database - expensive queries public Map<String, Object> executeQuery(String query) { // Expensive database query System.out.println("Database: Executing query: " + query + " (expensive operation)"); // Simulate expensive database operation try { Thread.sleep(2000); // Simulate database I/O } catch (InterruptedException e) { e.printStackTrace(); }
// Return mock result Map<String, Object> result = new HashMap<>(); result.put("result", "Data for " + query); result.put("rows", 100); return result; }
@Override public Map<String, Object> getUser(int userId) { return executeQuery("SELECT * FROM users WHERE id = " + userId); }}
// Step 3: Create Proxy with Cachingclass DatabaseProxy implements DatabaseInterface { // Proxy - adds caching to database access private Database database; private Map<String, Map<String, Object>> cache; // Cache for query results
public DatabaseProxy(Database database) { this.database = database; this.cache = new HashMap<>(); }
private String getCacheKey(String method, Object... args) { // Generate cache key from method and arguments StringBuilder keyBuilder = new StringBuilder(method); for (Object arg : args) { keyBuilder.append("_").append(arg); } return keyBuilder.toString(); }
@Override public Map<String, Object> getUser(int userId) { // Get user by ID - with caching String cacheKey = getCacheKey("getUser", userId);
// Check cache first if (cache.containsKey(cacheKey)) { System.out.println("Proxy: Cache hit for user " + userId + " (fast!)"); return cache.get(cacheKey); }
// Cache miss - query database System.out.println("Proxy: Cache miss for user " + userId + ", querying database..."); Map<String, Object> result = database.getUser(userId);
// Store in cache cache.put(cacheKey, result); System.out.println("Proxy: Cached result for user " + userId);
return result; }
public void clearCache() { // Clear the cache cache.clear(); System.out.println("Proxy: Cache cleared"); }}
// Usage - With caching!public class Main { public static void main(String[] args) { Database database = new Database(); DatabaseProxy proxy = new DatabaseProxy(database);
System.out.println("=== First call (cache miss) ===\n"); proxy.getUser(1); // Cache miss - queries database
System.out.println("\n=== Second call (cache hit) ===\n"); proxy.getUser(1); // Cache hit - returns cached result
System.out.println("\n=== Third call (cache hit) ===\n"); proxy.getUser(1); // Cache hit - returns cached result
System.out.println("\n✅ Proxy Pattern: Caching reduces database load!"); }}Proxy Pattern Variants
Section titled “Proxy Pattern Variants”There are different types of proxies based on their purpose:
1. Virtual Proxy (Lazy Loading)
Section titled “1. Virtual Proxy (Lazy Loading)”Loads expensive objects only when needed:
# Virtual Proxy - lazy loadingclass Proxy: def __init__(self): self._real_object = None # Not loaded yet
def operation(self): if self._real_object is None: self._real_object = ExpensiveObject() # Load on demand return self._real_object.operation()// Virtual Proxy - lazy loadingclass Proxy implements Subject { private RealSubject realSubject; // Not loaded yet
@Override public void operation() { if (realSubject == null) { realSubject = new RealSubject(); // Load on demand } realSubject.operation(); }}Use case: Lazy loading of expensive resources
2. Protection Proxy (Access Control)
Section titled “2. Protection Proxy (Access Control)”Controls access to objects based on permissions:
# Protection Proxy - access controlclass ProtectionProxy: def __init__(self, user_role: str): self._real_object = RealObject() self._user_role = user_role
def operation(self): if self._user_role != "admin": raise PermissionError("Access denied") return self._real_object.operation()// Protection Proxy - access controlclass ProtectionProxy implements Subject { private RealSubject realSubject; private String userRole;
public ProtectionProxy(String userRole) { this.realSubject = new RealSubject(); this.userRole = userRole; }
@Override public void operation() { if (!"admin".equals(userRole)) { throw new SecurityException("Access denied"); } realSubject.operation(); }}Use case: Access control, security
3. Remote Proxy
Section titled “3. Remote Proxy”Represents objects on remote servers:
# Remote Proxy - remote accessclass RemoteProxy: def __init__(self, server_url: str): self._server_url = server_url
def operation(self): # Make remote call response = requests.post(self._server_url, data={"operation": "do"}) return response.json()// Remote Proxy - remote accessclass RemoteProxy implements Subject { private String serverUrl;
public RemoteProxy(String serverUrl) { this.serverUrl = serverUrl; }
@Override public void operation() { // Make remote call // HTTP request to serverUrl return remoteCall(); }}Use case: Remote method invocation, RPC
4. Caching Proxy
Section titled “4. Caching Proxy”Caches results to avoid expensive operations:
# Caching Proxy - cachingclass CachingProxy: def __init__(self): self._real_object = RealObject() self._cache = {}
def operation(self, key: str): if key in self._cache: return self._cache[key] # Return cached result = self._real_object.operation(key) self._cache[key] = result # Cache result return result// Caching Proxy - cachingclass CachingProxy implements Subject { private RealSubject realSubject; private Map<String, Object> cache;
public CachingProxy() { this.realSubject = new RealSubject(); this.cache = new HashMap<>(); }
@Override public Object operation(String key) { if (cache.containsKey(key)) { return cache.get(key); // Return cached } Object result = realSubject.operation(key); cache.put(key, result); // Cache result return result; }}Use case: Caching, performance optimization
When to Use Proxy Pattern?
Section titled “When to Use Proxy Pattern?”Use Proxy Pattern when:
✅ You want lazy loading - Load expensive objects only when needed
✅ You want access control - Control who can access the object
✅ You want caching - Cache results to avoid expensive operations
✅ You want logging/monitoring - Track access to objects
✅ You want remote access - Access objects on remote servers
When NOT to Use Proxy Pattern?
Section titled “When NOT to Use Proxy Pattern?”Don’t use Proxy Pattern when:
❌ Simple objects - If object is simple, proxy adds unnecessary complexity
❌ No need for control - If you don’t need lazy loading, caching, or access control
❌ Performance critical - Proxy adds indirection (usually negligible)
❌ Over-engineering - Don’t add complexity for simple cases
Common Mistakes to Avoid
Section titled “Common Mistakes to Avoid”Mistake 1: Proxy Not Delegating Properly
Section titled “Mistake 1: Proxy Not Delegating Properly”# ❌ Bad: Proxy doesn't delegate to real objectclass Proxy: def operation(self): return "Proxy result" # Bad: Doesn't use real object!
# ✅ Good: Proxy delegates to real objectclass Proxy: def __init__(self): self._real_object = None
def operation(self): if self._real_object is None: self._real_object = RealObject() return self._real_object.operation() # Good: Delegates// ❌ Bad: Proxy doesn't delegate to real objectclass Proxy implements Subject { @Override public void operation() { return "Proxy result"; // Bad: Doesn't use real object! }}
// ✅ Good: Proxy delegates to real objectclass Proxy implements Subject { private RealSubject realSubject;
@Override public void operation() { if (realSubject == null) { realSubject = new RealSubject(); } return realSubject.operation(); // Good: Delegates }}Mistake 2: Not Maintaining Same Interface
Section titled “Mistake 2: Not Maintaining Same Interface”# ❌ Bad: Proxy has different interfaceclass Proxy: def proxy_operation(self): # Bad: Different method name! pass
# ✅ Good: Proxy has same interfaceclass Proxy(Subject): def operation(self): # Good: Same interface pass// ❌ Bad: Proxy has different interfaceclass Proxy { public void proxyOperation() { // Bad: Different method name! }}
// ✅ Good: Proxy has same interfaceclass Proxy implements Subject { @Override public void operation() { // Good: Same interface }}Mistake 3: Creating Real Object Unnecessarily
Section titled “Mistake 3: Creating Real Object Unnecessarily”# ❌ Bad: Creates real object even if not neededclass Proxy: def __init__(self): self._real_object = RealObject() # Bad: Created immediately!
def operation(self): return self._real_object.operation()
# ✅ Good: Creates real object only when neededclass Proxy: def __init__(self): self._real_object = None # Good: Not created yet
def operation(self): if self._real_object is None: self._real_object = RealObject() # Good: Created on demand return self._real_object.operation()// ❌ Bad: Creates real object even if not neededclass Proxy implements Subject { private RealSubject realSubject = new RealSubject(); // Bad: Created immediately!
@Override public void operation() { return realSubject.operation(); }}
// ✅ Good: Creates real object only when neededclass Proxy implements Subject { private RealSubject realSubject; // Good: Not created yet
@Override public void operation() { if (realSubject == null) { realSubject = new RealSubject(); // Good: Created on demand } return realSubject.operation(); }}Benefits of Proxy Pattern
Section titled “Benefits of Proxy Pattern”- Lazy Loading - Loads expensive objects only when needed
- Access Control - Controls who can access objects
- Caching - Caches results to avoid expensive operations
- Remote Access - Accesses objects on remote servers
- Separation of Concerns - Adds cross-cutting concerns without modifying real object
- Transparent - Client doesn’t know it’s using a proxy
Revision: Quick Catch-Up
Section titled “Revision: Quick Catch-Up”What is Proxy Pattern?
Section titled “What is Proxy Pattern?”Proxy Pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. The proxy has the same interface as the real object.
Why Use It?
Section titled “Why Use It?”- ✅ Lazy loading - Load expensive objects only when needed
- ✅ Access control - Control who can access objects
- ✅ Caching - Cache results to avoid expensive operations
- ✅ Remote access - Access objects on remote servers
- ✅ Separation of concerns - Add cross-cutting concerns
How It Works?
Section titled “How It Works?”- Define subject interface - Common interface for proxy and real object
- Implement real subject - The actual object
- Create proxy - Implements same interface, controls access
- Client uses proxy - Client interacts with proxy (transparent)
- Proxy delegates - Proxy delegates to real object when needed
Key Components
Section titled “Key Components”Client → Proxy → RealSubject- Subject - Interface for proxy and real object
- RealSubject - The actual object
- Proxy - Controls access to real subject
- Client - Uses proxy (doesn’t know it’s a proxy)
Simple Example
Section titled “Simple Example”class Proxy(Subject): def __init__(self): self._real_subject = None
def operation(self): if self._real_subject is None: self._real_subject = RealSubject() # Lazy load return self._real_subject.operation()When to Use?
Section titled “When to Use?”✅ Lazy loading
✅ Access control
✅ Caching
✅ Remote access
✅ Logging/monitoring
When NOT to Use?
Section titled “When NOT to Use?”❌ Simple objects
❌ No need for control
❌ Performance critical
❌ Over-engineering
Key Takeaways
Section titled “Key Takeaways”- Proxy Pattern = Controls access to real object
- Proxy = Placeholder with same interface
- RealSubject = Actual object
- Benefit = Lazy loading, caching, access control
- Use Case = Expensive resources, caching, security
Common Pattern Structure
Section titled “Common Pattern Structure”class Proxy(Subject): def __init__(self): self._real_subject = None
def operation(self): if self._real_subject is None: self._real_subject = RealSubject() return self._real_subject.operation()Remember
Section titled “Remember”- Proxy Pattern controls access to real objects
- It has the same interface as real object
- It’s about control, not just wrapping!
- Use it for lazy loading, caching, access control
- Keep it transparent - client shouldn’t know it’s a proxy!
Interview Focus: Proxy Pattern
Section titled “Interview Focus: Proxy Pattern”Key Points to Remember
Section titled “Key Points to Remember”1. Core Concept
Section titled “1. Core Concept”What to say:
“Proxy Pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. The proxy has the same interface as the real object, allowing it to be used interchangeably while adding functionality like lazy loading, caching, or access control.”
Why it matters:
- Shows you understand the fundamental purpose
- Demonstrates knowledge of when to use it
- Indicates you can explain concepts clearly
2. When to Use Proxy Pattern
Section titled “2. When to Use Proxy Pattern”Must mention:
- ✅ Lazy loading - Load expensive objects only when needed
- ✅ Access control - Control who can access objects
- ✅ Caching - Cache results to avoid expensive operations
- ✅ Remote access - Access objects on remote servers
- ✅ Logging/monitoring - Track access to objects
Example scenario to give:
“I’d use Proxy Pattern when building an image viewer. Loading images from disk is expensive. Instead of loading all images upfront, I create image proxies that load images only when they’re actually displayed. This improves startup time and reduces memory usage.”
3. Proxy vs Decorator Pattern
Section titled “3. Proxy vs Decorator Pattern”Must discuss:
- Proxy: Controls access, same interface, adds functionality like lazy loading/caching
- Decorator: Adds behavior, same interface, wraps to enhance functionality
- Key difference: Proxy controls access, Decorator adds behavior
Example to give:
“Proxy Pattern controls access to an object and adds functionality like lazy loading or caching. Decorator Pattern adds behavior to an object dynamically. Proxy is about control and optimization, Decorator is about adding features. Proxy typically has one real object, Decorator can wrap multiple decorators.”
4. Proxy Variants
Section titled “4. Proxy Variants”Must discuss:
- Virtual Proxy: Lazy loading of expensive resources
- Protection Proxy: Access control and security
- Remote Proxy: Remote object access
- Caching Proxy: Caching expensive operations
Example to give:
“I use Virtual Proxy for lazy loading images, Protection Proxy for access control in a secure system, Remote Proxy for accessing objects on remote servers, and Caching Proxy for caching database query results. Each variant serves a specific purpose.”
5. Benefits and Trade-offs
Section titled “5. Benefits and Trade-offs”Benefits to mention:
- Lazy loading - Loads expensive objects only when needed
- Access control - Controls who can access objects
- Caching - Caches results to avoid expensive operations
- Separation of concerns - Adds cross-cutting concerns without modifying real object
- Transparent - Client doesn’t know it’s using a proxy
Trade-offs to acknowledge:
- Complexity - Adds abstraction layer
- Performance - Small overhead (usually negligible)
- Over-engineering risk - Can be overkill for simple cases
6. Common Interview Questions
Section titled “6. Common Interview Questions”Q: “What’s the difference between Proxy Pattern and Decorator Pattern?”
A:
“Proxy Pattern controls access to an object and adds functionality like lazy loading, caching, or access control. Decorator Pattern adds behavior to an object dynamically. Proxy is about control and optimization, Decorator is about adding features. Proxy typically manages one real object, Decorator can wrap multiple decorators in a chain.”
Q: “When would you use Virtual Proxy vs Caching Proxy?”
A:
“I use Virtual Proxy when I want to defer the creation of expensive objects until they’re actually needed - like loading images only when displayed. I use Caching Proxy when I want to cache the results of expensive operations - like caching database query results to avoid repeated queries.”
Q: “How does Proxy Pattern relate to SOLID principles?”
A:
“Proxy Pattern supports the Single Responsibility Principle by separating access control, caching, or lazy loading into the proxy. It supports the Open/Closed Principle - you can add functionality (proxy) without modifying the real object. It supports the Dependency Inversion Principle by having clients depend on the Subject interface rather than concrete implementations.”
Interview Checklist
Section titled “Interview Checklist”Before your interview, make sure you can:
- Define Proxy Pattern clearly in one sentence
- Explain when to use it (with examples showing lazy loading/caching)
- Describe Proxy vs Decorator Pattern
- Implement Proxy Pattern from scratch
- Compare with other structural patterns (Decorator, Adapter)
- List proxy variants (Virtual, Protection, Remote, Caching)
- List benefits and trade-offs
- Identify common mistakes (not delegating, different interface)
- Give 2-3 real-world examples
- Connect to SOLID principles
- Discuss when NOT to use it
Remember: Proxy Pattern is about controlling access to objects - providing a placeholder that adds functionality like lazy loading, caching, or access control! 🎯