carlos caballero
Angular
JavaScript
NestJS
NodeJS
TypeScript
UI-UX
ZExtra

Design Patterns: Adapter

6 min read

There are 23 classic design patterns, which are described in the original book, Design Patterns: Elements of Reusable Object-Oriented Software. These patterns provide solutions to particular problems, often repeated in the software development.

In this article, I am going to describe the how the Adapter Pattern; and how and when it should be applied.

Adapter Pattern: Basic Idea

An adapter allows two incompatible interfaces to work together. This is the real-world definition for an adapter. Interfaces may be incompatible, but the inner functionality should suit the need. The adapter design pattern allows otherwise incompatible classes to work together by converting the interface of one class into an interface expected by the clients. — Wikipedia

Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces. - Design Patterns: Elements of Reusable Object-Oriented Software

The main feature of this pattern is reusing a class which has different interfaces compared with the rest of classes or several classes which have different interface so they can works together.

There are two versions of this pattern:

  • The object adapter implements the target interface by delegating to an adaptee object at run-time.
  • The class adapter implements the target interface by inheriting from an adaptee class at compiler-time.

Since multiple inheritance is not supported by many languages including Java and is associated with many problems we have not shown implementation using class adapter pattern.

To sum up, the object adapter (AKA adapter) contains an instance of the class it wraps. The UML's diagram of this pattern is the following one:

Adapter Pattern

The Adaptor class contains the Adaptee class. Which meansAdaptee class will be used by Adaptor.

Adapter Pattern: When To Use

  1. There is a class which its interfaces do not match the one you need.
  2. There are several subclasses but it's unpractical to adapt their interface by subclassing every one.

Adapter Pattern: Advantages

The Adapter Pattern has several advantages, summarised in the following points:

  • The code is more resuable and flexible.
  • Clean code because the client/context doesn't use a different interface in each concrete class and can use polymorphism to swap between different adapters.

Adapter pattern - Example 1: A new Android arrives to the city example using JavaScript/TypeScript

I will now show you how you can implement this pattern using JavaScript/TypeScript. Please bear in mind that Javascript lacks both interfaces and abstract classes. Therefore, the best way to understand this pattern is by using an example and TypeScript. In our case, I have made up a problem in which there is an abstract class named Warrior which defines a Warrior. A warrior has a set of attributes not relevant to the problem and a method called attack. There is a classification of types of warriors such as Saiyan and Namekian which defines a concrete implementation of the attack method but a new class arrives to the system called Android which does not satisfy Warrior's interface but internally implemented the method just in different way. The following UML diagram shows the scenario that I have just described.

Adapter-Problem-1-1

The solution is to use an adapter pattern that consits in a class (AndroidAdapter) which contains the Android object and use it to do it compatible with the warrior's interface. I.e, the new UML diagram using the adapter pattern is shown below:

Adapter-problem-1-resolved-1

The code associate to the models and interface are the following ones:

export interface Warrior {
  ATTACK_BASE: number;
  attack(): number;
}
import { Warrior } from '../interfaces/warrior.interface';

export class Saiyan implements Warrior {
  public ATTACK_BASE = 100;
  public attack(): number {
    return Math.random() * 100 + this.ATTACK_BASE;
  }
}
import { Warrior } from '../interfaces/warrior.interface';

export class Namekian implements Warrior {
  public ATTACK_BASE = 50;
  public attack(): number {
    return Math.random() * 50 + this.ATTACK_BASE;
  }
}
export class Android {
  public punch(): number {
    return 10;
  }
  public kick(): number {
    return Math.random() * this.punch() + this.punch();
  }
}

Piccolo_asustado

As you can see the Android class has not implemented the warrior's interface. So, the class AndroidAdapter is responsible for solving this problem.

import { Warrior } from '../interfaces/warrior.interface';
import { Android } from './android.model';

export class AndroidAdapter implements Warrior {
  constructor(private android: Android) {}
  public ATTACK_BASE = 50;
  public attack(): number {
    return this.android.kick() + this.android.punch() + this.ATTACK_BASE;
  }
}

135032

