The Proxy Design Pattern is a design pattern in Java that is used to provide a surrogate or placeholder for another object to control access to it. The proxy acts as an intermediary between the client and the actual object, allowing the proxy to control access to the object and provide additional functionality as needed. This pattern is particularly useful for managing the creation and access to objects in a distributed system, as it can provide a layer of abstraction to insulate the client from the complexities of the system.
Types of Proxy Design Pattern
There are several types of proxy design pattern in Java, including:
- Remote Proxy: This type of proxy is used when the object being proxied is located on a remote server, such as in a distributed system. The remote proxy acts as a local representation of the object, allowing the client to access it without needing to be aware of its location.
- Virtual Proxy: This type of proxy is used when the object being proxied is too resource-intensive to create or is only needed on demand. The virtual proxy creates the object only when it is needed, and provides a placeholder in the meantime.
- Protection Proxy: This type of proxy is used when access to the object being proxied needs to be controlled or monitored. The protection proxy can enforce access controls, such as requiring authentication or limiting access to specific users.
- Smart Proxy: This type of proxy is used when the proxy needs to perform additional functionality beyond simply controlling access to the object. The smart proxy can perform tasks such as caching, logging, or optimizing access to the object.
Example of Proxy Design Pattern in Java
Suppose we have a remote service that returns some data from a database. The service can be expensive to call and can take some time to respond. To avoid calling the service unnecessarily, we can use a proxy to cache the response and return it if the same request is made again.
Here’s an implementation of the remote proxy in Java:
interface DataService {
String fetchData(String query);
}
class RealDataService implements DataService {
@Override
public String fetchData(String query) {
// Perform the expensive operation to fetch data from the database
System.out.println("Executing query: " + query);
try {
Thread.sleep(5000); // Simulating a delay in fetching data
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Data for query: " + query;
}
}
class CachedDataServiceProxy implements DataService {
private RealDataService realDataService;
private Map cache;
@Override
public String fetchData(String query) {
if (realDataService == null) {
realDataService = new RealDataService();
}
if (cache == null) {
cache = new HashMap<>();
}
if (cache.containsKey(query)) {
System.out.println("Returning cached data for query: " + query);
return cache.get(query);
} else {
String data = realDataService.fetchData(query);
cache.put(query, data);
return data;
}
}
}
public class Main {
public static void main(String[] args) {
DataService dataService = new CachedDataServiceProxy();
System.out.println(dataService.fetchData("SELECT * FROM table1"));
System.out.println(dataService.fetchData("SELECT * FROM table2"));
System.out.println(dataService.fetchData("SELECT * FROM table1"));
}
}
In this example, we have defined the DataService interface, which includes a single method fetchData. We have then created the RealDataService class that implements the DataService interface and contains the actual implementation of the fetchData method.
We have also created the CachedDataServiceProxy class that also implements the DataService interface. This class creates a placeholder object that does not actually execute the query, but instead checks if the result is already cached. If the result is cached, it is returned immediately, otherwise the query is executed by the RealDataService object, and the result is stored in the cache for future use.
Finally, in the Main class, we have created an instance of the CachedDataServiceProxy object and called the fetchData method on it multiple times with different queries. The first two calls will execute the query and cache the result, while the third call will return the cached result immediately without executing the query again.
Practical use case of Proxy Design Pattern
The Proxy Design Pattern is a powerful tool for software engineers that can be applied to solve a variety of design problems. Here are some common real-world use cases for the Proxy Design Pattern:
- Remote service access: A common use case for the Proxy Design Pattern is to handle remote service access. In such a scenario, the Proxy acts as a wrapper for a remote object or service. Instead of interacting with the remote service directly, the client interacts with the Proxy. The Proxy then handles the network communication and other details needed to interact with the remote service.
- Caching: Proxies can be used to cache expensive or resource-intensive operations. When a client requests data or resources from a system, the Proxy checks to see if the data or resources are already cached. If they are, the Proxy returns the cached data to the client. Otherwise, the Proxy executes the expensive operation and stores the result in the cache for future requests.
- Access control: Proxies can be used to control access to sensitive or secure resources. For example, a Proxy could restrict access to a database or file system, only allowing authorized clients to access the resources.
- Lazy initialization: Proxies can also be used to defer the creation of expensive objects or resources until they are actually needed. The Proxy acts as a placeholder for the actual object and only creates the object when it is actually needed by the client.
- Performance optimization: Proxies can be used to optimize the performance of a system by reducing the number of expensive operations. For example, a Proxy could be used to cache the results of frequently used calculations, eliminating the need to perform the calculation multiple times.
In summary, the Proxy Design Pattern is a versatile pattern that can be used to solve a variety of design problems, including remote service access, caching, access control, lazy initialization, and performance optimization.