Why do you need a proxy?
This pattern helps solve problems associated with controlled access to an object. You may ask, "Why do we need controlled access?" Let's look at a couple of situations that will help you figure out what's what.Example 1
Imagine that we have a large project with a bunch of old code, where there is a class responsible for exporting reports from a database. The class works synchronously. That is, the whole system is idle while the database processes the request. On average, it takes 30 minutes to generate a report. Accordingly, the export process starts at 12:30 AM, and management receives the report in the morning. An audit revealed that it would be better to be able to immediately receive the report during normal business hours. The start time cannot be postponed, and the system cannot block while it waits for a response from the database. The solution is to change how the system works, generating and exporting the report on a separate thread. This solution will let the system work as usual, and management will receive fresh reports. However, there is a problem: the current code cannot be rewritten, since other parts of the system use its functionality. In this case, we can use the proxy pattern to introduce an intermediate proxy class that will receive requests to export reports, log the start time, and launch a separate thread. Once the report is generated, the thread terminates and everyone is happy.Example 2
A development team is creating an events website. To get data on new events, the team queries a third-party service. A special private library facilitates interaction with the service. During development, a problem is discovered: the third-party system updates its data once a day, but a request is sent to it every time a user refreshes a page. This creates a large number of requests, and the service stops responding. The solution is to cache the service's response and return the cached result to visitors as pages are reloaded, updating the cache as needed. In this case, the proxy design pattern is an excellent solution that does not change the existing functionality.The principle behind the design pattern
To implement this pattern, you need to create a proxy class. It implements the interface of the service class, mimicking its behavior for client code. In this manner, the client interacts with a proxy instead of the real object. As a rule, all requests are passed on to the service class, but with additional actions before or after. Simply put, a proxy is a layer between client code and the target object. Consider the example of caching query results from an old and very slow hard disk. Suppose we're talking about a timetable for electric trains in some ancient app whose logic cannot be changed. A disk with an updated timetable is inserted every day at a fixed time. So, we have:TrainTimetable
interface.ElectricTrainTimetable
, which implements this interface.- The client code interacts with the file system through this class.
TimetableDisplay
client class. ItsprintTimetable()
method uses the methods of theElectricTrainTimetable
class.
printTimetable()
method, the ElectricTrainTimetable
class accesses the disk, loads the data, and presents it to the client. The system functions okay, but it is very slow. As a result, the decision was made to increase system performance by adding a caching mechanism.
This can be done using the proxy pattern:
Thus, the TimetableDisplay
class does not even notice that it is interacting with the ElectricTrainTimetableProxy
class instead of the old class.
The new implementation loads the timetable once a day. For repeat requests, it returns the previously loaded object from memory.
What tasks are best for a proxy?
Here are a few situations where this pattern will definitely come in handy:- Caching
- Delayed, or lazy, initialization Why load an object right away if you can load it as needed?
- Logging requests
- Intermediate verification of data and access
- Starting worker threads
- Recording access to an object
Advantages and disadvantages
- + You can control access to the service object however you wish
- + Additional abilities related to managing the life cycle of the service object
- + It works without a service object
- + It improves performance and code security.
- - There is a risk that performance may get worse due to additional requests
- - It makes the class hierarchy more complicated
The proxy pattern in practice
Let's implement a system that reads train timetables from a hard disk:
public interface TrainTimetable {
String[] getTimetable();
String getTrainDepartureTime();
}
Here's the class that implements the main interface:
public class ElectricTrainTimetable implements TrainTimetable {
@Override
public String[] getTimetable() {
ArrayList<String> list = new ArrayList<>();
try {
Scanner scanner = new Scanner(new FileReader(new File("/tmp/electric_trains.csv")));
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
list.add(line);
}
} catch (IOException e) {
System.err.println("Error: " + e);
}
return list.toArray(new String[list.size()]);
}
@Override
public String getTrainDepartureTime(String trainId) {
String[] timetable = getTimetable();
for (int i = 0; i < timetable.length; i++) {
if (timetable[i].startsWith(trainId+";")) return timetable[i];
}
return "";
}
}
Each time you get the train timetable, the program reads a file from disk. But that's only the beginning of our troubles. The entire file is read every time you get the timetable for even a single train! It's good that such code exists only in examples of what not to do :) Client class:
public class TimetableDisplay {
private TrainTimetable trainTimetable = new ElectricTrainTimetable();
public void printTimetable() {
String[] timetable = trainTimetable.getTimetable();
String[] tmpArr;
System.out.println("Train\\tFrom\\tTo\\t\\tDeparture time\\tArrival time\\tTravel time");
for (int i = 0; i < timetable.length; i++) {
tmpArr = timetable[i].split(";");
System.out.printf("%s\t%s\t%s\t\t%s\t\t\t\t%s\t\t\t%s\n", tmpArr[0], tmpArr[1], tmpArr[2], tmpArr[3], tmpArr[4], tmpArr[5]);
}
}
}
Example file:
9B-6854;London;Prague;13:43;21:15;07:32
BA-1404;Paris;Graz;14:25;21:25;07:00
9B-8710;Prague;Vienna;04:48;08:49;04:01;
9B-8122;Prague;Graz;04:48;08:49;04:01
Let's test it:
public static void main(String[] args) {
TimetableDisplay timetableDisplay = new timetableDisplay();
timetableDisplay.printTimetable();
}
Output:
Train From To Departure time Arrival time Travel time
9B-6854 London Prague 13:43 21:15 07:32
BA-1404 Paris Graz 14:25 21:25 07:00
9B-8710 Prague Vienna 04:48 08:49 04:01
9B-8122 Prague Graz 04:48 08:49 04:01
Now let's walk through the steps required to introduce our pattern:
Define an interface that allows the use of a proxy instead of the original object. In our example, this is
TrainTimetable
.Create the proxy class. It should have a reference to the service object (create it in the class or pass to the constructor).
Here's our proxy class:
public class ElectricTrainTimetableProxy implements TrainTimetable { // Reference to the original object private TrainTimetable trainTimetable = new ElectricTrainTimetable(); private String[] timetableCache = null @Override public String[] getTimetable() { return trainTimetable.getTimetable(); } @Override public String getTrainDepartureTime(String trainId) { return trainTimetable.getTrainDepartureTime(trainId); } public void clearCache() { trainTimetable = null; } }
At this stage, we are simply creating a class with a reference to the original object and forwarding all calls to it.
Let's implement the logic of the proxy class. Basically, calls are always redirected to the original object.
public class ElectricTrainTimetableProxy implements TrainTimetable { // Reference to the original object private TrainTimetable trainTimetable = new ElectricTrainTimetable(); private String[] timetableCache = null @Override public String[] getTimetable() { if (timetableCache == null) { timetableCache = trainTimetable.getTimetable(); } return timetableCache; } @Override public String getTrainDepartureTime(String trainId) { if (timetableCache == null) { timetableCache = trainTimetable.getTimetable(); } for (int i = 0; i < timetableCache.length; i++) { if (timetableCache[i].startsWith(trainId+";")) return timetableCache[i]; } return ""; } public void clearCache() { trainTimetable = null; } }
The
getTimetable()
checks whether the timetable array has been cached in memory. If not, it sends a request to load the data from disk and saves the result. If the timetable has already been requested, it quickly return the object from memory.Thanks to its simple functionality, the getTrainDepartureTime() method did not have to be redirected to the original object. We simply duplicated its functionality in a new method.
Don't do this. If you have to duplicate the code or do something similar, then something went wrong, and you need to look at the problem again from a different angle. In our simple example, we had no other option. But in real projects, the code will most likely be written more correctly.
In the client code, create a proxy object instead of the original object:
public class TimetableDisplay { // Changed reference private TrainTimetable trainTimetable = new ElectricTrainTimetableProxy(); public void printTimetable() { String[] timetable = trainTimetable.getTimetable(); String[] tmpArr; System.out.println("Train\\tFrom\\tTo\\t\\tDeparture time\\tArrival time\\tTravel time"); for (int i = 0; i < timetable.length; i++) { tmpArr = timetable[i].split(";"); System.out.printf("%s\t%s\t%s\t\t%s\t\t\t\t%s\t\t\t%s\n", tmpArr[0], tmpArr[1], tmpArr[2], tmpArr[3], tmpArr[4], tmpArr[5]); } } }
Check
Train From To Departure time Arrival time Travel time 9B-6854 London Prague 13:43 21:15 07:32 BA-1404 Paris Graz 14:25 21:25 07:00 9B-8710 Prague Vienna 04:48 08:49 04:01 9B-8122 Prague Graz 04:48 08:49 04:01
Great, it works correctly.
You could also consider the option of a factory that creates both an original object and a proxy object, depending on certain conditions.
GO TO FULL VERSION