Thursday, 22 August 2024

Proxy pattern

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.

Decorator pattern

The Decorator Pattern is a design pattern used to dynamically add new functionality at runtime to an object, without altering their structure. This ability to modify an objects behaviour dynamically is the advantage over using class inheritance. When this need to dynamically alter an object's functionality does not exist than the decorator pattern is most likely over engineering a solution. 

It is most appropriate for situations in which either you find yourself creating dozens of classes that are extremely similar, but have slightly variation functionality (known as class explosion), or in situations where their is a high likelihood that you will need to append existing functionality in the future while maintaining its original capabilities.

You can think of decorators as wrappers, for a concrete class, they can add functionality or modify existing behaviour. 

In the above illustration, you can start to visualise how the decorator works, it wraps a concrete class and either modifies functionality or adds functionality, in theory you could have an unlimited number of wrappers. Below is a UML representation of how this works


Notice that the IDecorator interface, not only implements or extends either the base interface or concrete class, but it also has a reference to to that base definition or implementation. This creates both a "Has a" and "Is A" relationship with the base concrete class. This provides us with two advantages, firstly and most commonly this allows us to dynamically add functionality and behaviour to existing classes at runtime, the most common example of this beverage pricing and naming. 

The most common example used to teach the decorator pattern is a rather contrived coffee house example. The coffeehouse example lends itself very well to illustrate how this pattern works, however it's not really how one would leverage this pattern in production code. Instead of the coffee example, I'm going to create a cocktail one. I realise that it sounds exactly the same, and to a degree it is. The reason I'm going with a cocktail example is because after the we understand the fundamentals of how the decorator pattern works. The cocktail example acts as an excellent jump off point to emphasise a real world problem which this pattern is meant to solve.

Let's start with an ingredient interface and class, each beverage will be made up of a combination of ingredients which will determine the price of the finished drink.


interface IIngredient {
name: string;
pricePerUnit: number;
unit: number
}

class Ingredient implements IIngredient {
name: string;
pricePerUnit: number;
unit: number

constructor(name: string, price: number, unit: number) {
this.name = name;
this.pricePerUnit = pricePerUnit;
this.unit = unit;
}
}

At the very base level, whether we are talking wine, cocktails, mocktails, coffee, or Italian sodas, every beverage is going to have a price, and a recipe, at this point it would make sense to include an array of Ingredients in our Beverage class, however it would completely defeat the need for the decorator pattern, for this reason, though this example articulates the use of the decorator pattern rather well, it's not a practical one. 


abstract class Beverage {
abstract getPrice(): number;
abstract getRecipe(): string;
}

Notice that anything that inherits from the abstract Beverage class is going to have to implement both the getPrice and getRecipe functions. Next let's create a cocktail class which extends the abstract Beverage class and implements the abstract methods getPrice and getRecipe().


export default class Cocktail extends Beverage {
name: string;

constructor(name: string) {
super();
this.name = name;
}
getPrice(): number {
return 0;
}

getRecipe(): string {
return `A ${this.name} made with`;
}
}

With that complete we have a UML diagram like so


notice that our Ingredient interface and class are not really attached to anything, let's now introduce the idea of a decorator and fix that.


abstract class CocktailDecorator extends Beverage {
base: Beverage;
ingredient: IIngredient;

constructor(base: Beverage, name: string, price: number, oz: number) {
this.base = base;
this.ingredient = new Ingredient(name, price, oz);
}

getPrice = (): number => {
const price = this.ingredient.ozPrice;
const quantity = this.ingredient.ozQuantity

const thisPrice = price * quantity;
const basePrice = this.base.getPrice();

return basePrice + thisPrice;
}

getRecipe = (): string => {
const prefix = this.base.getRecipe();
const conjunction = this.base instanceof Cocktail ? ":" : ",";
const quantity = this.ingredient.ozQuantity;
const parts = `${quantity < 1 ? 'part' : 'parts'}`
const name = this.ingredient.name;

return `${prefix}${conjunction} ${quantity} ${parts} ${name}`;
};
}

Here we have this interesting "is a" and "has a" relationship, this is what allows us to wrap not just anything that inherits from the abstract Beverage class, but it also lets us modify the functionality of anything defined in the Beverage class. notice that our decorator uses the ingredient to calculate the price as well as the recipe.

Lets create three variations of our decorator


export class VodkaDecorator extends CocktailDecorator {
constructor(base: Beverage, oz: number) {
super(base, "Vodka", 4, oz);
}
}

export class GinDecorator extends CocktailDecorator {
constructor(base: Beverage, oz: number) {
super(base, "Gin", 5, oz);
}
}

export class VermouthDecorator extends CocktailDecorator {
constructor(base: Beverage, oz: number) {
super(base, "Vermouth", 1, oz);
}
}

Now we can instantiate a either a gin or vodka martini like so


const vodkaMartini = new VermouthDecorator(new VodkaDecorator(new Cocktail("Vodka martini"), 2), .25);
const ginMartini = new VermouthDecorator(new GinDecorator(new Cocktail("Gin martini"), 2), .25);

This demonstrates how we can use the decorator pattern to append functionality to an existing base implementation, notice that in this case we create this daisy chain of classes which implement and have an instance of the Beverage class, down to the base concrete implementation which only implements the abstract Beverage class. 

