Understanding the Proxy Design Pattern
There are 23 classic design patterns described in the original book Design Patterns: Elements of Reusable Object-Oriented Software. These patterns provide solutions to particular problems often repeated in software development.
In this article, I am going to describe how the Flyweight pattern works and when it should be applied.
Proxy: Basic Idea
The first thing we will do, as with the other design patterns, is to look at the definition offered by the book “Design Patterns” by the Gang of Four:
Provide a surrogate or placeholder for another object to control access to it.
A proxy can perform additional operations (such as controlled access, lazy loading, etc.) before accessing the real object. The most common uses of the proxy pattern according to its purpose are as follows:
- Virtual Proxy: controls access to resources that are expensive to create, such as objects that require a lot of memory or processing time. It is used for lazy loading of an object.
- Protection Proxy: controls access to an object by providing different permissions to different users.
- Remote Proxy: allows access to an object that resides in a different address space. For example, this proxy could handle all the necessary communication between a client and a server on different machines.
As we have done in the entire series of pattern videos, let’s start by looking at the UML class diagram to understand each of the elements of it.
- Subject: Defines a common interface for
RealSubject
andProxy
so thatProxy
can be used in place ofRealSubject
. - RealSubject: The real object that the proxy represents.
- Proxy: Maintains a reference that allows the proxy to access the
RealSubject
. Provides an identical interface to that ofSubject
so it can substituteRealSubject
. Controls access toRealSubject
and can be responsible for its creation and deletion.
Now that we have a clear understanding of the basic structure, let’s see when this pattern is useful to us.
Proxy Pattern: When to Use
It The Proxy pattern is an excellent solution when we need to add a level of control or intermediation between the client and the real object. Here are some common situations where using the Proxy pattern can be beneficial:
- Monitoring and Logging: If we need to record or monitor access to an object, a
Proxy
can intercept calls and add additional functionality, such as logging, without modifying the real object. This is useful for debugging, auditing, and usage analysis. - Access Control: When we need to control access to an object, such as restricting who can interact with it or under what conditions. For example, in a system with different permission levels, a Protection Proxy can ensure that only authorized users can perform certain operations.
- Lazy Loading: If creating an object is costly in terms of resources and time, a Virtual Proxy can defer the creation of the object until it is really necessary. This is useful for optimizing performance and reducing resource consumption.
- Remote Access: In distributed applications where objects reside on different machines or processes, a Remote Proxy can handle all communication and data transfer, providing a local interface to the remote object. This simplifies interaction and hides the complexity of network communication.
- Resource Optimization: When handling heavy objects or costly resources, a
Proxy
can manage the object’s lifecycle, such as creation and destruction, to optimize the use of system resources.
In summary, the Proxy pattern is useful in situations where additional control is needed, resource usage needs to be optimized, or interaction with remote objects needs to be facilitated. Now, let’s look at some examples of this pattern in application.
Proxy Pattern: Examples
Next, we will illustrate the proxy pattern with several useful use cases:
- Basic Structure of the Proxy Pattern: In this example, we will translate the theoretical UML diagram into TypeScript code to identify each of the classes involved in the pattern.
- From the basic schema, we will create different types of proxies to understand many of the uses, so we will implement the logger proxy, security proxy, lazy loading proxy, remote access proxy, and caching proxy.
Example 1: Basic Structure of the Proxy Pattern
As always, the starting point is the UML class diagram to define each of the elements. This first class diagram is the same as the one we explained at the beginning of the post, and we won’t repeat the explanation here to avoid making the post too long.
However, what we will do is review the implementation of it using the TypeScript programming language.
Let’s start with the Subject interface. This interface defines the contract that both the RealSubject
and the Proxy
must fulfill. This is evident since the proxy will intercept the RealSubject
and must comply with the same interface.
interface Subject {
operation(): void;
}
On one hand, the RealSubject
class only implements the methods to fulfill the Subject
interface.
class RealSubject implements Subject {
operation(): void {
console.log('RealSubject: Handling operation.');
}
}
On the other hand, we see that the Proxy
class has an instance of RealSubject
through composition, and in this example, we will accomplish this through the constructor. Although we could have used an accessor method to perform this composition. If we focus on the operation
method, which is the one we need to implement to satisfy the Subject
interface, we are simply invoking the operation
method of the Subject
instance. Obviously, as we are showing it, we are doing absolutely NOTHING, but it is precisely here where we would have the interception of the real method, and where we would implement the logic that we want the proxy to perform.
class Proxy implements Subject {
private realSubject: RealSubject;
constructor(realSubject: RealSubject) {
this.realSubject = realSubject;
}
operation(): void {
// Different actions can be performed here
this.realSubject.operation();
}
}
Lastly, we would have the client that would interact directly with the Proxy
and not with the RealSubject
. However, to illustrate the difference in the client, it would be that initially, the client would interact directly with RealSubject
, but when we introduce the Proxy pattern, we would switch to interacting with the Proxy
class instead of the RealSubject
class.
console.log('Client: Executing the client code with a real subject:');
const realSubject = new RealSubject();
realSubject.operation()
console.log('');
console.log('Client: Executing the same client code with a proxy:');
const proxy = new Proxy(realSubject);
proxy.operation();
Example 2: Different Types of Proxies
In this example, we are going to create five different types of proxies to cover various use cases where the Proxy pattern is useful. Our examples will be simple but will give us the intent of the pattern, which is what really matters: having a recurring problem and how the pattern can help us solve it.
The UML class diagram for this proposal would be as follows:
We start by seeing that we have the Subject
interface and the RealSubject
class, which remain exactly the same as in the previous case, an interface that defines the contract that both RealSubject
and the proxies must fulfill.
On the other hand, we have a package with different Proxies
. In our case, we start with the LoggingProxy
class, which will act to monitor access to a subject, LazyProxy
which will delegate the creation of the subject object until it is requested, RemoteProxy
which will load objects from an external and remote environment such as a backend, and AccessProxy
which focuses on validating that a user can access the object.
On the other hand, we have a different Subject
and RealSubject
, which we have called SubjectCache
and RealSubjectCache
because the interface is different. In this case, we will have a request
operation that receives a parameter, and therefore the object to which we apply the proxy will be RealSubjectCache
. Finally, we have the proxy called CacheProxy
, which will store the subjects that are requested from an external resource, and if this subject has already been previously loaded, it will be stored to speed up its loading.
So, let’s start with the implementation of these proxies.
Logging Proxy
Let’s quickly review the Subject
interface and the RealSubject
to check that we still have a very simple interface, where a single operation called operation
is defined, and as the RealSubject
method implements this interface, we are just showing a trace message to know that we have passed through the RealSubject
.
export interface Subject {
operation(): void;
}
import { Subject } from "./subject.interface";
export class RealSubject implements Subject {
operation(): void {
console.log('RealSubject: Handling operation.');
}
}
Now let’s start by looking at the first proxy, and the simplest of all we will see, this would be the LoggingProxy
. This proxy is exactly the same as the book example (Gang of Four), but now in the operation
method, we have added a private method called logAccess
which would store or communicate to the monitoring system that the proxy has been accessed.
In our particular case, we will just show on the screen that we have accessed it, and the next thing the proxy would do is invoke the RealSubject
method. That is, this proxy would only add extra functionality before executing the RealSubject
method.
import { RealSubject, Subject } from "../subjects/";
export class LoggingProxy implements Subject {
private realSubject: RealSubject;
constructor(realSubject: RealSubject) {
this.realSubject = realSubject;
}
operation(): void {
this.logAccess();
this.realSubject.operation();
}
private logAccess(): void {
console.log('LoggingProxy: Logging access to RealSubject.');
}
}
Access Proxy
The next proxy we address is AccessProxy
, this proxy focuses on controlling which users or other elements with permission can access the RealSubject
. For this example, the proxy stores a new private property that is the user’s role, which we communicate to the Proxy
through the constructor.
If we now look at the operation
method, we see that we have a guard clause. In this case, if the checkAccess
is not satisfied, the method will automatically return a message indicating that access to the resource is not allowed. Otherwise, we can execute the RealSubject's
operation method.
If we look at the private checkAccess
method, there is the access logic to the RealSubject
, in our case, it simply means that the user’s role should be admin.
import { RealSubject, Subject } from "../subjects/";
export class AccessProxy implements Subject {
private realSubject: RealSubject;
private userRole: string;
constructor(realSubject: RealSubject, userRole: string) {
this.realSubject = realSubject;
this.userRole = userRole;
}
operation(): void {
if (!this.checkAccess()) {
return console.log('AccessProxy: Access denied.');
}
this.realSubject.operation();
}
private checkAccess(): boolean {
console.log(`AccessProxy: Checking access for role ${this.userRole}.`);
return this.userRole === 'admin';
}
}
Lazy Proxy
Very similar in implementation that AccessProxy
but with a very different objective, we have the LazyProxy
. In this case, we will use the proxy to delay the creation of the RealSubject
until the moment the operation
method is executed.
First, we notice that this proxy does not receive the RealSubject
through the constructor, but it is responsible for instantiating it in the operation
method.
The first thing it does is check if the instance has a value other than null
, in which case it is instantiated, and then the subject’s operation
method is invoked.
import { RealSubject, Subject } from "../subjects/";
export class LazyProxy implements Subject {
private realSubject: RealSubject | null = null;
operation(): void {
if (this.realSubject === null) {
this.realSubject = new RealSubject();
}
this.realSubject.operation();
}
}
Remote Proxy
The next proxy is a bit more complex than the previous one but follows the same idea. We are talking about the remote proxy.
In this proxy, if we look at the operation
method, we would have a solution very similar to the previous one, but now instead of directly creating the RealSubject
if the instance does not exist in the proxy, we invoke a method that will remotely connect to retrieve the RealSubject
. The connectToRemote
method simulates an HTTP request or an external resource. In this case, we connect to the URL passed as an argument to the fetch
method. As we know, in JavaScript, this method returns a Promise
, which we can wait for and retrieve its result from the external resource with the reserved word await
. We then only check that the request to the external resource did not respond with the value ok
, and in that case, we return an error
, using a guard clause again.
We then simulate a small delay in the connection using promises and setTimeout
. This code will not exist in production code because here we are only delaying execution for one second, to finally create the RealSubject
. In a real scenario, the RealSubject
would be constructed from the information retrieved from the requested HTTP resource.
import { RealSubject, Subject } from "../subjects/";
export class RemoteProxy implements Subject {
private realSubject: RealSubject | null = null;
private readonly remoteServerUrl: string = 'https://jsonplaceholder.typicode.com/posts/1';
async operation(): Promise<void> {
if (this.realSubject === null) {
await this.connectToRemote();
}
if (this.realSubject) {
this.realSubject.operation();
}
}
private async connectToRemote(): Promise<void> {
console.log('RemoteProxy: Connecting to remote server...');
// Simulate HTTP request to connect to remote server
const response = await fetch(this.remoteServerUrl);
if (!response.ok) {
return console.error('RemoteProxy: Failed to connect to remote server.');
}
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('RemoteProxy: Connected to remote server.');
this.realSubject = new RealSubject();
}
}
Cache Proxy
The last proxy example we will see is a bit more complex than the previous ones and would be building a cache of subject objects. So, the first thing we will see is the SubjectCache
and RealSubjectCache
interface, which differ from the previous one since now SubjectCache
instead of having an operation
method without parameters, we will have a request
method that receives a resource argument of type string, and this function returns a promise of strings. We already know that this method will be asynchronous.
export interface SubjectCache {
request(resource: string): Promise<string>;
}
import { SubjectCache } from './subject-cache.interface';
export class RealSubjectCache implements SubjectCache {
async request(resource: string): Promise<string> {
// Simulate fetching resource from external API
console.log(`RealSubject: Fetching resource from ${resource}`);
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 1000));
return `Response from ${resource}`;
}
}
The RealSubjectCache
class, which is the real implementation of this interface, simply simulates that a request is being made to an external resource or API. To do this, we simulate a delay of one second until we return the String
with a message from this Subject
. In this use case, we do not want to go to the server and have a latency of one second or even more, and what we want is that if the Subject
has already been queried, we do not request it again, but store it in a data structure. So, in CacheProxy
, we have an attribute called cache
, which is a Map
that contains as a key a character string, which will be the resource, and as a value another character string, which will be the string we get in response.
We could also store an object in more advanced examples. Now if we look at the request
method of the proxy, we can see that what is being done is a check through an if, where it is being checked if the cache has an entry with the resource key. If so, we are not invoking the RealSubject
but returning the value we have stored, that is, we are improving the response time, in exchange for hardware resources such as memory for having the Map
data structure.
On the other hand, if the resource has not been queried, what we will do is invoke the RealSubject
method to get a response. Here we will have the same result as if we did not have the proxy, but immediately afterward we store the returned value in the data structure, and finally, the value is returned as well.
import { RealSubjectCache, SubjectCache } from "../subjects/";
export class CacheProxy implements SubjectCache {
private realSubject: RealSubjectCache;
private cache: Map<string, string> = new Map();
constructor(realSubject: RealSubjectCache) {
this.realSubject = realSubject;
}
async request(resource: string): Promise<string> {
if (this.cache.has(resource)) {
console.log(`CacheProxy: Retrieving response from cache for ${resource}`);
return this.cache.get(resource)!;
} else {
const response = await this.realSubject.request(resource);
console.log(`CacheProxy: Caching response for ${resource}`);
this.cache.set(resource, response);
return response;
}
}
}
And so far several types of proxies that have surely given you ideas and circumstances in which you can apply the pattern. Now let’s see how this pattern relates to other design patterns
Proxy Pattern: Relationship with Other Patterns
The Proxy pattern has connections with several other design patterns, as it can complement and enhance their functionality. Some of the most relevant relationships are:
- Decorator Pattern: The Decorator pattern and the Proxy pattern share a similar structure, as both patterns wrap an object to add functionality. However, while the Decorator focuses on dynamically adding additional behaviors, the Proxy focuses on controlling access to the object.
- Adapter Pattern: Both patterns wrap another object but with different purposes. The Adapter is used to transform one interface into another, making an object compatible with an interface it otherwise could not use. The Proxy, on the other hand, controls access to the object.
- Singleton Pattern: The Proxy can use the Singleton pattern to ensure that there is only one instance of the proxy, especially in situations where centralized control of access to a shared resource is desired.
- Factory Method Pattern: A Proxy can be created using the Factory Method pattern to manage the creation of proxies in a flexible and decoupled manner. This is useful for maintaining a clean and modular code structure.
- Flyweight Pattern: A Proxy can wrap a Flyweight object, providing additional control over access to shared objects. This is useful when more granular management of Flyweight objects is needed, such as memory management or lifecycle control.
- Composite Pattern: The Proxy can work with the Composite pattern to handle hierarchical structures of objects where access to subcomponents needs to be controlled individually. This can be useful in systems where the object structure is complex and detailed access control is required.
By leveraging these relationships with other patterns, the Proxy pattern can improve the modularity, scalability, and maintainability of a system while providing additional control over the access and management of objects.
Conclusions
The Proxy pattern offers a versatile and effective solution for systems that require additional control over object access. By providing an intermediary that handles various responsibilities such as access control, lazy loading, and remote communication, the Proxy pattern can significantly enhance the system’s security, performance, and flexibility.
The most important thing about the Proxy pattern is not its specific implementation, but the ability to recognize the problem that this pattern can solve and when it can be applied. The specific implementation is not that important, as it will vary depending on the programming language used.
See this GitHub repo for the full code.