In this article I’ll write about the Proxy Design Pattern. This pattern is used to control access to resources and objects. The real value of this pattern is to reduce memory costs for objects until you really need them.
About the Proxy Design pattern
Let’s see what the Gang of Four (GoF) says about this pattern:
“Allows for object level access control by acting as a pass through entity or a placeholder object.”
So this makes sense and as in the introduction already mentioned: it has a very good use to reduce resource cost. For example you load something from your database, a huge object, and don’t want to initialize all its properties at once only on demand. In this case you create a proxy object which is later “de-proxied” when needed. With this approach you save some performance. If you are familiar with JPA and/or Hibernate you may know that this is an approach persistence provider use to save some memory when doing lazy fetching.
In practice the client gets the proxy object when calling for something. If the client needs details it calls the right methods on the proxy object which then delegates to the target.
Example
Today’s example is a very simple one. We will create a simple library where we want to load objects containing a big text from the database (BLOB or CLOB for relational database users) or the file system. This is interesting because it can consume memory to have these large texts in memory — and to load them from an external source too.
The easiest way to support compatibility between the proxy and the real target is to have a common interface between them which enables the client to know which methods it can use and to hide that it has a proxy instead of a real object.
We create an interface for the object, let’s call it Book:
/**
* The common interface between the Proxy and the Real object.
*
* @author GHajba
*
*/
public interface Book {
String getTitle();
String getAuthor();
String getContent();
}
Now we need our real implementation of the book. This implementation takes care of loading the big data at construction time.
import java.text.MessageFormat;
/**
* A simple implementation of our real book which loads its contents when constructed.
*
* @author GHajba
*
*/
public class RealBook implements Book {
private final String title;
private final String author;
private String content;
public RealBook(final String title, final String author) {
this.title = title;
this.author = author;
loadContentFromDatabase(title, author);
}
@Override
public String getTitle() {
return title;
}
@Override
public String getAuthor() {
return author;
}
@Override
public String getContent() {
return content;
}
private void loadContentFromDatabase(final String title, final String author) {
System.out.println("Loading content from database...");
try {
Thread.sleep(2500);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.content = MessageFormat.format("Interesting and very large content of {0} by {1}", title, author);
}
}
The sleep in the loadContent method is for simulate the time elapsed when loading the example.
To avoid loading every time we create a proxy object: this has almost the same information than the real book but the proxy only loads the content on-demand.
/**
* Simple proxy implementation which only loads the content when it is needed. Until that it handles the available
* information as it were the real book.
*
* @author GHajba
*
*/
public class ProxyBook implements Book {
private final String title;
private final String author;
private RealBook realBook;
public ProxyBook(final String title, final String author) {
this.title = title;
this.author = author;
}
@Override
public String getTitle() {
return title;
}
@Override
public String getAuthor() {
return author;
}
@Override
public String getContent() {
if (realBook == null) {
realBook = new RealBook(title, author);
}
return realBook.getContent();
}
}
And finally let’s create a LibraryService where you can look-up books by title and author.
import static java.util.stream.Collectors.toMap;
import java.util.Map;
import java.util.stream.Stream;
/**
* Simple service to find the right books and load their content.
*
* And this is the main entry point of the application too.
*
* @author GHajba
*
*/
public class LibraryService {
public static void main(final String... args) {
new LibraryService().findContentByAuthorAndTitle("GoF", "Design Patterns").entrySet()
.forEach(b -> System.out.println(b.getKey() + " --> " + b.getValue()));
}
/**
* Finds the right book for our test purposes and then loads its content.
*/
public Map<String, String> findContentByAuthorAndTitle(final String author, final String title) {
return filterByTitle(filterByAuthor(findAllBooks(), author), title)
.collect(toMap(Book::getTitle, Book::getContent));
}
/*
* The following methods are really simple and I think straightforward too.
*/
private Stream<Book> findAllBooks() {
return Stream.of(new ProxyBook("Design Patterns", "Gabor Laszlo Hajba"),
new ProxyBook("Design Patterns", "GoF"), new ProxyBook("Python 3 in Anger", "Gabor Laszlo Hajba"),
new ProxyBook("Design Patterns", "Head First"));
}
private Stream<Book> filterByAuthor(final Stream<Book> books, final String author) {
return books.filter(b -> author.equals(b.getAuthor()));
}
private Stream<Book> filterByTitle(final Stream<Book> books, final String title) {
return books.filter(b -> title.equals(b.getTitle()));
}
}
Running the result we get the following written to the console:
Loading content from database…
Design Patterns –> Interesting and very large content of Design Patterns by GoF
As you can see in the example above, loading the whole book contents happens only when it is needed. If we aren’t interested in a given book it’s content is not loaded.
Conclusion
The Proxy Design Pattern is very useful to leverage loading and memory overhead until that moment where you really need the cause of the overhead.