Proxy Pattern | Deep Dive In Design Patterns
The Proxy pattern is a structural design pattern that provides an intermediary (proxy) to control access to an object. This pattern is often used to provide a placeholder for another object, allowing control over the interactions with the real object. Here’s a breakdown of the Proxy pattern, including its advantages, disadvantages, and a TypeScript code sample.
Advantages
- Control Access: The Proxy can control access to the real object, including permission checks, logging, and other preprocessing or postprocessing tasks.
- Lazy Initialization: The Proxy can delay the creation of the real object until it’s actually needed (virtual proxy).
- Remote Proxy: It can represent an object that resides in a different address space (remote proxy).
- Protection Proxy: It can provide access control and security checks for the real object (protection proxy).
Disadvantages
- Complexity: Adds additional layers of indirection, which can make the code more complex and harder to understand.
- Performance Overhead: May introduce performance overhead due to additional operations performed by the Proxy.
Code Sample
Here’s an example of how to implement the Proxy pattern in TypeScript:
// Step 1: Define a Subject Interface
interface Subject {
request(): void;
}
// Step 2: Implement the RealSubject
class RealSubject implements Subject {
public request(): void {
console.log('RealSubject: Handling request.');
}
}
// Step 3: Implement the Proxy
class Proxy implements Subject {
private realSubject: RealSubject | null = null;
public request(): void {
if (!this.realSubject) {
this.realSubject = new RealSubject();
}
this.logAccess();
this.realSubject.request();
}
private logAccess(): void {
console.log('Proxy: Logging access before forwarding the request.');
}
}
// Client code
function clientCode(subject: Subject) {
subject.request();
}
const proxy = new Proxy();
clientCode(proxy);
Explanation
- Subject Interface: Defines the common interface for
RealSubject
andProxy
. - RealSubject: The actual object that performs the real work.
- Proxy: Controls access to the
RealSubject
, implementing additional functionality such as logging access.
In this example, the Proxy
class manages access to the RealSubject
and performs additional tasks (logging access) before delegating the request to the real object. This design helps improve code by encapsulating the additional behavior in the Proxy class, keeping the RealSubject focused on its core functionality.
Let’s consider a use case where the Proxy pattern improves code: Lazy Initialization. This is where you want to delay the creation of an object until it’s actually needed, which can be particularly useful if object creation is expensive in terms of time or resources.
Use Case: Lazy Initialization with Proxy Pattern
Imagine you’re building a system where you need to manage user profiles. Each user profile involves loading data from a database, which can be a time-consuming operation. You want to avoid loading all profiles at startup and instead load them only when accessed.
Without Proxy Pattern
class UserProfile {
private data: string;
constructor(userId: string) {
console.log('Loading profile from database...');
// Simulate a time-consuming database operation
this.data = `User data for ${userId}`;
}
public getData(): string {
return this.data;
}
}
// Client code
const profile = new UserProfile('user123');
console.log(profile.getData());
In this example, the UserProfile
is created and loaded from the database immediately upon instantiation, even if the profile data might not be used right away.
With Proxy Pattern
Here’s how you can use the Proxy pattern to implement lazy initialization:
// Step 1: Define a Subject Interface
interface IUserProfile {
getData(): string;
}
// Step 2: Implement the RealSubject
class RealUserProfile implements IUserProfile {
private data: string;
constructor(userId: string) {
console.log('Loading profile from database...');
// Simulate a time-consuming database operation
this.data = `User data for ${userId}`;
}
public getData(): string {
return this.data;
}
}
// Step 3: Implement the Proxy
class UserProfileProxy implements IUserProfile {
private realUserProfile: RealUserProfile | null = null;
private userId: string;
constructor(userId: string) {
this.userId = userId;
}
public getData(): string {
if (!this.realUserProfile) {
this.realUserProfile = new RealUserProfile(this.userId);
}
return this.realUserProfile.getData();
}
}
// Client code
const profileProxy = new UserProfileProxy('user123');
console.log('Profile data will be fetched only when accessed:');
console.log(profileProxy.getData());
Explanation
- RealUserProfile: This class simulates loading user profile data from a database, which is an expensive operation.
- UserProfileProxy: This proxy class implements the
IUserProfile
interface and delays the creation of theRealUserProfile
instance until itsgetData()
method is called. This is known as lazy initialization.
Benefits
- Performance Improvement: The profile data is only loaded when actually needed, which can improve performance, especially if there are many profiles and only a few are accessed at runtime.
- Resource Efficiency: Reduces the initial resource usage since profiles are only loaded on demand.
By using the Proxy pattern in this scenario, you improve code efficiency and reduce unnecessary resource consumption, which can be crucial in performance-sensitive applications.
Thanks for reading.