Do you have a library in your application that is highly likely to be replaced? Maybe you have a significant piece of code that is used repeatedly and comes from external dependencies? Or the new library blandly matches old calls in your code? Such problems are answered by the Adapter pattern.
The operation of the Adapter pattern is to create a new consistent interface with system components, improving code readability and modularity.
Let's assume that in a hypothetical application we use a library to display notifications.
// ReactComponent.tsx
import notification from "notification-lib"
export const ReactComponent = () => {
const handleData = () => {
/// some JS magic here ///
notification.show({success:'Data sent'})
}
return (
<div>
<button onClick={handleData}>Send Data</button>
</div>
);
};
In case we have many such components then replacing the library becomes time-consuming.
Using the Adapter pattern, we will create a class that will override the library with an additional abstraction and create a versatile interface.
// NotificationAdapter.ts
import notification from "notification-lib";
class NotificationAdapter {
static success(message: string) {
notification.show({ success: message });
}
static error(message: string) {
notification.show({ error: message });
}
static warning(message: string) {
notification.show({ warning: message });
}
}
export default NotificationAdapter;
Now we can use the Adapter in the component.
// ReactComponent.tsx
import NotificationAdapter from "./NotificationAdapter";
export const ReactComponent = () => {
const handleData = () => {
/// some JS magic here ///
NotificationAdapter.success('Data sent');
}
return (
<div>
<button onClick={handleData}>Send Data</button>
</div>
);
};
Unit test of such an Adapter in Vitest.
// NotificationAdapter.test.ts
import notification from "notification-lib";
import NotificationAdapter from "./NotificationAdapter";
import { describe, it, expect, vi, afterEach } from "vitest";
vi.mock("notification-lib", () => ({
show: vi.fn(),
}));
describe("NotificationAdapter", () => {
afterEach(() => {
(notification.show as ReturnType<typeof vi.fn>).mockClear();
});
it("should call notification.show with success message", () => {
NotificationAdapter.success("Data sent");
expect(notification.show).toHaveBeenCalledWith({ success: "Data sent" });
});
it("should call notification.show with error message", () => {
NotificationAdapter.error("An error occurred");
expect(notification.show).toHaveBeenCalledWith({ error: "An error occurred" });
});
it("should call notification.show with warning message", () => {
NotificationAdapter.warning("This is a warning");
expect(notification.show).toHaveBeenCalledWith({ warning: "This is a warning" });
});
});
Advantages:
- With this solution, we already make updates to the notification library only in the Adapter.
- We have a dedicated place where we define how notifications are called, separating the library from the application logic.
Disadvantages:
- We introduce an additional level of abstraction that can be confusing for new team members.
- When using the Adapter, we are often limited to only the most commonly used functions, so when using a class specifically, the Adapter should be extended.
Go back to Articles