Structural Design Pattern Series | Adapter

Ali Mohammad
Stackademic
Published in
5 min readApr 15, 2024

--

In this series, we will look at the structural design patterns:

  1. Composite
  2. Adapter
  3. Bridge
  4. Decorator
  5. Facade
  6. Flyweight
  7. Proxy

🤔 What is it?

The adapter pattern is a structural pattern that allows objects with incompatible interfaces to collaborate.

Refactoring Guru

To further explain it, it is a pattern that let’s you adapt to new things without losing old functionality.

You might say here, but why do we even need the old functionality if we have new requirements? True, but sometimes the new requirements will require you to go live in phases, and these phases might require weeks, months, and maybe, if you are unlucky, years.

Let’s see the structure of the Adapter pattern

Here, we can see 2 use cases of the adapter pattern: the first is the regular old one where the client is already implementing the old business logic within itself, and the second is implementing it with a service.

To break it a bit

Client

A class that contains the existing business logic of the program

Client Interface/Service Interface

A protocol that other classes must follow to be able to unify the functionality

Service

The service is where we are implementing the old/new business logic.

Adapter

For the first one, a class that is able to work with both the client and the service implements the client interface while wrapping the service object.

For the second one, a class will reference the adapter to be able to work with both functionalities.

🧑🏽‍💻Let’s get our hands dirty

I guess enough small talk; let’s get down to business with a little scenario.

We have a small React application that sends in-app notifications to users on desktop.

Days came by, and our product manager, Bilal, came to us and gave us new requirements through a Jira ticket: We need to start sending notifications through the new .Net API alongside the old functionality.

Our tech lead, Canaan, says there is no problem and has delegated the task to us.

Okay, so let’s make our basic high-level system design before we even begin to code, so that we don’t shoot ourselves in the foot.

There will be context that will provide us access to the adapter, which we will call the sendNotification function. The adapter will be responsible for adapting to the new functionality without losing the old one.

First, lets create our services interface

// notify.interface.ts or you can do it with notify.d.ts
export interface INotify {
sendNotification: () => void;
}

// And here we will create a device type that will determine which service to send the notification to
export enum DeviceType {
MOBILE_ONLY,
DESKTOP_ONLY,
}

Then let’s define our services

// notify.service.ts
export class NotifyService implements INotify {
async sendNotification() {
// Code to reach out to the node api and send the request
}
}

// one-signal.service.ts
export class OneSignalService implements INotify {
async sendNotification() {
// Code to reach out to the node api and send the request
}
}

Then we create our adapter

export class ServiceAdapter {
// we can definitely create a good old constructor that
// will be fed the service from the context level,
// but i think this will be more applicable so that we
// seperate our client from the buisness logic

const notificationService = {
[DeviceType.MOBILE_ONLY]: new OneSignalService(),
[DeviceType.DESKTOP_ONLY]: new NotifyService(),
}

async sendNotification(deviceType: DeviceType) {
if (deviceType in notificationService)
await notificationService[deviceType].sendNotification();
// throw an error or handle the not implemented device
}

}

We create the adapter Context

// Adapter Context
const AdapterContext = createContext();

const useNotificationAdapter = () => {
const adapter = useContext(AdapterContext);
if (!adapter) {
throw new Error('useNotificationAdapter must be used within AdapterProvider');
}
return adapter;
};

const AdapterProvider = ({ children }) => {
const notificationAdapter = new ServiceAdapter();
return (
<AdapterContext.Provider value={notificationAdapter}>
{children}
</AdapterContext.Provider>
);
};

Then we use it

// SendNotification Page 
const SendNotificationPage = () => {
const notificationAdapter= useNotificationAdapter();
const deviceType = useDeviceType();

// We can call it with a use effect
useEffect(() => {
const sendNotification = async () => {
notificationAdapter.sendNotification(deviceType);
}
sendNotification();
}, [deviceType]);

// Or we can call it on a button click. It's really up to you
const handleSendNotification = async () => {
await notificationAdapter.sendNotification(deviceType)
}

return (
<div>
{/* Some JSX */}
</div>
);
};

And in our App

// Usage in App Component
const App = () => {
return (
<AdapterProvider>
<SendNotificationPage />
</AdapterProvider>
);
};
export default App;

Great, right?

The Fast Saga A clip from Fate Of The Furious

✈️ Another use case

Another great use of the adapter pattern in React is when we want to use third-party libraries that are incompatible, so we introduce a wrapper around them. This wrapper will serve as the adapter, ensuring that the app is stable around this wrapping 🌯

We can achieve this wrapping by

  1. Component Wrapper: To wrap a library component
  2. Function Wrapper: To wrap library functions

Another great way is to adapt to the BE responses. Sometimes, as FE developers, we are getting some out-of-this-world BE JSON responses that we have to fight and deal with, unfortunately. Stupid, i know but sometimes this is the case 😕

I hope you had fun reading and learning about the adapter pattern! ❤️

Stackademic 🎓

Thank you for reading until the end. Before you go:

--

--