The proxy pattern provides a surrogate or placeholder for another object in order to control access to it. In essence what that means is that the proxy acts as a middle man between the target and the source, this may sound familiar; it is more or less exactly what the facade, adapter, and decorator patterns do. The difference however is in the intent or reason behind these patterns, in the case of the proxy pattern we have several different types of proxies:
Remote proxy: A remote proxy acts as a local representative for an object located in a different address space, such as on a different machine or server. It facilitates communication and data transfer between the client and the remote object. It abstracts the details of network communication, allowing the client to interact with a remote object seamlessly.
Virtual proxy: A virtual proxy acts as a placeholder for an object that is expensive to create or load. It delays the instantiation or loading of the actual object until it is genuinely needed. Provides a “stand-in” for an actual object, which is instantiated only on demand.
Protection proxy: A protection proxy controls access to an object by implementing additional security, permissions, or authentication mechanisms. It determines whether the client is authorised to interact with the object. It acts as a gatekeeper, allowing or denying access based on specific rules.
Logging proxy: Records details about the interactions with the real object, often for debugging, monitoring, or analytics purposes.
Caching proxy: A caching proxy stores frequently accessed data or responses to improve performance and reduce resource usage.
To name a few, despite the difference in intent they accomplish their goal in the same way, the key concept to a proxy is that it eavesdrops on a call. Let's hypothetically say that we have the following configuration.
Above we depict a contrived, but fairly common configuration, we have an object which we create locally and a rest service to load and save that object to and from some sort of cloud repository. Now as before let's imagine that it is a significantly more intricate, and let's also say that perhaps these objects are significantly more intricate and take up much more memory, but don't change very much. To solve a problem such as that a caching proxy would make perfect sense. Our UML would look like the following.
Exactly like the decorator pattern, the proxy has this "is a" and "has a" relationsip, however unlike the decorator pattern the intent of a proxy is not to add or change functionality, but to intercept, do something, and then pass the original message back to the caller.
In code it could like like the following without the proxy
interface IIngredient {
id: string;
name: string;
unit: number;
pricePerUnit: number;
}
class Ingredient implements IIngredient {
id: string;
name: string;
unit: number;
pricePerUnit: number;
constructor(id: string, name: string, unit: number, pricePerUnit: number) {
this.id = id;
this.name = name;
this.unit = unit;
this.pricePerUnit = pricePerUnit;
}
getPrice(): number {
return this.unit * this.pricePerUnit;
}
getDescription(): string {
const quantity = this.unit;
const parts = `${quantity < 1 ? 'part' : 'parts'}`;
const name = this.name;
return `${quantity} ${parts} ${name}`;
}
}
interface IIngredientService {
url: string;
getIngredient(id:string): Promise<Ingredient[]>;
createIngredient(ingredient: Ingredient): Promise<Ingredient>;
updateIngredient(ingredient: Ingredient): Promise<Ingredient>;
}
class IngredientService implements IIngredientService {
url: string;
constructor(url: string) {
this.url = url;
}
async getIngredient(id: string): Promise<Ingredient[]> {
const response = await fetch(`${this.url}/ingredients/${id}`);
const data = await response.json();
return data.map((item: any) => new Ingredient(item.id, item.name, item.unit, item.pricePerUnit));
}
async createIngredient(ingredient: Ingredient): Promise<Ingredient> {
const response = await fetch(`${this.url}/ingredients`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(ingredient)
});
const data = await response.json();
return new Ingredient(data.id, data.name, data.unit, data.pricePerUnit);
}
async updateIngredient(ingredient: Ingredient): Promise<Ingredient> {
const response = await fetch(`${this.url}/ingredients/${ingredient.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(ingredient)
});
const data = await response.json();
return new Ingredient(data.id, data.name, data.unit, data.pricePerUnit);
}
}
and then if we add the proxy it could like like the following
class ProxyCache implements IIngredientService {
url: string;
private cache: Map<string, Ingredient[]> = new Map();
private ingredientService: IngredientService;
constructor(url: string) {
this.url = url;
this.ingredientService = new IngredientService(url);
}
async getIngredient(id: string): Promise<Ingredient[]> {
if (this.cache.has(id)) {
return this.cache.get(id)!;
} else {
const ingredients = await this.ingredientService.getIngredient(id);
this.cache.set(id, ingredients);
return ingredients;
}
}
async createIngredient(ingredient: Ingredient): Promise<Ingredient> {
const newIngredient = await this.ingredientService.createIngredient(ingredient);
this.cache.set(newIngredient.id, [newIngredient]);
return newIngredient;
}
async updateIngredient(ingredient: Ingredient): Promise<Ingredient> {
const updatedIngredient = await this.ingredientService.updateIngredient(ingredient);
this.cache.set(updatedIngredient.id, [updatedIngredient]);
return updatedIngredient;
}
async savetoCache(ingredient: IIngredient): Promise<void> {
this.cache.set(ingredient.id, [ingredient as Ingredient]);
}
async loadFromCache(id: string): Promise<IIngredient[]> {
if (this.cache.has(id)) {
return this.cache.get(id)!;
} else {
throw new Error('Ingredient not found in cache');
}
}
}
notice that our proxy class, makes no changes to the request, it simply stores it in a cache, then on a subsequent call it checks if the request is in the cache before making a network call.