Introduction
The Proxy Design Pattern is a design pattern belonging to the set of structural patterns. Structural patterns are a category of design patterns used to simplify the design of a program on its structural level.
As its name suggests, the proxy pattern means using a proxy for some other entity. In other words, a proxy is used as an intermediary in front of, or wrapped around, an existing object. This can be used, for example, when the actual object is very resource-intensive or when there are certain conditions which need to be checked before using the actual object. A proxy can also be useful if we'd like to limit the access or functionality of an object.
In this article, we'll describe the proxy pattern and show some examples in which it can be used.
The Idea behind Proxy
Proxy is used to encapsulate functionalities of another object or system. Consider remote method invocation, for example, which is a way of calling methods on another machine. In Java, this is accomplished via a remote proxy which is essentially an object providing a local representation of another remote object. Calling a method from another machine is then made possible simply by calling a method of the proxy object.
Each proxy is realized in such a way that it offers exactly the same interface to the client as a real object. This means that the client effectively notices no difference while using the proxy object.
There are several type of proxy objects. As can probably be inferred from the previous example, remote proxies are used to access some remote objects or resources. Besides remote proxies, there are also virtual proxies and protection proxies. Let's briefly describe each of them for a better understanding.
Remote Proxies
Remote Proxies provide a local representation of another remote object or resource. Remote proxies are responsible not just for representation but also for some maintenance work. Such work could include connecting to a remote machine and maintaining the connection, encoding and decoding characters obtained through networking traffic, parsing, etc.
Virtual Proxies
Virtual Proxies wrap expensive objects and loads them on-demand. Sometimes we don't immediately need all functionalities that an object offers, especially if it is memory/time-consuming. Calling objects only when needed might increase performance quite a bit, as we'll see in the example below.
Protection Proxies
Protection Proxies are used for checking certain conditions. Some objects or resources might need appropriate authorization for accessing them, so using a proxy is one of the ways in which such conditions can be checked. With protection proxies, we also get the flexibility of having many variations of access control.
For example, if we're trying to provide access to a resource of an operating system, there are usually multiple categories of users. We could have a user who isn't allowed to view or edit the resource, a user who can do with the resource whatever they wish, etc.
Having proxies act as wrappers around such resources is a great way of implementing customized access control.
Implementation
Virtual Proxy Example
One example of a virtual proxy is loading images. Let's imagine that we're building a file manager. Like any other file manager, this one should be able to display images in a folder that a user decides to open.
If we assume there exists a class, ImageViewer
, responsible for loading and displaying images - we might implement our file manager by using this class directly. This kind of approach seems logical and straight-forward but it contains a subtle problem.
If we implement the file manager as described above, we're going to be loading images every time they appear in the folder. If the user only wishes to see the name or size of an image, this kind of approach would still load the entire image into memory. Since loading and displaying images are expensive operations, this can cause performance issues.
A better solution would be to display images only when actually needed. In this sense, we can use a proxy to wrap the existing ImageViewer
object. This way, the actual image viewer will only get called when the image needs to be rendered. All other operations (such as obtaining the image name, size, date of creation, etc.) don't require the actual image and can therefore be obtained through a much lighter proxy object instead.
Let's first create our main interface:
interface ImageViewer {
public void displayImage();
}
Next, we'll implement the concrete image viewer. Note that operations occurring in this class are costly:
public class ConcreteImageViewer implements ImageViewer {
private Image image;
public ConcreteImageViewer(String path) {
// Costly operation
this.image = Image.load(path);
}
@Override
public void displayImage() {
// Costly operation
image.display();
}
}
Now we'll implement our lightweight image viewer proxy. This object will call the concrete image viewer only when needed, i.e. when the client calls the displayImage()
method. Until then, no images will be loaded or processed, which will make our program much more efficient.
public class ImageViewerProxy implements ImageViewer {
private String path;
private ImageViewer viewer;
public ImageViewerProxy(String path) {
this.path = path;
}
@Override
public void displayImage() {
this.viewer = new ConcreteImageViewer(this.path);
this.viewer.displayImage();
}
}
Finally, we'll write the client-side of our program. In the code below, we are creating six different image viewers. First, three of them are the concrete image viewers that automatically load images on creation. The last three images don't load any images into memory on creation.
Only with the last line will the first proxy viewer start loading the image. Compared to the concrete viewers, performance benefits are obvious:
public static void main(String[] args) {
ImageViewer flowers = new ConcreteImageViewer("./photos/flowers.png");
ImageViewer trees = new ConcreteImageViewer("./photos/trees.png");
ImageViewer grass = new ConcreteImageViewer("./photos/grass.png");
ImageViewer sky = new ImageViewerProxy("./photos/sky.png");
ImageViewer sun = new ImageViewerProxy("./photos/sun.png");
ImageViewer clouds = new ImageViewerProxy("./photos/clouds.png");
sky.displayImage();
}
Another thing we could do is add a null
-check in the displayImage()
method of the ImageViewerProxy
:
@Override
public void displayImage() {
if (this.viewer == null) {
this.viewer = new ConcreteImageViewer(this.path);
}
this.viewer.displayImage();
}
So, if we call:
ImageViewer sky = new ImageViewerProxy("./photos/sky.png");
sky.displayImage();
sky.displayImage();
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
Only once will the new ConcreteImageViewer
call be executed. This will even further decrease the memory footprint of our application.
Note: This example does not contain fully-compilable Java code. Some method calls, such as
Image.load(String path)
, are fictional and written in a simplified way mostly for illustration purposes.
Protection Proxy Example
In this example, we'll be flying a spaceship. Prior to that, we need to create two things: The Spaceship
interface and the Pilot
model:
interface Spaceship {
public void fly();
}
public class Pilot {
private String name;
// Constructor, Getters, and Setters
}
Now we're going to implement the Spaceship
interface and create an actual spaceship class:
public class MillenniumFalcon implements Spaceship {
@Override
public void fly() {
System.out.println("Welcome, Han. The Millennium Falcon is starting up its engines!");
}
}
The MillenniumFalcon
class represents a concrete spaceship which can be used by our Pilot
. However, there could be some conditions we might like to check before allowing the pilot to fly the spaceship. For example, perhaps we would like to see if the pilot has the appropriate certificate or if they are old enough to fly. To check these conditions, we can use the proxy design pattern.
In this example, we're going to be checking if the pilot's name is "Han Solo" since he is the rightful owner of the ship. We start by implementing the Spaceship
interface as before.
We are going to use Pilot
and Spaceship
as our class variables since we can obtain all relevant information from them:
public class MillenniumFalconProxy implements Spaceship {
private Pilot pilot;
private Spaceship falcon;
public MillenniumFalconProxy(Pilot pilot) {
this.pilot = pilot;
this.falcon = new MillenniumFalcon();
}
@Override
public void fly() {
if (pilot.getName().equals("Han Solo")) {
falcon.fly();
} else {
System.out.printf("Sorry %s, only Han Solo can fly the Falcon!\n", pilotName);
}
}
}
The client side of the program can then be written as shown below. If "Han Solo" is the pilot, the Falcon will be allowed to fly. Otherwise, it won't be allowed to leave the hangar:
public static void main(String[] args) {
Spaceship falcon1 = new MillenniumFalconProxy(new Pilot("Han Solo"));
falcon1.fly();
Spaceship falcon2 = new MillenniumFalconProxy(new Pilot("Jabba the Hutt"));
falcon2.fly();
}
The output for the above calls will then result in the following:
Welcome, Han. The Millennium Falcon is starting up its engines!
Sorry Jabba the Hutt, only Han Solo can fly the Falcon!
Pros and Cons
Pros
- Security: By using a proxy, certain conditions can be checked while accessing the object and controlled usage of potentially "dangerous" classes and resources is enforced.
- Performance: Some objects might be highly demanding in terms of memory and execution time. By using a proxy, we can wrap such objects with costly operations so that they are called only when really needed, or avoid unnecessary instantiation.
Cons
-
Performance: Yes, performance can also be a disadvantage of the proxy pattern. How, you might ask? Let's say that a proxy object is used to wrap an object existing somewhere across the network. Since this is a proxy, it can hide from the client the fact that remote communication is involved.
This can in turn make the client inclined to write inefficient code because they will not be aware that an expensive network call is being made in the background.
Conclusion
The Proxy Design Pattern is a clever way of using some costly resources or providing certain access rights. It is structurally similar to the Adapter and Decorator patterns, although with a different purpose.
Proxy can be used in a variety of circumstances since demanding resources are a common occurrence in programming, especially when dealing with databases and networking.
Knowing how to efficiently access those resources while providing appropriate access control is therefore crucial for building scalable and secure applications.