One thing to keep in mind is that nesting order matters, when we call 

console.log(vodkaMartini.getRecipe());
console.log(vodkaMartini.getPrice());
console.log(ginMartini.getRecipe());
console.log(ginMartini.getPrice());

Each getRecipe and getPrice function is called from the outer down to the inner wrapper, this is why when we defined the getRecipe class in the base CocktailDecorator we leverage this idea of a prefix, so that the concretes GetRecipe implementation would be at the front of the output.


One thing to keep in mind when using the decorator pattern, any functionality that has been altered by the decorators is called from the outer ring down to centre. 

Our final UML looks something like this:


As mentioned before, this is a contrived example and though it illustrates how the decorator pattern works, it does not provide a realistic example of what context to actually use it in. For a problem like this, it would be far more reasonable for the cocktail class to be instantiated with an array of ingredients with an option to add/remove ingredients; so let's refactor our code to more appropriate version



notice how we've simplified our models 


interface IIngredient {
name: string;
ozPrice: number;
ozQuantity: number;
}

abstract class Priced {
abstract getPrice(): number;
abstract getDescription(): string;
}

export class Ingredient extends Priced implements IIngredient {
name: string;
ozPrice: number;
ozQuantity: number;

constructor(name: string, ozPrice: number, ozQuantity: number) {
super();
this.name = name;
this.ozPrice = ozPrice;
this.ozQuantity = ozQuantity;
}

getPrice(): number {
return this.ozPrice * this.ozQuantity;
}
getDescription(): string {
const quantity = this.ozQuantity;
const parts = `${quantity < 1 ? 'part' : 'parts'}`
const name = this.name;

return `${quantity} ${parts} ${name}`;
}
}

export default class Cocktail extends Priced {
name: string;
ingredients: Ingredient[]

constructor(name: string, ingredients:Ingredient[]) {
super();
this.name = name;
this.ingredients = ingredients;
}
getPrice(): number {
return this.ingredients.reduce((sum, item)=> sum + item.getPrice(), 0);
}

getDescription(): string {
return this.name + ': ' + this.ingredients.reduce((recipe, item)=> recipe + `, ${item.getPrice()}`, "");
}

addIngredient (ingredient: Ingredient) {
const i = this.ingredients.find(i => i.name = ingredient.name);
if(i)
i.ozQuantity += ingredient.ozQuantity;
else
this.ingredients.push(ingredient);
}

removeIngredient(ingredient: Ingredient){
const i = this.ingredients.findIndex(i => i.name = ingredient.name);
if(i > -1) {
const t = this.ingredients[i];
t.ozQuantity -= ingredient.ozQuantity;
if(t.ozQuantity < 0)
this.ingredients.splice(i,1);
}
}
}

we've significantly reduced the complexity of our code, now let's create a menu object; makes sense right, it's reasonable for a predefined menu of cocktails to exist for patrons to choose from with the ability to modify them, swap out the alcohol for a preferred brand, or add a double shot.


class Menu{
cocktails: Cocktail[]
constructor(cocktails: Cocktail[]){
this.cocktails = cocktails;
}

listCocktails(): [string, number][]{
return this.cocktails.map(c=> [c.getDescription(), c.getPrice()])
}
}

Let's say we go live with our solution, and it works as far as we know, we can define cocktails, their ingredients, and generate a menu. Now hypothetically let's say someone orders a dirty martini with 10 extra shots of vodka, well that's fine and dandy, but what about glasses? don't they have a capacity? now our example is rather trivial and i would say forget the open/closed principle and just refactor the code; and in this context no one would disagree with me, however in financial software that has thousands of legacy lines of code, no one would let you make that change for fear of introducing more bugs. 

Now for an intricate situation like that it would make sense to introduce a decorator, this way we can ensure that the original code continues to function, but we can now extend our system without risking legacy code.


export class GlassDecorator extends Cocktail{
base: Cocktail;
maxVolume: number;

constructor(base: Cocktail, maxVolume: number) {
super(base.name, base.ingredients);
this.base = base;
this.maxVolume = maxVolume
}

getFilledVolume(): number{
return this.base.ingredients.reduce((sum, i)=> sum + i.ozQuantity, 0);
}

override addIngredient(ingredient: Ingredient): void {
let filledVolume = this.getFilledVolume();
if(ingredient.ozQuantity + filledVolume > this.maxVolume)
throw Error("the glass does not have enough volume");
this.base.addIngredient(ingredient);
}
}

Our final UML looks like the following.


We can now use our Cocktail class and Glass decorator like so, :

const ginMartini = new Cocktail("gin martini", [gin, vermouth]);
const glass = new GlassDecorator(ginMartini, 5);

glass.addIngredient(new Ingredient("ice", 0, 4))

ginMartini.getDescription();
ginMartini.getPrice();

In conclusion the decorator pattern is useful when you want to add functionality to legacy code, but you want to mitigate any chance of impacting existing code. The change in behaviour impacts from the outside in. meaning that any function that is overridden or appended is first executed on the outer ring of decorators first.


A quick note on UML class diagrams, I have been showing code before the Class diagram. This is not how my actual or anyones work flow should go, software engineers use UML diagrams to plan out their code, to optimally structure it before writing code.