Tuesday, 30 July 2024

Facade Pattern

The Facade pattern is a design pattern that provides a simplified, unified interface to a set of interfaces in a subsystem, making it easier to use. It hides the complexities of the subsystem by providing a single point of access, thus reducing dependencies and making the subsystem easier to work with.

If you follow the Single Responsibility Principle and create a vast network of decoupled classes all of which focus on their own specific responsibility, you can very quickly create a rather complex ecosystem of loosely coupled classes, which can prove to be extremely beneficial from the code maintenance perspective. It's much easier to maintain and upgrade a lot of small focused classes, than it is to try and work with one giant monolith of spaghetti code; however this decoupled ecosystem may prove to be more difficult to work with for consumers of the functionality. 

The facade pattern aims to alleviate this complexity by providing a unified interface to a set of interfaces in a subsystem. The facade defines a higher-level interface that makes the subsystem easier to work with.

When should you use the Facade pattern?

  • When you want to provide a simple interface to a complex subsystem. Most patterns, when applied, result in many small domain specific classes. This makes the subsystem more reusable and customisable, but it also becomes harder to use for clients that don't need to customize it. A facade can provide a simple default view of the subsystem that is good enough for most clients
  • There are many dependencies between clients and the implementation classes of an abstraction. Introduce a facade to decouple the subsystem from clients and other subsystems, thereby promoting subsystem independence and portability.
  • Use a facade to define an entry point to each subsystem level. If subsystems are dependent, then you can simplify the dependencies between them by making them communicate with each other solely through their facades.
The Facade pattern is generally implemented for the sake of the Subsystem consumers, not so much the creators, though by simplifying the use of the subsystem this benefits the creators by limiting the amount of questions the consumers will have to make.

Monday, 22 July 2024

Composite adapter pattern

The (composite) adapter pattern is exactly what it sounds like, think of one of those universal adapters you know the ones; when you travel abroad and you need to plug a North American plug into a European outlet or vice versa.



In development we have the same idea, often times we have data in the form of an object, this could be a class that is represented by an interface, or maybe just a class; whatever the case is, we need to consume that data in a function that though our object has everything we need, it does not implement the expected interface or type which our function consumes. 

For example let's say that we have the following TypeScirpt interface and class for a person


export interface IPerson {
birthDay: number;
birthMoth: number;
birthYear: number;

firstName: string;
lastName: string;

getAge(): number;
getFullName(): string;
}

export default class Person implements IPerson {
birthDay: number;
birthMoth: number;
birthYear: number;
firstName: string;
lastName: string;

constructor(birthDay: number, birthMonth: number, birthYear: number, firstName: string, lastName: string) {
this.birthDay = birthDay;
this.birthMoth = birthMonth;
this.birthYear = birthYear;

this.firstName = firstName;
this.lastName = lastName;
}

getAge(): number {
const today = new Date(Date.now());
let age = today.getFullYear() - this.birthYear;

if (this.birthYear > today.getFullYear() -age)
age--;
return age;
}

getFullName(): string {

return `${this.firstName} ${this.lastName}`
}
}

Keep in mind this is a contrived example, let's say our solution has a function that requires an IHuman interface which looks like the following.


export interface IHuman {
birthDate: Date;
fullName: string;

getAge():number;
}

What we can do is to create an PersonToHumanAdapter, you can think of this as a wrapper that takes in IPerson as a constructor variable and wraps it in a class that implements the IHuman interface. 

As UML this can look like the following 

It's rather trivial, we create a PersonToHumanAdapter class, which implements the IHuman interface and takes in the IPerson implementation, it then adapts the IPerson implementation to an IHuman interface. Sometime this is also referred to as just a simple wrapper, however I find the word Adapter is more descriptive. 


export class PersonToHumanAdapter implements IHuman{
birthDate: Date;
fullName: string;

constructor(p: IPerson) {
this.birthDate = new Date(p.birthYear, p.birthMoth, p.birthDay);
this.fullName = p.getFullName();
this.getAge = p.getAge;
}

getAge(): number {
throw new Error("Method not implemented.");
}
}

An important distinction to make is that adapters are dumb, they do not modify or add functionality, they just make an implementation of a class usable by a client. In the above example we simply assigned all of the members of the person object to our adapter, however we could have created a private person variable and leveraged it instead.


export class PersonToHumanAdapter implements IHuman{
private _p: IPerson
public get birthDate() {
const p = this._p;
return new Date(p.birthYear, p.birthMoth, p.birthDay);;
}
public set birthDate(birthDate: Date) {
const p = this._p;
p.birthDay = birthDate.getDay();
p.birthMoth = birthDate.getMonth();
p.birthYear = birthDate.getFullYear();
}
public get fullName(){
return this._p.getFullName();
}
public set fullName(fullName: string) {
const p = this._p;
p.firstName = fullName.split(" ")[0]
p.lastName = fullName.split(" ")[1];
}

constructor(p: IPerson) {
this._p = p;
}

getAge(): number {
return this._p.getAge();
}
}

At the end of the day it really depends on the nuances of your particular requirements.

A more generic visualisation would be the following

Think of the client as "Our" program, the thing that is trying to use a function called request, which is defined in the Target interface. Our adapter implements the Target interface and tells our client that yes I have an implementation of the Request function, the adapter wraps the adaptee and lets our "Program" use the "Adaptee" even though the Adaptee does not implement the correct interface. The Adapter wraps the SpecificRequest function which is defined in the Adaptee and returns it's result to the client when called via the adapter.