Proxy Design Pattern

What is Proxy Pattern?

In OOPS, Proxy is a structural design pattern that provides a placeholder for another object to control its access. Here proxy object works as an intermediary between the client and the real object so that the client will interact with the proxy instead of the real object. In this situation, the proxy object can perform tasks like managing access to the real object and providing additional functionalities like filtering, caching, logging, etc.

Let’s understand via an analogy

In many situations, organizations use a representative to interact with other parties on their behalf. For example, suppose a company (client) that wants to negotiate a contract with a supplier (real object). Instead of sending a large team to meet with the supplier, the company can hire a single smart representative to negotiate on their behalf. Here representatives act as a proxy for the company and communicate with the supplier to reach an agreement.

Key components and structure

UML diagram of proxy design pattern

  • RealService: This does most of the real work and the proxy object controls access to it.
  • Service interface: Both Proxy and RealService implement the Service interface. This allows any client to treat the proxy just like the RealService.
  • Proxy: Proxy instantiates or handles the creation of the RealService object. For this, Proxy keeps a reference to the Service, so it can forward requests to the RealService.
  • Client: user who wants to use the RealService object.

Let’s understand via an example

public class Video {
    private String fileName;
    
    public Video(String fileName) {
        this.fileName = fileName;
        loadFromDisk(fileName);
    }
    
    public void play() {
        System.out.println("Playing video: " + fileName);
    }
    
    private void loadFromDisk(String fileName) {
        System.out.println("Loading video: " + fileName);
    }
}

// Client code
public class Main {
    public static void main(String[] args) {
        Video video = new Video("video.mp4");
        video.play();
    }
}

In the above code, we have created a Video class that loads and plays a video file. If we observe, we are loading the video file from the disk when we are creating an object of a Video class, even if the user doesn’t want to play it. So if a video file is large, the process of loading a video file from disk will be slow. This can result in a poor user experience, and it may not be an optimal solution.

So what would be the optimal solution? How do we use a proxy pattern to solve this problem? One idea is: We should not load the video file until we request to play it. Let’s understand this solution!

proxy design pattern real world example

We create an interface Video which is implemented by VideoImpl and VideoProxy. Here VideoProxy class will act as a proxy for the VideoImpl class.

Now user will use an object of VideoProxy. VideoProxy class will only create an instance of VideoImpl class when the user requests to play the video by calling the play method of VideoProxy. This lazy initialization will improve the user experience because we only load the video file when the user wants to play it.

interface Video {
    void play();
}

class VideoImpl implements Video {
    private String fileName;
    
    public VideoImpl(String fileName) {
        this.fileName = fileName;
        loadFromDisk(fileName);
    }
    
    public void play() {
        System.out.println("Playing video: " + fileName);
    }
    
    private void loadFromDisk(String fileName) {
        System.out.println("Loading video: " + fileName);
    }
}

class VideoProxy implements Video {
    private String fileName;
    private Video video;
    
    public VideoProxy(String fileName) {
        this.fileName = fileName;
    }
    
    public void play() {
        if (video == null) {
            video = new VideoImpl(fileName);
        }
        video.play();
    }
}

// Client code
public class Main {
    public static void main(String[] args) {
        Video video = new VideoProxy("video.mp4");
        video.play();
    }
}

Why do we use a proxy pattern?

  • To add an extra layer of security and abstraction between the client and the actual object: In certain scenarios, we may want to restrict access to the object. So we can use a proxy as a gatekeeper, which can allow or deny access to the object based on the given restriction.
  • To cache results from an object to improve performance: When a client requests a resource, the proxy will check if the resource has already been cached. If yes, then the proxy will return the cached resource instead of calling the actual object.
  • To instantiate real objects when it is needed (lazy loading): This could improve performance and reduce memory usage. In other words, this can be useful in situations where we want to defer the creation of an object until it is needed.
  • To provide remote access and separation of concern: A proxy can be used to provide a local representation of a remote object and hide the complexity of remote communication.

