Proxy Pattern #
The Proxy pattern is a structural design pattern that provides a surrogate or placeholder for another object. By controlling access to the underlying object, the Proxy pattern allows additional functionality, such as lazy initialization, access control, logging, or caching, to be added without modifying the original object.
Intent #
The main intent of the Proxy pattern is to control access to an object by acting as a stand-in for it, allowing additional functionality to be added transparently. This pattern is particularly useful when working with resource-intensive objects, objects that need controlled access, or objects that need enhanced functionality.
Problem and Solution #
Problem #
Suppose you’re building an image viewer that can display high-resolution images from a remote server. Loading every image in full resolution at once can be slow and resource-intensive. Without a mechanism to manage the loading process, the application might become sluggish or unresponsive.
Solution #
The Proxy pattern can address this by creating an ImageProxy
class that stands in for the actual image. The ImageProxy
can handle loading the image only when it’s actually needed (lazy loading), making the application more responsive. Once loaded, the proxy forwards requests to the real image, making the process seamless to the client.
Structure #
The Proxy pattern typically includes:
- Subject: Defines the common interface for both the Real Subject and the Proxy.
- Real Subject: The actual object that performs the work and to which access is controlled.
- Proxy: Controls access to the Real Subject and may add additional functionality before or after forwarding requests to it.
UML Diagram #
+------------------+ +------------------+
| Subject |<----| RealSubject |
|------------------| |------------------|
| + request() | | + request() |
+------------------+ +------------------+
^
|
+------------------+
| Proxy |
|-----------------|
| + request() |
+-----------------+
Example: Image Viewer with Lazy Loading #
Let’s implement an image viewer using the Proxy pattern. We’ll create an ImageProxy
that controls access to the high-resolution RealImage
object, loading it only when necessary.
Step 1: Define the Subject Interface #
The Image
interface defines the display
method that both the real image and the proxy must implement.
// Subject Interface
interface Image {
void display();
}
Step 2: Implement the Real Subject #
The RealImage
class represents a high-resolution image that takes time and resources to load.
// Real Subject
class RealImage implements Image {
private String filename;
public RealImage(String filename) {
this.filename = filename;
loadImageFromDisk();
}
private void loadImageFromDisk() {
System.out.println("Loading image from disk: " + filename);
}
@Override
public void display() {
System.out.println("Displaying image: " + filename);
}
}
Step 3: Implement the Proxy #
The ImageProxy
class controls access to the RealImage
, only loading it when the display
method is called for the first time.
// Proxy
class ImageProxy implements Image {
private RealImage realImage;
private String filename;
public ImageProxy(String filename) {
this.filename = filename;
}
@Override
public void display() {
if (realImage == null) {
realImage = new RealImage(filename); // Load only when needed
}
realImage.display();
}
}
Step 4: Client Code Using the Proxy #
The client interacts with ImageProxy
just as it would with RealImage
, without knowing if the image has been loaded yet.
public class Client {
public static void main(String[] args) {
Image image1 = new ImageProxy("high_res_image1.jpg");
Image image2 = new ImageProxy("high_res_image2.jpg");
// The image is loaded only when display() is called for the first time
System.out.println("Displaying images:");
image1.display(); // Loads and displays high_res_image1.jpg
image1.display(); // Displays high_res_image1.jpg without loading again
image2.display(); // Loads and displays high_res_image2.jpg
}
}
Output #
Displaying images:
Loading image from disk: high_res_image1.jpg
Displaying image: high_res_image1.jpg
Displaying image: high_res_image1.jpg
Loading image from disk: high_res_image2.jpg
Displaying image: high_res_image2.jpg
In this example:
RealImage
is the resource-intensive object that loads an image from disk.ImageProxy
controls access toRealImage
, only creating it whendisplay
is called for the first time (lazy loading).- The client interacts with
ImageProxy
, which behaves likeRealImage
and loads the image only when necessary.
Types of Proxy #
The Proxy pattern can be implemented in different ways, depending on the additional functionality needed:
- Virtual Proxy: Manages access to a resource-intensive object by creating it only when necessary (e.g., lazy loading).
- Remote Proxy: Manages interaction with an object located on a remote server, handling communication details.
- Protection Proxy: Controls access to an object by enforcing permissions or authentication checks.
- Cache Proxy: Provides temporary storage to improve performance by caching responses from a resource-intensive or remote object.
Applicability #
Use the Proxy pattern when:
- You need to control access to a resource-intensive or remote object.
- You want to add additional functionality, such as lazy loading, caching, or access control, to an object without modifying it.
- You want to separate concerns in your code, allowing a proxy to manage specific tasks independently of the real subject.
Advantages and Disadvantages #
Advantages #
- Controlled Access: Proxy allows controlled access to the real subject, enhancing security, performance, or resource management.
- Lazy Initialization: The pattern supports lazy loading, reducing resource usage by delaying object creation until necessary.
- Decoupling: Proxy separates client code from specific functionalities (e.g., caching, loading), making the system more modular and maintainable.
Disadvantages #
- Increased Complexity: Proxy introduces additional layers, which may increase code complexity.
- Potential Performance Overhead: Depending on implementation, proxies can add some latency, especially if multiple layers are involved.
- May Lead to Over-Engineering: Proxy should be used judiciously, as adding unnecessary proxies can lead to a complex and difficult-to-understand codebase.
Best Practices for Implementing the Proxy Pattern #
- Identify Core Functionality and Additional Concerns: Use Proxy to separate additional concerns like security, caching, or logging from the main functionality.
- Use Lazy Loading and Caching Carefully: Ensure that lazy loading and caching improve performance without introducing excessive delays or stale data.
- Keep Proxy Interface Consistent: Maintain consistency between the Proxy and Real Subject interfaces to avoid confusion and unexpected behavior.
Conclusion #
The Proxy pattern provides a flexible way to control access to objects, allowing you to add functionality such as lazy loading, caching, or access control transparently. This pattern enhances modularity and performance in systems with resource-intensive, remote, or protected objects.