You’re building a system that loads large images. Loading all images at startup would be slow and memory-intensive. You need images to load only when accessed, with caching to avoid reloading. The Proxy pattern provides a placeholder that controls access to the real object, enabling lazy loading, caching, access control, and more.
Problem
Consider an image loading system:
public class Image
{
private string _filename;
public Image(string filename)
{
_filename = filename;
LoadFromDisk(); // Expensive operation
}
private void LoadFromDisk()
{
Console.WriteLine($"Loading {_filename} from disk...");
// Simulate slow loading
System.Threading.Thread.Sleep(1000);
}
public void Display()
{
Console.WriteLine($"Displaying {_filename}");
}
}
Creating an Image object immediately loads it from disk, even if you never display it. If you create 100 image objects, you wait 100 seconds at startup. You need lazy loading—load only when needed.
Solution
The Proxy pattern provides a surrogate or placeholder for another object to control access to it. A proxy acts as an intermediary, controlling access to the real object.
Definition: The Proxy pattern lets you provide a substitute or placeholder for another object. A proxy controls access to the original object, allowing you to perform something either before or after the request gets through to the original object.
Common proxy types:
- Virtual Proxy: Lazy initialization of expensive objects
- Protection Proxy: Access control
- Remote Proxy: Local representation of remote objects
- Caching Proxy: Caching results of expensive operations
Pseudocode
Client
↓ uses
Subject Interface (IImage)
↓ implemented by
RealSubject (RealImage) and Proxy (ImageProxy)
↓ Proxy controls access to
RealSubject
Examples
Here’s a complete, runnable C# implementation:
using System;
using System.Collections.Generic;
// Subject interface
public interface IImage
{
void Display();
}
// Real subject
public class RealImage : IImage
{
private string _filename;
public RealImage(string filename)
{
_filename = filename;
LoadFromDisk();
}
private void LoadFromDisk()
{
Console.WriteLine($"Loading {_filename} from disk...");
// Simulate expensive operation
System.Threading.Thread.Sleep(500);
}
public void Display()
{
Console.WriteLine($"Displaying {_filename}");
}
}
// Proxy - virtual proxy for lazy loading
public class ImageProxy : IImage
{
private RealImage _realImage;
private string _filename;
public ImageProxy(string filename)
{
_filename = filename;
}
public void Display()
{
if (_realImage == null)
{
_realImage = new RealImage(_filename);
}
_realImage.Display();
}
}
// Usage
class Program
{
static void Main()
{
Console.WriteLine("Creating image proxies (no loading yet)...\n");
var image1 = new ImageProxy("photo1.jpg");
var image2 = new ImageProxy("photo2.jpg");
var image3 = new ImageProxy("photo3.jpg");
Console.WriteLine("Images created instantly!\n");
Console.WriteLine("Now displaying images (loading happens now):\n");
image1.Display();
image2.Display();
image3.Display();
}
}
Output:
Creating image proxies (no loading yet)...
Images created instantly!
Now displaying images (loading happens now):
Loading photo1.jpg from disk...
Displaying photo1.jpg
Loading photo2.jpg from disk...
Displaying photo2.jpg
Loading photo3.jpg from disk...
Displaying photo3.jpg
Real-World Example: Caching Proxy
// Subject interface
public interface IDataService
{
string GetData(string key);
}
// Real subject
public class DatabaseService : IDataService
{
public string GetData(string key)
{
Console.WriteLine($"Fetching {key} from database...");
// Simulate database query
System.Threading.Thread.Sleep(1000);
return $"Data for {key}";
}
}
// Proxy with caching
public class CachingProxy : IDataService
{
private DatabaseService _databaseService;
private Dictionary<string, string> _cache = new();
public CachingProxy()
{
_databaseService = new DatabaseService();
}
public string GetData(string key)
{
if (_cache.ContainsKey(key))
{
Console.WriteLine($"Returning {key} from cache");
return _cache[key];
}
var data = _databaseService.GetData(key);
_cache[key] = data;
return data;
}
}
// Usage
var service = new CachingProxy();
Console.WriteLine("First call (from database):");
var data1 = service.GetData("user-123");
Console.WriteLine($"Result: {data1}\n");
Console.WriteLine("Second call (from cache):");
var data2 = service.GetData("user-123");
Console.WriteLine($"Result: {data2}");
Protection Proxy Example
// Subject interface
public interface IDocument
{
void View();
void Edit();
}
// Real subject
public class Document : IDocument
{
private string _content;
public Document(string content)
{
_content = content;
}
public void View()
{
Console.WriteLine($"Viewing document: {_content}");
}
public void Edit()
{
Console.WriteLine($"Editing document: {_content}");
}
}
// Protection proxy
public class ProtectedDocument : IDocument
{
private Document _document;
private string _userRole;
public ProtectedDocument(Document document, string userRole)
{
_document = document;
_userRole = userRole;
}
public void View()
{
_document.View();
}
public void Edit()
{
if (_userRole == "Admin")
{
_document.Edit();
}
else
{
Console.WriteLine("Access denied: Only admins can edit documents");
}
}
}
// Usage
var doc = new Document("Secret Document");
var userDoc = new ProtectedDocument(doc, "User");
var adminDoc = new ProtectedDocument(doc, "Admin");
userDoc.View();
userDoc.Edit(); // Denied
adminDoc.View();
adminDoc.Edit(); // Allowed
Applications
Enterprise Use Cases:
- Lazy Loading: Loading expensive resources only when needed (images, videos, large datasets)
- Caching: Caching results of expensive operations (database queries, API calls)
- Access Control: Controlling access to sensitive objects based on permissions
- Remote Objects: Representing remote objects locally (RPC, web services)
- Logging: Adding logging behavior without modifying the real object
- Performance Monitoring: Tracking method call performance
- Connection Pooling: Managing database connections efficiently
- Firewall/Security: Controlling access to network resources
Benefits:
- Controls access to the real object
- Can perform operations before/after the real object is accessed
- Can add functionality (caching, logging) without modifying the real object
- Supports lazy initialization of expensive objects
- Follows the Open/Closed Principle
When to Use:
- You need lazy initialization of expensive objects
- You want to add access control to objects
- You need to add functionality (caching, logging) without changing the real object
- You’re working with remote objects
- You want to optimize performance through caching or connection pooling
Proxy vs Decorator:
- Proxy: Controls access to an object (same interface, controls lifecycle)
- Decorator: Adds behavior to an object (enhances functionality, doesn’t control access)
Key Insight: A proxy stands in for another object and controls access to it. The client interacts with the proxy as if it were the real object, but the proxy can add behavior like lazy loading, caching, or access control before delegating to the real object.
