Traditionally, structural patterns are described as the thing that helps simplify the process of object composition. Its purpose is to help identify the relationship between different objects, making sure that if something changes within the system, it doesn’t force the entire system to change with it.

Let’s begin with the adapter pattern.

Adapter Pattern

Imagine you have to build another cart solution for your boss. But there’s a caveat — you’re not allowed to change the original code that spits out the cart data. Why? Because no one knows how that change will impact the rest of the application. It’s a fragile piece of art that no one wants to touch, not yet, or ever. The situation is one of those moments where you just don’t have the time or mental space to question it. You’ll just have to accept that it’s going to be like this until you fully transition out of your legacy systems.

But the process of transitioning is going to take a long time and you don’t want your code to be too intertwined with the old code. What do you do?

In situations like these, the adapter pattern comes in handy.

The adapter pattern introduces an intermediary piece of code that makes two parts of a system compatible with one another. It also injects an element of lose coupling by keeping the two pieces of code separate. It means that you can write your code however you want without the need to take the other piece of code into consideration. Your adapter code will do the necessary translation and give you what you need and in whatever format you want.

When one side of the code changes, you only need to change the adapter for that particular part rather than both sides of the application.

So how exactly do you write an adapter pattern in JavaScript?

There’s no actual way to write it as such. It’s more a conceptual idea and the code itself is dependent on the situation you’re trying to bridge. It’s a process of abstracting the data you need from an external or 3rd party object. To do this, we often create an interface to represent our adapter and create the connections required between the two parts.

One thing to take note is that the adapter only has a single direction of dependency. The adapter only consumes what it aims to translates. In most cases, it’s the legacy API or 3rd party libraries. It doesn’t do anything else other than form a connection between two sides of an application. It should not contain any business logic.

Conventionally, an adapter sits in a utils folder and then imported into a file when it's needed. However, it can also be a stand alone function.

Let’s take a look at the exported class version first.

//your original cart data service
 //this is just a hypothetical name
 import { v4 as cart } from "cartServiceV4"
 
 class CartServiceAdapter {
     getCart(){
         //some cart code 
         //using data from the imported cart service
         //can transform the original output to suit your needs
         return cart;
     }
     
     removeItemFromCart(){
         //some cart code
         return cart;
     }
     
     //etc
 }
 
 export default new CartService();

import brings in the thing you want to translate. export makes your CartServiceAdapter available for usage by anything that imports it.

When you need the cart data, you just need to import your adapter into your code using import.

import cart from "./utils/CartServiceAdapter"
 
 console.log(cart.getCart());

What this hypothetical code looks like in diagram form:

This means that rather than forcing your new cart code to conform to the needs of the mysterious original code, you can just write an adapter that translates the original to fit with the new code. As a result, you’re free to construct your code however you want.

You can also write your adapters as normal functions rather than a class. There are no restrictions on how you write it. Here’s an example of what a possible interface can look like as a function:

//original cart data that cannot be changed at source
 //let's pretend this is from a mysterious data service
 //we have no direct access to this
 var cart = [
   {item: "vintage clock", sku: 9284, value: 15.99},
   {item: "motivate carts", sku: 9232, value: 19.99}
 ]
 
 //old interface pulls in data from mysterious data service
 //we're not allowed to touch this
 function Cart(){
     return this.cart;
 }
 
 //our adapter code to translate the data
 //prevents us from consuming the Cart() directly
 function CartAdapter(){  
   var originalCart = Cart();  
   var adapterCart = originalCart.map(function(obj){
     return {
       item: obj.item,
       productId: obj.sku,
       price:obj.value
       
     }
   });
   
   return adapterCart;
 }
 
 //now we can use the result of the CartAdapter() however we want
 //changes in Cart() will result in only a change in the adapter
 //the rest of the code remains unimpacted once the adapter is fixed
 console.log(CartAdapter());

n the example above, we’re pretending that the cart data format cannot be changed that it's being pulled into the application via the Cart() function. Cart() can be seen as the old interface that we don't want to touch. CartAdapter() as the intermediary piece of code that prevents us from consuming Cart() directly. When something changes in Cart(), we only need to change CartAdapter() rather than the family of functions that may be consuming the data from Cart() directly.

The adapter pattern comes in handy for when you’re working across multiple platforms, data sources, libraries and frameworks. It fills in the gaps that are caused by the various requirements and differences in environments. The context of the original code may make sense, but not in the use case that it needs to be consumed. The adapter pattern makes this possible by bridging the different sources together.

Share this post