Explore real-world applications of the Proxy Pattern in JavaScript and TypeScript, including lazy loading, access control, and remote services.
The Proxy Pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. This pattern is particularly useful in scenarios where direct access to an object is either undesirable or impossible. By using a proxy, we can introduce additional functionality to an object without changing its code. In this section, we’ll explore various real-world applications of the Proxy Pattern in JavaScript and TypeScript, such as lazy loading resources, access control, and remote services. We’ll also provide code snippets to illustrate these concepts and discuss the advantages in terms of security, performance, and resource management.
Lazy loading is a design pattern commonly used in programming to defer initialization of an object until the point at which it is needed. This can significantly improve performance and resource management, especially in applications that deal with large amounts of data or complex computations.
In web development, lazy loading images can improve page load times by only loading images as they are needed. Let’s see how the Proxy Pattern can be used to implement lazy loading for images in JavaScript.
class Image {
constructor(filename) {
this.filename = filename;
console.log(`Loading image from ${filename}`);
}
display() {
console.log(`Displaying ${this.filename}`);
}
}
class ProxyImage {
constructor(filename) {
this.filename = filename;
this.realImage = null;
}
display() {
if (this.realImage === null) {
this.realImage = new Image(this.filename);
}
this.realImage.display();
}
}
// Usage
const image = new ProxyImage('photo.jpg');
image.display(); // Loads and displays the image
image.display(); // Displays the image without loading it again
In this example, the ProxyImage
class acts as a proxy for the Image
class. The image is only loaded when the display
method is called for the first time, which demonstrates the lazy loading concept.
Access control is another common use case for the Proxy Pattern. By using a proxy, we can control access to an object, ensuring that only authorized users can perform certain actions.
Consider a banking system where only authorized personnel can access certain account details. We can use a proxy to enforce access control.
interface BankAccount {
getBalance(): number;
deposit(amount: number): void;
withdraw(amount: number): void;
}
class RealBankAccount implements BankAccount {
private balance: number;
constructor(initialBalance: number) {
this.balance = initialBalance;
}
getBalance(): number {
return this.balance;
}
deposit(amount: number): void {
this.balance += amount;
}
withdraw(amount: number): void {
if (amount <= this.balance) {
this.balance -= amount;
} else {
console.log('Insufficient funds');
}
}
}
class BankAccountProxy implements BankAccount {
private realBankAccount: RealBankAccount;
private isAuthorized: boolean;
constructor(initialBalance: number, isAuthorized: boolean) {
this.realBankAccount = new RealBankAccount(initialBalance);
this.isAuthorized = isAuthorized;
}
getBalance(): number {
if (this.isAuthorized) {
return this.realBankAccount.getBalance();
} else {
throw new Error('Unauthorized access');
}
}
deposit(amount: number): void {
if (this.isAuthorized) {
this.realBankAccount.deposit(amount);
} else {
throw new Error('Unauthorized access');
}
}
withdraw(amount: number): void {
if (this.isAuthorized) {
this.realBankAccount.withdraw(amount);
} else {
throw new Error('Unauthorized access');
}
}
}
// Usage
const accountProxy = new BankAccountProxy(1000, false);
try {
console.log(accountProxy.getBalance());
} catch (error) {
console.error(error.message); // Unauthorized access
}
In this example, the BankAccountProxy
class checks if the user is authorized before allowing access to the RealBankAccount
methods.
The Proxy Pattern can also be used to represent remote services. This is particularly useful in distributed systems where objects are located on different servers.
Let’s consider a scenario where we need to interact with a remote weather service. We can use a proxy to handle the communication with the remote server.
interface WeatherService {
getWeather(city: string): Promise<string>;
}
class RealWeatherService implements WeatherService {
async getWeather(city: string): Promise<string> {
// Simulate a network request
return new Promise((resolve) => {
setTimeout(() => {
resolve(`Weather in ${city}: Sunny`);
}, 2000);
});
}
}
class WeatherServiceProxy implements WeatherService {
private realWeatherService: RealWeatherService;
private cache: Map<string, string>;
constructor() {
this.realWeatherService = new RealWeatherService();
this.cache = new Map();
}
async getWeather(city: string): Promise<string> {
if (this.cache.has(city)) {
return Promise.resolve(this.cache.get(city)!);
}
const weather = await this.realWeatherService.getWeather(city);
this.cache.set(city, weather);
return weather;
}
}
// Usage
const weatherService = new WeatherServiceProxy();
weatherService.getWeather('New York').then(console.log); // Fetches from remote
weatherService.getWeather('New York').then(console.log); // Fetches from cache
In this example, the WeatherServiceProxy
caches the weather data to avoid unnecessary network requests.
The Proxy Pattern can be combined with other design patterns to enhance functionality. For example, it can be used with the Decorator Pattern to add additional behavior to objects dynamically.
Consider a scenario where we want to log every access to a bank account. We can use a proxy to log the access and a decorator to add additional behavior.
interface Account {
getBalance(): number;
}
class RealAccount implements Account {
private balance: number;
constructor(balance: number) {
this.balance = balance;
}
getBalance(): number {
return this.balance;
}
}
class AccountProxy implements Account {
private realAccount: RealAccount;
constructor(realAccount: RealAccount) {
this.realAccount = realAccount;
}
getBalance(): number {
console.log('Accessing account balance');
return this.realAccount.getBalance();
}
}
class AccountDecorator implements Account {
protected account: Account;
constructor(account: Account) {
this.account = account;
}
getBalance(): number {
return this.account.getBalance();
}
}
class LoggingAccountDecorator extends AccountDecorator {
getBalance(): number {
console.log('Logging access to account balance');
return super.getBalance();
}
}
// Usage
const realAccount = new RealAccount(1000);
const proxy = new AccountProxy(realAccount);
const loggingDecorator = new LoggingAccountDecorator(proxy);
console.log(loggingDecorator.getBalance());
In this example, the AccountProxy
logs access to the account balance, and the LoggingAccountDecorator
adds additional logging behavior.
While the Proxy Pattern offers many advantages, there are some common pitfalls to be aware of:
To better understand how the Proxy Pattern works, let’s visualize the interaction between the client, proxy, and real subject.
sequenceDiagram participant Client participant Proxy participant RealSubject Client->>Proxy: Request alt Cached Proxy-->>Client: Return cached response else Not Cached Proxy->>RealSubject: Forward request RealSubject-->>Proxy: Response Proxy-->>Client: Return response end
This sequence diagram illustrates how the Proxy Pattern handles requests by either returning a cached response or forwarding the request to the real subject.
Experiment with the code examples provided in this section. Try modifying the ProxyImage
example to load different types of media, such as videos or audio files. Implement additional access control checks in the BankAccountProxy
example. Explore how caching can be optimized in the WeatherServiceProxy
example by setting expiration times for cached data.
As you explore the Proxy Pattern, consider the following questions:
Remember, mastering design patterns is a journey. The Proxy Pattern is just one of many patterns that can help you write more maintainable and scalable code. Keep experimenting, stay curious, and enjoy the journey!