User Professor Hans Noodles
Professor Hans Noodles
Level 41

Proxy design pattern

Published in the Java Developer group
6640 members
In programming, it is important to plan your application's architecture correctly. Design patterns are an indispensable way to accomplish this. Today let's talk about proxies. Proxy design pattern: - 1

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:
  1. TrainTimetable interface.
  2. ElectricTrainTimetable, which implements this interface.
  3. The client code interacts with the file system through this class.
  4. TimetableDisplay client class. Its printTimetable() method uses the methods of the ElectricTrainTimetable class.
The diagram is simple: Proxy design pattern: - 2At present, with each call of the 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: Proxy design pattern: - 3Thus, 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:
  1. Caching
  2. Delayed, or lazy, initialization Why load an object right away if you can load it as needed?
  3. Logging requests
  4. Intermediate verification of data and access
  5. Starting worker threads
  6. Recording access to an object
And there are also other use cases. Understanding the principle behind this pattern, you can identify situations where it can be applied successfully. At first glance, a proxy does the same thing as a facade, but that's not the case. A proxy has the same interface as the service object. Also, don't confuse this pattern with the decorator or Adapter patterns. A decorator provides an extended interface, and an adapter provides an alternative interface.

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:
  1. Define an interface that allows the use of a proxy instead of the original object. In our example, this is TrainTimetable.

  2. 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.

  3. 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.

  4. 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.

Proxy design pattern: - 4

Before we say goodbye, here is a helpful link

That's all for today! It wouldn't be a bad idea to return to the lessons and try out your new knowledge in practice :)
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION