Proxy Pattern
The Proxy Pattern provides a surrogate or placeholder for another object to control access to it. This pattern is useful when we want to add a layer between client code and an actual service object, allowing us to perform various actions before or after the request gets through to the original object.
Problem
There are scenarios where we need to control access to an object, such as when an object is resource-intensive to create, located remotely, or requires access control. Directly accessing such objects might be inefficient, impractical, or insecure.
Components
- Subject: An interface that defines common operations for both the RealSubject and Proxy.
- RealSubject: The actual object that the proxy represents.
- Proxy: Maintains a reference to the RealSubject and controls access to it.
- Client: Works with subjects through the Subject interface.
Implementation Example
Let's implement a simple image loading proxy that defers loading large images until they're actually needed.
// Subject Interface
class Image {
constructor() {
if (this.constructor === Image) {
throw new Error("Abstract class cannot be instantiated");
}
}
display() {
throw new Error("Method 'display()' must be implemented");
}
}
// RealSubject
class HighResolutionImage extends Image {
constructor(filename) {
super();
this.filename = filename;
this.loadImage();
}
loadImage() {
// Simulating a heavy image loading process
console.log(`Loading high-resolution image: ${this.filename}`);
// In reality, this would involve actual image loading logic
this.content = `[Content of ${this.filename}]`;
}
display() {
console.log(`Displaying: ${this.content}`);
return this.content;
}
}
// Proxy
class ImageProxy extends Image {
constructor(filename) {
super();
this.filename = filename;
this.realImage = null;
}
display() {
// Lazy initialization: create the RealSubject only when needed
if (this.realImage === null) {
console.log(`ImageProxy: Loading image on demand`);
this.realImage = new HighResolutionImage(this.filename);
}
// Delegate to the RealSubject
return this.realImage.display();
}
}
// Client code
function clientCode() {
// Using proxies for multiple images
const imageGallery = [
new ImageProxy("image1.jpg"),
new ImageProxy("image2.jpg"),
new ImageProxy("image3.jpg")
];
console.log("Application started. No images loaded yet.");
// Only when display() is called, the actual image gets loaded
console.log("User clicks on the first image:");
imageGallery[0].display();
console.log("User clicks on the third image:");
imageGallery[2].display();
}
Interactive Demo
Virtual Proxy: Lazy Image Loading
This demo shows how a proxy can defer loading large images until they're needed. Click on the thumbnails to load the high-resolution versions.
When to Use
- For lazy initialization (Virtual Proxy): Delaying the creation of a resource-intensive object until it's actually needed
- For access control (Protection Proxy): Checking if the client has appropriate permissions before passing the request to the service object
- For logging requests (Logging Proxy): Keeping a record of requests to the service object
- For caching request results (Caching Proxy): Storing results of expensive operations for future reuse
- For reference counting (Smart Reference Proxy): Tracking when an object is no longer needed and automatically freeing it
Benefits
- Controls access to the original object
- Can add behavior without changing the original object (Open/Closed Principle)
- Works even when the service object isn't ready or available (Remote Proxy)
- Manages the lifecycle of the service object when clients don't care about it
Real-World Uses
- Lazy loading of images or data in web applications
- Protection of sensitive operations through access controls
- Caching mechanisms to improve performance
- Remote proxies for communicating with external services (like REST APIs)
- Smart pointers in languages like C++ for automatic memory management