Different types of proxies

  1. Remote Proxy is used when objects are located in a different address space, such as a remote server. In this case, the proxy object serves as a local representative for the remote object.
  2. Virtual Proxy is used to create expensive objects on demand. In this scenario, the proxy creates a virtual representation of the real object and only creates the real object when it is actually needed.
  3. Protection Proxy is used to control access to an object. In this case, the proxy checks if the client has the necessary permissions to access the real object and only allows access if the client is authorized.
  4. Smart Proxy is used to add additional functionality to an object, such as caching (storing the results for reuse in the future) or logging (logging the use of an object for debugging purposes).

A sample use case of proxy pattern

Problem: Suppose we have a user service that returns a list of users from a database, but the service is slow and we want to improve its performance. Constraint: We cannot modify the user service code directly.

Solution: We can use the proxy pattern to create a new class that acts as a proxy for the user service. This proxy will intercept client requests to retrieve the user list and return a cached version if it exists. Here’s an example implementation:

// Interface for the user service
public interface UserService {
    List<User> getUsers();
}

// Implementation of the user service
class UserServiceImpl implements UserService {
    public List<User> getUsers() {
        // Code to retrieve user list from database
    }
}

// Proxy implementation of the user service
public class UserServiceProxy implements UserService {
    private UserService userService;
    private List<User> cachedUsers;

    public List<User> getUsers() {
        if (userService == null) {
            userService = new UserServiceImpl();
            cachedUsers = userService.getUsers();
        }

        return cachedUsers;
    }
}

// Example usage of the proxy implementation
public class Main {
    public static void main(String[] args) {
        UserService proxy = new UserServiceProxy();

        // First call retrieves the user list from the real service and caches it
        List<User> users = proxy.getUsers();

        // Subsequent calls retrieve the cached user list
        List<User> cachedUsers = proxy.getUsers();
    }
}

In the above example, UserServiceImpl class is the real implementation of the UserService interface, which contains the code to retrieve the user list from the database. UserServiceProxy class is a proxy and it intercepts calls to getUsers().

If the user list has not already been retrieved, it delegates the call to the UserServiceImpl implementation and caches the result. After this, subsequent calls to getUsers() return the cached user list instead of going to the real service. This can improve performance by a huge margin.

Best practices

  • Use an interface or abstract class to define an interface between the real object and the proxy. This will make it easier to swap out different types of proxies based on demand.
  • Proxy can add extra latency and overhead. So the best idea would be to use lazy loading to defer the creation or retrieval of the real object.
  • Use a proper caching strategy to improve performance.
  • For easier maintenance, avoid unnecessary functionalities and keep the proxy code as simple as possible.
  • Use dependency injection to manage the creation of proxies. This can help to reduce coupling.
  • Use error handling and logging strategies to diagnose issues that may arise with the proxy.

Relation with other design patterns

  • Adapter: A proxy can act as an adapter between a client and a real object, allowing the client to use the real object in a way that it wasn’t originally designed for. This is similar to how Adapter Pattern allows objects with incompatible interfaces to work together.
  • Decorator: Decorator and Proxy have different purposes but similar structures. A proxy can act as a decorator by adding behavior to the real object, either before or after it is called. This is similar to how the Decorator Pattern adds behavior to an object at runtime.
  • Factory: The factory pattern can be used to create instances of a proxy object dynamically, based on the needs of the system. This can be useful when you want to create different types of proxy objects depending on the configuration of the system.
  • We can use asynchronous programming techniques (callbacks or promises) with proxies to improve performance and responsiveness. Here we use a proxy to represent a long-running or resource-intensive operation and return control to the client code immediately, while the operation continues in the background. When the operation is complete, proxy will notify the client and return the result.

Please write in the message below if you find anything incorrect, or if you want to share more insight. Enjoy learning, OOPS!

More from EnjoyAlgorithms

Self-paced Courses and Blogs