Finally, the client/context that wants to use the three class should use the AndroidAdapter class together with Saiyan and Namekian as you can see in the next code:

import { Saiyan } from './models/saiyan.model';
import { Namekian } from './models/namekian';
import { AndroidAdapter } from './models/android-adapter.model';
import { Android } from './models/android.model';

const goku = new Saiyan();
const vegeta = new Saiyan();
const piccolo = new Namekian();
const C17 = new AndroidAdapter(new Android());

console.log(`Goku attack: ${goku.attack()}`);
console.log(`Vegeta attack: ${vegeta.attack()}`);
console.log(`Piccolo attack: ${piccolo.attack()}`);
console.log(`C17 attack: ${C17.attack()}`);

Adapter pattern - Example 2: Several races want to works together (Genki-dama) using JavaScript/TypeScript

Another interesting problem which is resolved using Adapter pattern is when there are several classes with different interfaces but they can works together. In the following UML's diagram you can see this situation:

Adapter-problem-2

In this case, the races have different interface to communicate between them. In concrete, each race has one or several methods to shared its power. For example, the humans have sharedPower and the namekians have getPower and even more complex are the saiyans which have two methods to shared its power (myPowerPart1 and myPowerPart2).

The solution to do that several races works together is used the adapter pattern as you can see in the following UML's diagram.

adapter-problem-2-resolved

A new adapter class is used for each class, this adapter class contains the original object and the methods that implements the interface. The code for this example is quite easy.

export class Human {
  public sharedPower(): number {
    return 10;
  }
}
export class Namekian {
  public getPower(): number {
    return Math.random() * 20 + 20;
  }
}
export class Saiyan {
  myPowerPart1(): number {
    return Math.random() * 100 + 100;
  }
  myPowerPart2(): number {
    return Math.random() * 1000 + 500;
  }
}

And their respective adapters are the following:

import { PureRace } from '../interfaces/pure-race.interface';
import { Human } from './human.model';

export class HumanAdapter implements PureRace {
  constructor(private human: Human) {}

  public genki(): number {
    return this.human.sharedPower();
  }
}

/**/

import { PureRace } from '../interfaces/pure-race.interface';
import { Namekian } from './namekian';

export class NamekianAdapter implements PureRace {
  constructor(private namekian: Namekian) {}
  public genki(): number {
    return this.namekian.getPower();
  }
}

/**/

import { PureRace } from '../interfaces/pure-race.interface';
import { Saiyan } from './saiyan.model';

export class SaiyanAdapter implements PureRace {
  constructor(private saiyan: Saiyan) {}
  public genki(): number {
    return this.saiyan.myPowerPart1() + this.saiyan.myPowerPart2();
  }
}

Finally, all the objects contributing energy to fight evil and achieve great power.

import { Saiyan } from './models/saiyan.model';
import { Namekian } from './models/namekian';
import { Human } from './models/human.model';
import { SaiyanAdapter } from './models/saiyan-adapter.model';
import { NamekianAdapter } from './models/namekian-adapter.model';
import { HumanAdapter } from './models/human-adapter.model';
import { PureRace } from './interfaces/pure-race.interface';

const gohan = new SaiyanAdapter(new Saiyan());
const vegeta = new SaiyanAdapter(new Saiyan());
const piccolo = new NamekianAdapter(new Namekian());
const krilin = new HumanAdapter(new Human());

const everybody = [gohan, vegeta, piccolo, krilin];

const genki = everybody.reduce(
  (power: number, pureRace: PureRace) => power + pureRace.genki(),
  0
);
console.log(`everybody attack: ${genki}`);

I have created two npm scripts that run the two examples shown here after applying the Adapter pattern.

npm run example1
npm run example2

Conclusion

Adapter Pattern can avoid duplicated code in your project, when there are class that can work together but their interface are not compatible. In this post you have been able to have a look a simple implementation using JavaScript/TypeScript language.

The most important thing has not implement the pattern as I have shown you, but to be able to recognise the problem which this specific pattern can resolve, and when you may or may not implement said pattern. This is crucial, since implementation will vary depending on the programming language you use.

More more more...