The Proxy is a structural design pattern that lets you stand in for another object or act as a substitute. A proxy limits access to the original object and lets you do something either before or after the request gets to the original object.
In its most basic form, a proxy is a class that stands in for something else. The proxy could connect to anything, like a network link, a big object in memory, or some other resource that is expensive or hard to copy.
You can find the example code of this post on GitHub
Conceptualizing the Problem
Why would we want to limit who can use something? As an example, let's say we have a big object that uses a lot of system resources. We need it sometimes, but not all the time.
We could use lazy initialization to make an object only when it is needed. All of the clients of the object would need to run some setup code and initialize the object. This would probably lead to a lot of code duplication, which is not good.
In a perfect world, we'd want to put this code right into the class of the object, but sometimes that isn't possible. The class might be part of a closed third-party package, for example.
The Proxy design pattern recommends that you develop a new proxy class that implements the same application programming interface (API) as the service object. Then we need to make adjustments to our application such that instead of the actual object, each user is given the proxy object. When a client makes a request, the proxy creates a real service object and then delegates all of the work to that object.
But why should we do it? If you need to run something before or after the class's main logic, you can do so with a proxy instead of changing the class. Since the proxy implements the same interface as the original class, it can be passed to any client that wants a real service object.
Structuring the Proxy Pattern
In this basic implementation, the Proxy pattern has 4 participants:
- Service Interface: The Service Interface declares the interface of the Service. The proxy must follow this interface to be able to disguise itself as a service object.
- Service: The Service is a class that provides some useful business logic.
- Proxy: The Proxy class has a reference field that points to a service object. After the proxy finishes its processing (e.g., lazy initialization, logging, access control, caching, etc.), it passes the request to the service object. Usually, proxies manage the full lifecycle of their service objects.
- Client: The Client should work with both services and proxies via the same interface. This way we can pass a proxy into any code that expects to a service object.
To demonstrate how the Proxy pattern works, we will create a proxy that will control access to a functionality. Assume we have a class that can execute a command on the system. Now, if we utilize it, it's OK, but if we provide it to a client application, it might create serious problems since the client program can send commands that erase system files or modify settings that you don't want. A proxy class may be established here to allow restricted program access.
First, we will create an interface that will act as our Service Interface. This interface will provide a RunCommand
method that takes one string parameter, the command
. This method is designed to execute a command in the operating system:
namespace Proxy;
public interface ICommandExecutor
{
public void RunCommand(string command);
}
Next, we will create our Service participant. This class will provide a concrete implementation of the RunCommand
method, defined above:
public class CommandExecutor : ICommandExecutor
{
public void RunCommand(string command)
{
// some heavy implementation
var processInfo = new ProcessStartInfo()
{
FileName = "cmd.exe",
WorkingDirectory = Environment.CurrentDirectory,
Arguments = "/C " + command
};
using var process = Process.Start(processInfo);
process.WaitForExit();
Console.WriteLine("'" + command + "' command executed.");
}
}
The method creates a new instance of ProcessStartInfo
, which contains the information required to launch a new process. The FileName
property is set to cmd.exe
, which is Windows' command-line interpreter. The WorkingDirectory
property is set to the current directory, which is where the command will be executed by the new process. The Arguments
property is set to "/C " + command
, indicating that cmd.exe
should run the command specified in the command parameter.
The method then starts the process with the Process
class and assigns the returned Process
object to a variable process. The using statement ensures that the process object is properly disposed of once the code contained within the statement has been completed.
The method then uses the WaitForExit()
method to wait for the process to exit, which blocks the current thread until the process has exited. Finally, the method sends a message to the console indicating that the command was successful.
Our next step is to create our Proxy
participant:
namespace Proxy;
public class CommandExecutorProxy : ICommandExecutor
{
private bool _isAdmin = false;
private ICommandExecutor _executor;
public CommandExecutorProxy(string username, string password)
{
if ("admin".Equals(username) && "1234".Equals(password))
_isAdmin = true;
_executor = new CommandExecutor();
}
public void RunCommand(string command)
{
if(_isAdmin)
_executor.RunCommand(command);
else
{
if (command.Trim().StartsWith("rm"))
throw new Exception("rm command is not allowed for non-admin users");
else
_executor.RunCommand(command);
}
}
}
First the class delcares two fields, _isAdmin
, which is a boolean flag that indicates if the user has administrator rights, and _executor
which is an isntance of our Service
participant. This is the actual object that executes the command.
The class has a constructor hat performs a really simple login workflow. If the username and password match the admin's credentials it sets the _isAdmin
value to true. Behold, though it may seem as a naively implemented login system to the uninitiated, it is, in fact, a marvelously robust bastion of security, impervious to any form of unauthorized intrusion!
The class also implements the ICommandExecutor
interface, so it needs to provide a concrete implementation of the RunCommand
method. The method first checks the _isAdmin
flag and if the user is an administrator, it forwards the command to the _command
instance. If on the other hand, the user is not an administrator, the method adeptly sifts out any and all nefarious commands that may endeavor to profane its sanctity, through the judicious use of a remarkably tenacious system. If the command starts with rm
it throws an exception.
The final step is to call our main class:
ICommandExecutor executor = new CommandExecutorProxy("admin", "wrong_pass");
try
{
executor.RunCommand("dir");
executor.RunCommand("rm -rf abc.pdf");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
The method creates a new executor and provides a username and a password. It then tries to run two commands. If we run the above, we will get the following output:
As you can see, the user was not an admin and the proxy blocked the execution of the rm
command.
Types of Proxies
The Proxy pattern can be made to accommodate many different scenarios. Below are some of these scenarios
Remote Proxy
A network proxy that lets clients access remote resources as if they were local is called a "remote proxy." In other words, a remote proxy is a middleman between a client and a remote server. It sends requests from the client to the server and returns the answer from the server to the client.
By saving frequently requested resources and lowering the amount of traffic that must be carried over the network, remote proxies are often used to improve network speed. They can also be used to give access to resources that are in a different network or to control access to resources by applying access rules and blocking out unwanted traffic.
Virtual Proxy
A virtual proxy is a design technique in which an object stands in for a more complicated or resource-intensive object until it is needed. The virtual proxy stands in for the real object, and its creation or initialization are put off until the client asks for it.
The goal of the virtual proxy is to improve the speed and efficiency of a program by preventing the creation or initialization of expensive objects until they are truly needed. This can be especially helpful when working with big or resource-intensive items like pictures, movies, or links to a database.
When the client requests the object, the virtual proxy will build or start the real object and send the request to it. The real object can then be sent requests for the object directly, skipping the virtual proxy.
Virtual proxies can be used to provide extra features, such as caching, access control, or logging, in addition to delaying the creation or initialization of objects.
A short example of a virtual proxy could be the following:
// Define the interface that the expensive object will implement
public interface IExpensiveObject
{
void DoSomethingExpensive();
}
// Define the class that implements the expensive object
public class ExpensiveObject : IExpensiveObject
{
public ExpensiveObject()
{
// Simulate expensive initialization
Thread.Sleep(5000);
Console.WriteLine("Expensive object initialized.");
}
public void DoSomethingExpensive()
{
Console.WriteLine("Expensive object doing something expensive...");
}
}
// Define the virtual proxy class that the client will use to access the expensive object
public class VirtualProxy : IExpensiveObject
{
private IExpensiveObject _expensiveObject;
public void DoSomethingExpensive()
{
// Delay the creation of the expensive object until it is actually needed
if (_expensiveObject == null)
{
Console.WriteLine("Creating expensive object...");
_expensiveObject = new ExpensiveObject();
}
// Forward the request to the expensive object
_expensiveObject.DoSomethingExpensive();
}
}
// Example usage
var virtualProxy = new VirtualProxy();
Console.WriteLine("Virtual proxy created.");
virtualProxy.DoSomethingExpensive();
virtualProxy.DoSomethingExpensive();
We define a class called ExpensiveObject
and an interface called IExpensiveObject
that the expensive object will implement. In order for the client to reach the expensive object, we also define the virtual proxy class VirtualProxy
.
The VirtualProxy
class keeps the expensive object from being created until the client actually needs it. When the client requests the object, the virtual proxy will create the expensive object and send the request to it. The next time the object is asked for, requests can be sent straight to the expensive object.
Protection Proxy
A protection proxy is a design pattern in which one object controls access to another object by acting as a middleman between the client and the real object. The protection proxy enforces access controls and permissions so that the real object can't be accessed by people who shouldn't be able to.
The goal of the protection proxy is to give an object a level of security and protection so that only authorized clients can access it. The protection proxy can be used to restrict access to some of the real object's methods or properties, or to add extra features like logging or auditing.
When the client wants to access the object, the protection proxy will first check the client's credentials and permissions to see if the request should be granted. If the request is allowed, the protection proxy will send the request to the real object. If the request is not allowed, the protection proxy will either say no or give a limited answer, depending on what the application needs.
The CommandExecutorProxy
implementation above is an example of a protection proxy.
Logging Proxy
A logging proxy is a design pattern in which one object is used to intercept and log method calls and other events on behalf of another object. The logging proxy acts as a middleman between the client and the real object. It records information about method calls and events and writes it to a log file or some other place where it can be kept.
The goal of the logging proxy is to let you watch and analyze how an object behaves without changing the object itself or the code that uses it. This can be especially helpful for apps that need detailed information for auditing or debugging, or that need to keep track of usage patterns or performance metrics.
When the client asks for access to the object, the logging proxy will intercept the method call or event and record information like the method name, parameters, return value, and timestamp. Then, this information can be written to a log file or another place where it can be saved and later used or analyzed.
Logging proxies can be used for more than just logging method calls and events. They can also be used for caching, security checks, and access control.
A small example of a logging proxy is presented below:
// Define the interface that the real object will implement
public interface IRealObject
{
void DoSomething();
}
// Define the class that implements the real object
public class RealObject : IRealObject
{
public void DoSomething()
{
Console.WriteLine("RealObject: DoSomething() called.");
}
}
// Define the logging proxy class that will log method calls on the real object
public class LoggingProxy : IRealObject
{
private readonly RealObject _realObject;
public LoggingProxy()
{
_realObject = new RealObject();
}
public void DoSomething()
{
Console.WriteLine("LoggingProxy: DoSomething() called.");
_realObject.DoSomething();
Console.WriteLine("LoggingProxy: DoSomething() finished.");
}
}
// Example usage
var loggingProxy = new LoggingProxy();
loggingProxy.DoSomething();
In this example, we define an interface IRealObject
that the real object will implement and a class called RealObject
that implements the real object. We also define the logging proxy class LoggingProxy
, which intercepts and logs calls to methods on the real object.
The LoggingProxy
class creates an instance of the real object in the constructor and sends method calls to the real object while logging information before and after the method call.
When we want to call the real object, we create a new LoggingProxy
. When we call the DoSomething()
method on the logging proxy, it intercepts the call, logs information about it, sends the call to the real object, and logs information about the end of the method call.
Caching Proxy
A caching proxy is a proxy pattern that stores the results of expensive or frequently used operations to speed up an application. The caching proxy stops method calls to the real object and checks to see if the result of the method call has already been cached. If the result is already in the cache, the cached value is returned instead of the real object's method. If the result is not in the cache, the method is run on the real object, and the result is cached before being sent back to the client.
The caching proxy is especially helpful when the real object is expensive to create or when calling its methods takes a long time, like when connecting to a remote database or web service. By storing the results of these calls in a cache, the next calls can be handled much more quickly, which leads to better performance.
Here is a simple example of a caching proxy in C#:
// Define the interface that the real object will implement
public interface IExpensiveObject
{
string GetResult();
}
// Define the class that implements the real object
public class ExpensiveObject : IExpensiveObject
{
public string GetResult()
{
// Simulate an expensive operation
Thread.Sleep(500);
return "Result from ExpensiveObject";
}
}
// Define the caching proxy class that will cache the results of calls to the real object
public class CachingProxy : IExpensiveObject
{
private readonly IExpensiveObject _realObject;
private string _cachedResult;
public CachingProxy(IExpensiveObject realObject)
{
_realObject = realObject;
}
public string GetResult()
{
if (_cachedResult == null)
{
_cachedResult = _realObject.GetResult();
}
return _cachedResult;
}
}
// Example usage
var expensiveObject = new ExpensiveObject();
var cachingProxy = new CachingProxy(expensiveObject);
// First call will be slow, as the result is not cached
Console.WriteLine(cachingProxy.GetResult());
// Subsequent calls will be fast, as the result is cached
Console.WriteLine(cachingProxy.GetResult());
In this example, the real object that does an expensive operation (in this case, simulating an operation with a 500-millisecond delay) is represented by the ExpensiveObject
class. The CachingProxy
class acts as a cache wrapper around the real object. It intercepts calls to the GetResult()
method and checks to see if the result has already been cached. If the result is not cached, the method is called on the real object, and the result is cached before being sent back to the client. If the result has already been cached, the cached value is returned instead of the real object's method.
Using the caching proxy makes subsequent calls to GetResult()
much faster since the result is already cached and can be returned right away.
The Proxy pattern in a microservice environment
The proxy pattern can be used to manage how different services talk to each other in a microservices architecture. In particular, a proxy service can be used to connect a client to a set of backend services.
In this situation, the proxy service can help in a number of ways, including:
- Load balancing: The proxy can send requests to multiple copies of a backend service so that the load is spread out evenly. This helps make sure that no single instance of a service gets too busy with requests, and it can also help the system as a whole be more responsive and available. The proxy can use different load-balancing algorithms, such as round-robin, least connections, or IP hash, to decide which backend instance should handle each request.
- Caching: The proxy can save responses from the backend services, which lets it handle requests more quickly the next time they come in. This can be very helpful for requests that take a long time to process or that return content that is static or mostly static. To manage the cache, the proxy can use different caching strategies, such as time-based expiration or LRU eviction.
- Security: The proxy can make sure that only authorized users can get to the backend services by enforcing authentication and authorization policies. This helps stop people from getting into the system without permission or abusing it. Different security protocols, like OAuth2 or JWT, can be used by the proxy to handle authentication and authorization.
- Monitoring: The proxy can get metrics and logs from the backend services, which shows how well they are working and if they are healthy. This can help find problems and bottlenecks in the system, as well as improve performance and make better use of resources. Different monitoring tools, like Prometheus or Grafana, can be used by the proxy to collect and show metrics.
Enter the Circuit Breaker
The circuit breaker pattern is a way to handle failures and make a system more reliable. It is used in distributed systems. In a microservices architecture, a circuit breaker can be built into a proxy service to control how clients and backend services talk to each other.
The circuit breaker pattern works by checking the status of a remote service and breaking the circuit (stopping requests to the service) if the service is failing or not responding. This keeps the service from getting too busy with more requests and can help stop failures from snowballing.
Pros and Cons of Composite Pattern
✔ We can control the service object without clients knowing. | ❌The code may become more complicated since we need to introduce a lot of new classes. |
✔When clients don't care, we can manage of the lifecycle of the service object. | ❌ The response from the service might get delayed. |
✔The proxy works even if the service object isn’t ready or is not available. | |
✔We can introduce new proxies without changing the service or clients, thus respecting the Open/Closed Principle |
Relations with Other Patterns
- The object that is being wrapped gets a different interface from the Adapter, the same interface from the Proxy, and a better interface from the Decorator.
- Both Facade and Proxy buffer a complex entity and start it up on their own. Unlike Facade, Proxy and its service object have the same interface, which means they can be swapped out.
- Decorator and Proxy are both built in a similar way, but they are used for very different things. Both patterns are based on the principle of composition, which says that one object should give some of its work to another. The main difference is that a Proxy usually takes care of its own service object's life cycle on its own, while Decorators are always put together by the client.
Final Thoughts
In this article, we have discussed what is the Proxy pattern, when to use it and what are the pros and cons of using this design pattern. We then examined some use cases for this pattern and how the Proxy relates to other classic design patterns.
It's worth noting that the Proxy pattern, along with the rest of the design patterns presented by the Gang of Four, is not a panacea or a be-all-end-all solution when designing an application. Once again it's up to the engineers to consider when to use a specific pattern. After all these patterns are useful when used as a precision tool, not a sledgehammer.
Top comments (0)