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.