The Adapter Pattern in JavaScript (ES6+): Making Incompatible Stuff Work Together

The Adapter Pattern in JavaScript (ES6+): Making Incompatible Stuff Work Together

Alright, let’s talk about the Adapter Pattern. If you’ve ever been in a situation where you’re trying to use some existing code or a library, but its interface just doesn’t match what your app needs, you know how frustrating that can be. The Adapter Pattern is here to save the day. It’s like a universal adapter for your code—making incompatible interfaces work together without breaking a sweat.


What’s the Adapter Pattern All About?

The Adapter Pattern is all about converting the interface of a class or module into something your app can work with. It’s also known as the Wrapper Pattern because, well, it wraps the incompatible stuff and makes it compatible. Think of it like a USB-C to HDMI adapter—it lets you connect things that weren’t designed to work together.


The Problem: When Things Don’t Fit

Let’s say you’re building a cool drawing app in JavaScript. Your app uses a base class called Shape to handle all the graphical elements like lines, polygons, and text. Here’s what the Shape class might look like:

class Shape {
  boundingBox() {
    throw new Error("Method not implemented.");
  }

  createManipulator() {
    throw new Error("Method not implemented.");
  }
}        

Now, imagine there’s a third-party library with a TextView class that’s perfect for handling text display and editing. But here’s the catch: TextView doesn’t have the methods your app expects (boundingBox and createManipulator). It looks something like this:

class TextView {
  getOrigin(x, y) {
    console.log("Getting text origin...");
  }

  getExtent(width, height) {
    console.log("Getting text dimensions...");
  }

  isEmpty() {
    console.log("Checking if text is empty...");
    return false;
  }
}        

So, how do you use TextView in your app without rewriting it or breaking everything? That’s where the Adapter Pattern comes in.


The Solution: Adapter to the Rescue

The Adapter Pattern lets you create a middleman class (the adapter) that translates the interface of TextView into something your app can use. In JavaScript, we usually go with the Object Adapter approach, which uses composition instead of inheritance. Here’s how it works:


UML Diagram

Article content

Code Example: Object Adapter in JavaScript

Here’s how you can implement the Adapter Pattern in modern JavaScript (ES6+):

// Target interface (what your app expects)
class Shape {
  boundingBox() {
    throw new Error("Method not implemented.");
  }

  createManipulator() {
    throw new Error("Method not implemented.");
  }
}

// Adaptee (the incompatible class)
class TextView {
  getOrigin(x, y) {
    console.log("Getting text origin...");
  }

  getExtent(width, height) {
    console.log("Getting text dimensions...");
  }

  isEmpty() {
    console.log("Checking if text is empty...");
    return false;
  }
}

// Adapter (the middleman)
class TextShape extends Shape {
  constructor(textView) {
    super();
    this.textView = textView; // Composition!
  }

  boundingBox() {
    const bottomLeft = { x: 0, y: 0 };
    const topRight = { x: 0, y: 0 };
    this.textView.getOrigin(bottomLeft.x, bottomLeft.y);
    this.textView.getExtent(topRight.x, topRight.y);
    console.log("Calculating bounding box...");
    return { bottomLeft, topRight };
  }

  createManipulator() {
    console.log("Creating a manipulator for text...");
    return new TextManipulator(this);
  }
}

// TextManipulator (just for demo purposes)
class TextManipulator {
  constructor(textShape) {
    this.textShape = textShape;
  }

  manipulate() {
    console.log("Manipulating the text shape...");
  }
}

// Client code
const textView = new TextView();
const textShape = new TextShape(textView);

textShape.boundingBox(); // Now it works!
textShape.createManipulator().manipulate(); // Added functionality        

Why Use the Adapter Pattern?

  1. Reusability: You can reuse existing code or libraries without modifying them.
  2. Flexibility: The Object Adapter approach works with any class, even if it’s not related by inheritance.
  3. Clean Code: Your app’s code stays clean and doesn’t get tangled up with the adaptee’s implementation details.


When Should You Use It?

  • When you need to use an existing class, but its interface doesn’t match your app’s requirements.
  • When you want to create a reusable class that can work with multiple unrelated classes.
  • When you need to adapt several classes, but creating subclasses for each one would be a pain.


Real-World Example: Adapting an API Response

Let’s say you’re working with a third-party API that returns data in a format your app can’t use. You can create an adapter to transform the data:

// Data from a third-party API
const thirdPartyData = {
  user_name: "JohnDoe",
  user_age: 30,
};

// Adapter to transform the data
class UserAdapter {
  constructor(data) {
    this.data = data;
  }

  get name() {
    return this.data.user_name;
  }

  get age() {
    return this.data.user_age;
  }
}

// Client code
const user = new UserAdapter(thirdPartyData);
console.log(`Name: ${user.name}, Age: ${user.age}`);        

Wrapping Up

The Adapter Pattern is like a Swiss Army knife for your code—it helps you connect things that weren’t designed to work together. Whether you’re dealing with third-party libraries, legacy code, or external APIs, the Adapter Pattern keeps your app flexible and your code clean.

So next time you’re stuck with incompatible interfaces, just remember: Adapter’s got your back.

To view or add a comment, sign in

Explore content categories