Adapter pattern in TypeScript

This post is 4 years old. (Or older!) Code samples may not work, screenshots may be missing and links could be broken. Although some of the content may be relevant please take it with a pinch of salt.

Design patterns play an important part in software development and they do help us to design better software and write better code.

GoF

Back in 1994, a book was authored by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides that discusses 23 desgin patterns, titled Design Patterns: Elements of Resuable Object-Oriented Software. You may have heard of this book or the authors as Gang of Four (GoF).

The book and the outlined design patterns in them formtulate a solid base for today's software development and those patterns introduced and explained in 1994 are still applicable today and can be used in modern day applications.

Design Pattern types

Design patterns can be categorised into the following categories based on what and how they aim to achieve

  • Creational: deal with object creation
  • Structural: deal with relationships between objects (or other entities)
  • Behavioural: deal with communication patterns between objects
  • Concurrency: deal with multi-threaded paradigms

Adapter Pattern

The adapter pattern is one of the structural software design patterns. The main purpose of this pattern is to allow different interfaces (classes) to work together and it achieves this without having to modify the original class.

This pattern works with 3 components: a client, an adapter and an adaptee. The client doesn't care about the implementation of the interface, it only wants to access a certain method for example. The adapter class makes sure that this can happen as it provides an interface to the adaptee class - which is the incompatilble class with the client.

When to use the adapter pattern?

The adapter pattern can be used when we are working with legacy interfaces, or different APIs or we just need to integrate a new API / interface into our system and we cannot (or don't want to) modify the client implementation.

TypeScript implementation

The adapter pattern could be implemented in any programming language, including vanilla JavaScript as well but I thought it'd be interesting to see how a TypeScript implementation would look like.

It goes without saying that should you want to get the JavaScript implementation, just transpile the TypeScript code to JavaScript, either to ES2015 or to ES5.

Adapter example

For the purposes of this example we are going to assume that we have integrated a mapping API into our application. From the client weare using the Map class and its locate() method to make a hypothetical reverse gecoding call. Then some requirements change and suddenly we need to integrate another Mapping API but we want to keep the client's functionality where we only invoke a locate() method. However the new class does not expose a locate method but instead a geocode() method.

This is how our basic implementation would look like:

`    // impl-class.ts
export interface IGeoCoder {
locate(): string;
}

export class GeoCoder implements IGeoCoder {
private latitude: number;
private longitude: number;
constructor(latitude, longitude) {
this.latitude = latitude;
this.longitude = longitude;
}
public locate(): string {
// do geolookup
return
`
${this.latitude} | ${this.longitude} is in London.`
}
}

// client.ts
import { GeoCoder } from './impl-class';

const gpsCoords = new GeoCoder(51.5074, 0.1278);
const location = gpsCoords.locate();
console.log(location);

The above code is fairly straight forward, we have class and in our client.ts file we implement that class.

So now we get a call that we need to work with another API that looks like this:

interface ILocator {
geocode(): string;
}

export class Locator implements ILocator {
private latitude: number;
private longitude: number;
constructor(latitude, longitude) {
this.latitude = latitude;
this.longitude = longitude;
}
public geocode(): string {
// do geolookup
return `The specified location (${this.latitude}, ${this.longitude}) returns London.`;
}
}

We could, at this point, incorporate and integrage the new class as well easily into our client but we'd have to change how client.ts looks like. A much better solution would be to create an adapter class. In the adapter class we need to adapt the new class's behaviour - this also means that future classes can be incorporated to our adapter:

// adatper.ts
import { IGeoCoder, GeoCoder } from './impl-class';
import { Locator } from './new-class';

export class GeocodingAdapter implements IGeoCoder {
private latitude: number;
private longitude: number;
private type: string;
constructor(latitude, longitude, type) {
this.latitude = latitude;
this.longitude = longitude;
this.type = type;
}

locate(): string {
if (this.type === 'GeoCoder') {
const geocoder = new GeoCoder(this.latitude, this.longitude);
return geocoder.locate();
} else if (this.type === 'Locator') {
const locator = new Locator(this.latitude, this.longitude);
return locator.geocode();
} else {
throw new Error('Please use either GeoCoder or Locator');
}
}
}

The adapter requires us to specify not only the latitude and longitude values but also a 'type' which is going to let our code know which exact other adapted class to instansiate. The implemention of client requires one tiny modification - we need to import the adapter class:

// client.ts
import { GeocodingAdapter } from './adapter';

const gAdapter = new GeocodingAdapter(51.5074, 0.1278, 'Locator');
const gAdapter2 = new GeocodingAdapter(51.5074, 0.1278, 'GeoCoder');
const locationFromLocator = gAdapter.locate();
const locationFromGeoCoder = gAdapter2.locate();

console.log(locationFromLocator);
console.log(locationFromGeoCoder);

The actual invocation of the locate() method did not change.