Adapter Design Pattern

About The Pattern

An Adapter is a "wrapper" that wraps around an interface. Why do we need this? It could be because we are trying to use foreign code and it doesn't fit in nicely with our codebase.

To make it play nicely, we'd want to wrap it in a more familiar interface. Now our code will interact with the wrapper, and internally the wrapper will use the foreign code under the hood.

Essentially, the Adapter pattern consists of 4 parts. The client, which wants to interface with the adaptee the same way it interfaces with the existing target. But it cannot do so unless we create an adapter for that target.

Intro

We'll be using some anime references in this example. Hope you'll manage to follow along.

Imagine we are Game Developers and we are creating a new game with Heros from One Punch Man. We thought it'll be fun to bring characters from our old Street Fighter game into this new game, but those characters weren't designed to fit in this new game.

We could go sprinkly code in our project to handle these 2 different character types but we're better than that.

Let's get started with them individually and then bring it all together at the end.

The Target

Introducing... the Target, our Hero.

interface Hero {
  normalAttack(): number;
  specialAttack(): number;
}

Our Hero is very simple, they can only do 2 things normalAttack and specialAttack.

Now let's create 2 Heros.

class Saitama implements Hero {

  normalAttack() {
    return this.normalPunch();
  }

  specialAttack() {
    return this.seriousPunch();
  }
}

class Genos implements Hero {

  normalAttack() {
    return this.machineGunBlow();
  }

  specialAttack() {
    return this.incinerationCannon();
  }
}

As you can see, the two of them have different normal attacks and special attacks as they should.

Now for whatever reason, we want to throw some Street Fighter characters into this world of Heros.

The Adaptee

interface StreetFighter {
  specialMove(): number;
  ultimateMove(): number;
}

The Street Fighter is quite similar to our Hero above. But sadly, not as similar as we might have hoped for since they have different attack functions.

Let's create our two Street Fighters to continue with the example.

class Ryu implements StreetFighter {

  specialMove() {
    return this.shoryuken();
  }

  ultimateMove() {
    return this.shinkuHadoken();
  }
}

class Ken implements StreetFighter {

  specialMove() {
    return this.whirlwindKick();
  }

  ultimateMove() {
    return this.godMartialAnnihilator();
  }
}

They got nice moves, but our client

The Adapter

Everything will be much simpler if everyone was a Hero so we can just use normalAttack and specialAttack. But Street Fighters don't have these methods. We don't want to be sprinkling in if statements to detect if the character implements StreetFigther or Hero and then act accordingly. Here is where the Adapter pattern comes in handily.

Let's create a Street Fighter Adapter class that can turn any Street Fighter into a Hero.

class StreetFighterAdapter implements Hero {
  private streetFighter: StreetFighter;

  constructor(streetFighter: StreetFighter) {
    this.streetFighter = streetFighter;
  }

  normalAttack() {
    this.streetFighter.specialMove();
  }

  specialAttack() {
    this.streetFighter.ultimateMove();
  }
}

Let's put our adapter to use.

The Client

const streetFighterRyu = new Ryu();

// The following won't work since
// Street Fighters don't have Special Attacks
streetFighterRyu.specialAttack(); 

const ryu = new StreetFighterAdapter(streetFighterRyu);

// Now with our adapted version, this works like a charm
// This will trigger Street Fighters Ultimate Move
ryu.specialAttack();

Now let's make a random hero and have them show off their special attack.

const saitama = new Saitama();
const genos = new Genos();
const ryu = new StreetFighterAdapter(new Ryu());
const ken = new StreetFighterAdapter(new Ken());

const heros: Hero[] = [saitama, genos, ken, ryu];

const getRandomHero = (heros: Hero[]) => {
  const randomIndex = Math.floor(Math.random() * heros.length);
  return heros[randomIndex];
};

const hero = getRandomHero(heros);
hero.specialAttack();

End

Look at that beauty, since they all implement Hero, we don't care which hero we get back. Special attack will always work 👊