The proxy design pattern

In some applications, developers could face the need to provide access control to objects. This could be due to many reasons. Some of them include hiding implementation details, improving interaction with expensive resources, interfacing with remote resources, caching, providing lazy or eager initialization, and so on. The proxy design pattern helps to achieve these.

Note

Its purpose is to provide an interface to something else that then gets served behind the scenes to the user.

The proxy design pattern is another example of a wrapper. It is pretty similar to the decorator design pattern, but feels more basic and limited. The reason for this is that the relationship between the proxy and the wrapped object is established during compile time and decorators could be applied at runtime. In the end, its purpose is different.

Class diagram

For the class diagram, let's imagine that we have an application that visualizes text from files. It might need to visualize the text, or might not depending on user actions. These files could be enormous or could be somewhere on a remote location. Here is how the proxy design pattern could help us achieve this:

Class diagram

According to the preceding diagram, we could use the FileReaderProxy objects and only when someone needs to access the file contents, we will delegate the functionality to the FileReaderReal. This design is nice and convenient because we can actually use the FileReader object; however, we can keep our application efficient by not needing to load everything at the same time but just once when needed.

Code example

Let's now have a closer look at the code that implements the preceding class diagram. The first thing we need to define is the interface (using a Scala trait):

trait FileReader {
  def readFileContents(): String
}

We then create two classes that implement it: FileReaderReal and FileReaderProxy. Let's first see how the former implements the file read as it has nothing of real significance:

class FileReaderReal(filename: String) extends FileReader {
  val contents = {
    val stream = this.getClass.getResourceAsStream(filename) 
    val reader = new BufferedReader(
      new InputStreamReader(
        stream
      )
    )
    try {
      reader.lines().iterator().asScala.mkString(System.getProperty("line.separator"))
    } finally {
      reader.close()
      stream.close()
    }
  }
  
  System.out.println(s"Finished reading the actual file: $filename")
  
  override def readFileContents(): String = contents
}

During the construction of the object, it will get the file, read it and store it in the contents variable. Then whenever readFileContents is called, the class will return whatever it has buffered. Let's now take a look at the FileReaderProxy implementation:

class FileReaderProxy(filename: String) extends FileReader {
  private var fileReader: FileReaderReal = null
  
  override def readFileContents(): String = {
    if (fileReader == null) {
      fileReader = new FileReaderReal(filename)
    }
    fileReader.readFileContents()
  }
}

The implementation contains an instance of FileReaderReal, which is created the first time readFileContents is called. The actual file read is then delegated to the FileReaderReal class.

Note

A more elegant implementation of the FileReaderProxy would use a lazy val instead of a mutable variable. In such a case the if statement won't be needed anymore.

Let's now see how our proxy can be used in an application:

object ProxyExample {
  def main(args: Array[String]): Unit = {
    val fileMap = Map(
      "file1.txt" -> new FileReaderProxy("file1.txt"),
      "file2.txt" -> new FileReaderProxy("file2.txt"),
      "file3.txt" -> new FileReaderProxy("file3.txt"),
      "file4.txt" -> new FileReaderReal("file1.txt")
    )
    System.out.println("Created the map. You should have seen file1.txt read because it wasn't used in a proxy.")
    System.out.println(s"Reading file1.txt from the proxy: ${fileMap("file1.txt").readFileContents()}")
    System.out.println(s"Reading file3.txt from the proxy: ${fileMap("file3.txt").readFileContents()}")
  }
}

It's worth noting that each file is actually a resource in the application and contains a line of text in the form of I am file x. After running the preceding example, we will get the following output:

Code example

As you can see from the preceding screenshot , the real object is lazily created and so the actual file read is done on demand. This causes our application to skip reading file2.txt because we don't even request for it. Someone might come up with a different solution that serves the same purpose but it will probably be a different design pattern or something similar to proxy.

What is it good for?

The proxy design pattern is good when we want to delegate some expensive operations to other classes, do operations lazily, and thus make our applications more efficient.

What is it not so good for?

The proxy design pattern is pretty simple and really, there are no drawbacks that could be mentioned. As with every other design pattern, they should be used carefully and only when actually needed.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.167.195