Adapter Design Pattern

Erland Muchasaj
5 min readJan 2, 2024

--

A Comprehensive Guide with Examples and Use Cases

A Comprehensive Guide with Examples and Use Cases
A Comprehensive Guide with Examples and Use Cases

Design patterns are crucial in object-oriented programming, providing reusable solutions to common software design problems.

In the previous article, we described a design pattern and the types of design patterns. If you missed it, you can check it out here.

Today we will talk about a Structural Design Pattern, more specifically Adapter Design Pattern.

Definition

The adapter pattern is a software design pattern that allows the interface of an existing class to be used as another interface.

It tries to solve the problem of making two (or more) incompatible classes compatible by using an intermediate class that implements a common interface. It allows objects of incompatible interfaces to collaborate.

Problem

Let’s suppose you are building a product feed application.
The app downloads the feeds from different E-commerce API sources (for example: Shopify, BigCommerce, WooCommerce, and Prestashop), processes them, and saves them in a unified products table that will be ready to process and send to different marketplaces (eBay, Amazon, GoogleShopping, Wish etc).

The issue is that not all e-commerces have the same API and return the same payload. So we need something generic to connect each API from e-commerces to our platform.

Another issue is what happens if later we need to add a new product feed source for example Magento?

Solution

Adapter pattern to the rescue. It consists of 4 building blocks.

  • Client 💻: This is the Class, that uses the Target interface, and wants to connect to multiple sources. This would be ProductFeed in our example. It contains the existing business logic of the program.
  • Target 🎯: An interface or contract that defines a single API the Client will interact with. This would be in FeedAdapterInterface our example.
  • Adapter 🔌: A class that implements the Target interface, Holds an instance of Adaptee, and Adapts the calls from the Target interface to the adaptee’s interface. This would be in ShopifyApiAdapter in our example.
  • Adaptee 📑: A source the Client wants to connect to that has an incompatible interface. All requests get delegated to the adaptee. In our example we can have three: ShopifyApi, BigCommerceApi & WooCommerceApi.
Adapter pattern UML schema
Adapter pattern UML schema

A more real-world analogy is when you travel from one country to another, often happens that when you go to your hotel and try to charge your phone just to find out that the wall socket is different from your home country, for example, if you are traveling from the UK to another country in Europe or vice-versa definitely you have encountered this problem. It can be solved by using an Adapter so you plug your charger into the adapter and the adapter into the wall socket and the problem is solved.

Adapter pattern schema
Adapter pattern schema

Code

First, we will define the interface that the Client class (ProductFeed) will implement. It will have a single method getFeed() that returns an array of products.

interface FeedAdapterInterface
{
/**
* @return array<int, array<string, string>>
*/
public function getFeed(): array;
}

Now suppose, as we mentioned above, we want to connect to different product feeds, such as ShopifyApi, BigCommerceApi & WooCommerceApi.

// Adaptee Classes aka Service classes

class ShopifyApi {
public function fetchItems(): array {
// Returns an `array` of products, that hold a `title`, `date`, `image` and `url`.
}
}

class BigCommerceApi {
public function getProducts(): array {
// returns an array of products tha have 'name', 'published_at', 'picture_url', 'url'
}
}

class WooCommerceApi{
public function getList(): array {
// returns an array of products tha have 'name', 'published_date', 'cover', 'link'
}
}

As you can see each API service has a different method name and also returns different formats of payloads.

Now we will create the Adapters that implement the FeedAdapterInterface and implement the getFeed() method and delegate the calls from the Client class ProductFeed to the specific Adaptee service for example ShopifyApi.

class ShopifyApiAdapter implements FeedAdapterInterface
{
public function __construct(public ShopifyApi $api) {}

public function getFeed(): array
{
return array_map(
fn (array $item) => [
'title' => $item['title'],
'date' => \DateTime::createFromFormat('H:i:s Y-m-d', $item['date'])->format('Y-m-d H:i:s'),
'img' => $item['image'],
'url' => $item['url'],
],
$this->api->fetchItems(), // <==
);
}
}
class BigCommerceApiAdapter implements FeedAdapterInterface
{
public function __construct(public BigCommerceApi $api) {}

public function getFeed(): array
{
return array_map(
fn (array $item) => [
'title' => $item['name'],
'date' => \DateTime::createFromFormat('H:i:s Y-m-d', $item['published_at'])->format('Y-m-d H:i:s'),
'img' => $item['picture_url'],
'url' => $item['url'],
],
$this->api->getProducts(), // <==
);
}
}
class WooCommerceApiAdapter implements FeedAdapterInterface
{
public function __construct(public WooCommerceApi $api) {}

public function getFeed(): array
{
return array_map(
fn (array $item) => [
'title' => $item['name'],
'date' => \DateTime::createFromFormat('H:i:s Y-m-d', $item['published_date'])->format('Y-m-d H:i:s'),
'img' => $item['cover'],
'url' => $item['link'],
],
$this->api->getList(), // <==
);
}
}

Now we create the Client class (ProductFeed) that implements the Target interface (FeedAdapterInterface) and that will use these adapters (ShopifyApiAdapter, BigCommerceApiAdapter, WooCommerceApiAdapter) to connect to service classes (ShopifyApi, BigCommerceApi & WooCommerceApi).

class ProductFeed
{
/**
* @param FeedAdapterInterface[] $adapters
*/
public function __construct(public array|mixed $adapters) {}

public function getAllProducts(): iterable
{
$adapters= is_array($this->adapters) ? $this->adapters: func_get_args();

foreach ($adapters as $adapter) {
yield from $adapter->getFeed();
}
}
}

We loop over each adapter and yield their result so we can have a single and continuous stream of data as a generator.

You can read more about the generators here:

Usage

Our ProductFeed gets the list of adapters injected in its constructor.

$shopify= new ShopifyApiAdapter(new ShopifyApi());
$wooCommerce= new WooCommerceApiAdapter(new WooCommerceApi());
$bigCommerce= new BigCommerceApiAdapter(new BigCommerceApi());

$productFeed= new ProductFeed([
$shopify,
$wooCommerce,
$bigCommerce
]);

foreach ($productFeed->getAllProducts() as $products) {
var_dump($products);
}

Benefits

It helps you separate the interface or data conversion code from the primary business logic of the program.

You can introduce new types of adapters, without having to change your code on the Client class.

The code maintains the Single Responsibility Principle, Open/Close Principle, and Dependency Inversion Principle.

Other Examples:

Other examples where you can use the Adapter Design Pattern are:

  1. Getting news feeds from different sources (Twitter, FB, Instagram, etc).
  2. Sending notifications through different channels (SMS, Slack, Push, etc).
  3. Using different cache drivers (Redis, MemCache, etc).
  4. Using different file storage adapters (Local, S3, Google, etc).
  5. Logger.
  6. Database Clients (MySQL, PostgreSQL, MongoDB, etc).

Feel free to Subscribe for more content like this 🔔, clap 👏🏻 , comment 💬, and share the article with anyone you’d like

And as it always has been, I appreciate your support, and thanks for reading.

--

--