Mastering the Adapter Design Pattern
With Typescript, Node.js and Express.js
Imagine this situation: you’re working on an application that downloads financial information from various sources and presents it in attractive graphics. Suddenly, you come across a powerful analysis library but designed to handle data in a completely different format from what you’re using. This dilemma is more common than you might think, and this is where the Adapter Pattern comes into play.
As we progress, we will learn about the challenges that the Adapter pattern solves, its essential purpose, and how its elegant structure enables collaboration between objects with initially incompatible interfaces.
Join me in this article to delve into the Adapter Pattern and discover how you can apply it to enhance your skills in software development.
When Interfaces Don’t Match
In the universe of software development, the concept of collaboration between components is crucial. Suppose you’re building an application that collects financial data from various sources and presents it attractively to users.
However, in your quest to enhance your application’s functionality, you come across an analysis library that is extremely powerful.
This library could elevate your application to the next level, but there’s a significant obstacle: this library only works with data in a completely different format from what you’ve been using.
This scenario raises a critical question: how can you make the most of this analysis library when its interface is incompatible with the data structure you’re handling? This is where the Adapter Pattern comes into play.
The Dilemma of Incompatible Interfaces
Interfaces are invisible contracts that allow software components to communicate effectively with each other.
However, when these interfaces differ among components that need to work together, compatibility issues arise.
In this case, you’re dealing with a true paradox: you have a valuable tool that could boost your application, but its inability to speak the same “language” as your existing components poses an insurmountable barrier.
Practical Example: The Stock Market
This app fetches crucial financial information from different sources, like internal data sources and external web services. Each source provides data in a particular format — some use XML, while others use JSON.
In your pursuit of enhancing the quality of your analysis and graphics, you come across a third-party analysis library that’s exceptionally powerful. However, this library works only with data in JSON format.
Herein lies the problem: your existing components work with data in XML format, creating an incompatibility between the library and your data.
This conflict of incompatible interfaces is a common challenge in software development. The Adapter Pattern offers an elegant solution to address this issue and enables objects with different interfaces to work together harmoniously.
Solution: Adapting Interfaces
Faced with the challenge of incompatible interfaces, the Adapter Pattern emerges as a clever and effective solution.
This structural design pattern focuses on enabling objects with incompatible interfaces to collaborate harmoniously, without affecting the existing code due to these differences.
Essentially, the Adapter acts as an intermediary that translates requests from one object into terms that the other object can understand, providing a common interface that facilitates collaboration.
The Concept of the Adapter
Visualize you have an analysis library that only works with data in JSON format, but your existing components handle data in XML format.
The solution of the Adapter Pattern involves creating a special object called an “adapter.”
This adapter wraps one of the objects and acts as a translator between the incompatible interfaces. The wrapped object doesn’t even need to be aware of the adapter’s existence, as the adapter hides the complexity of the conversion that occurs behind the scenes.
Class Adapter and Object Adapter
There are two main approaches to implementing the Adapter Pattern: class adapter and object adapter.
Each approach has its own advantages and disadvantages, and the choice depends largely on the context and specific project needs.
Class Adapter
In the class adapter implementation, the adapter publicly inherits from the target interface and privately from the class that needs to be adapted.
This approach is especially useful when you want to adapt a specific class to the target interface.
However, it has limitations as it doesn’t allow adapting an entire hierarchy of subclasses of the adaptable class.
Object Adapter
The object adapter relies on object composition. In this approach, the adapter implements the target interface and wraps an adaptable object.
The adapter acts as a bridge between the client and the adaptable object, translating requests as necessary.
This approach is more flexible as it can adapt multiple subclasses of the adaptable object and add functionalities that the adaptable object doesn’t provide.
Addressing the Stock Market Dilemma
Let’s return to the stock market scenario. To solve the problem of incompatible interfaces between your application and the analysis library, you can create XML-to-JSON adapters for each class within the library.
These adapters allow your code to communicate with the library only through these common interfaces.
When an adapter receives a call, it translates the incoming XML data into a JSON structure and then invokes the appropriate methods in the analysis library.
Structure of the Adapter Pattern
The Adapter Pattern consists of several key participants working together to achieve interface adaptation and effective collaboration between components.
These participants play specific roles that enable the pattern to function cohesively.
Let’s explore each of them:
Pattern Participants Target
Target: Defines the specific domain interface that the Client uses. In other words, it represents the common interface to which incompatible objects must be adapte
Client: Collaborates with objects that conform to the Target interface. The Client interacts with objects through the Target interface, without worrying about interface incompatibility between the actual components.
Adaptee: Defines an existing interface that needs to be adapted. In many cases, this interface doesn’t match the Target interface required by the Client. The Adaptee is the object we want to use, but its interface is incompatible.
Adapter: Implements the Target interface and wraps an Adaptee object. The Adapter acts as an intermediary that translates calls from the Target interface into operations on the Adaptee object. This way, the Client can interact with the Adapter as if it were a Target object.
In-Depth Implementation
Now it’s time to delve into the concrete implementation of the Adapter Pattern. We’ll explore the two main approaches: class adapter and object adapter.
Each approach has its place and is useful depending on the context in which it’s applied. Join us on this journey through the detailed steps to implement both approaches.
Class Adapter
The class adapter is suitable when we want to adapt a specific class to the target interface. Here’s a step-by-step guide to its implementation:
- Definition of the Target Interface: Begin by defining the target interface that the Client will use. This interface should be common and consistent with the problem context.
- Creation of the Adaptee Class: Define the class you want to adapt (Adaptee). This class already exists and has an interface that’s incompatible with the target interface.
- Creation of the Adapter: Create a new Adapter class that inherits from the target interface and the Adaptee class. This ensures that the adapter is a subtype of the target interface.
- Implementation of the Adapter: In the Adapter class, implement the methods of the target interface. These methods should translate calls to the methods of the Adaptee class.
- Interaction with the Client: The Client interacts with the adapter through the target interface, without needing to know about the existence of the Adaptee class.
Object Adapter
The object adapter is more flexible and allows adapting multiple subclasses of the Adaptee class. Let’s see how to implement it:
- Definition of the Target Interface: Just like in the previous approach, start by defining the target interface that the Client will use.
- Creation of the Adaptee Class: Define the Adaptee class with its existing interface.
- Creation of the Adapter: Create a new Adapter class that implements the target interface and contains an instance of the Adaptee class.
- Implementation of the Adapter: In the Adapter class, implement the methods of the target interface using the methods of the Adaptee class as needed.
- Interaction with the Client: The Client interacts with the object adapter through the target interface, just like in the case of the class adapter.
To illustrate these approaches, let’s consider an example in the context of a the Stock Market.
Get Diego Motta | Fullstack Developer’s stories in your inbox
Join Medium for free to get updates from this writer.
Suppose we have a PaymentProcessor class that handles payments in XML format, and we need to adapt it to work with an external library that operates in JSON format.
Class Adapter
Let’s focus on the class adapter. Following the mentioned steps, we create a JsonPaymentAdapter class that inherits from the PaymentProcessor interface and also contains an instance of the external library that handles payments in JSON format. We implement the methods of the target interface in JsonPaymentAdapter, translating calls to the corresponding methods of the JSON library.
Object Adapter
Now, let’s focus on the object adapter. We create a JsonPaymentProcessorAdapter class that implements the PaymentProcessor interface and contains an instance of the JsonPaymentProcessor class from the JSON library. We implement the methods of the target interface using the methods of the JsonPaymentProcessor class.
Both approaches achieve the same goal: allowing the payment processing system to work with XML and JSON formats seamlessly. The choice between the class adapter and object adapter depends on the specific project requirements and needed flexibility.
Implementing the Adapter Pattern in TypeScript with Node.js and Express.js
Conceive you’re working on a project that needs to combine data from different sources, like APIs, to present it in a unified manner.
The challenge is that these APIs have incompatible data formats — one returns XML and the other returns JSON. This is where the Adapter pattern comes into play to make these APIs work together seamlessly.
First, we create the XmlApi and JsonApi classes in the apis directory to simulate fetching data from the APIs. One API returns XML and the other JSON. As we proceed, make sure you understand how these basic APIs work.
Now, in the adapters directory, we create adapters to convert the API data into a unified format. For the XML API, we create XmlApiAdapter, and for the JSON API, we have JsonApiAdapter. These adapters take instances of the original APIs and tailor them to provide data in the format we need.
We made a file called unifiedData.ts to define an interface named UnifiedData. This interface sets how the unified data format should look, regardless of the underlying data source. This ensures that all adapters generate consistent data.
Finally, in app.ts, we use Express to create a server that responds to the /xml and /json routes. When a client accesses these routes, we create instances of the original APIs and their corresponding adapters. The adapters process the API data and present it in the unified format defined by the UnifiedData interface. Then, we send this adapted data as a response to the client.
This implementation demonstrates how the Adapter pattern makes it easy to integrate components with incompatible interfaces.
Structure files
adapter-pattern-example/
│
├── apis/
│ ├── xmlApi.ts
│ └── jsonApi.ts
│
├── adapters/
│ ├── xmlApiAdapter.ts
│ └── jsonApiAdapter.ts
│
├── unifiedData.ts
├── app.ts
└── package.json
Let’s break down the contents of each directory and file:
apis/: This directory contains the classes representing the existing APIs.xmlApi.ts: Contains theXmlApiclass.jsonApi.ts: Contains theJsonApiclass.adapters/: This directory contains the adapter classes that adapt the APIs to the unified data format.xmlApiAdapter.ts: Contains theXmlApiAdapterclass.jsonApiAdapter.ts: Contains theJsonApiAdapterclass.unifiedData.ts: Contains the interface definition for the unified data format.app.ts: This is the main application file that sets up the Express server and defines the routes.package.json: Contains the dependencies and configuration for the Node.js application.
The content of each file in the directory structure:
apis/xmlApi.ts:
// apis/xmlApi.ts
export class XmlApi {
fetchData(): string {
// Simulating XML data retrieval
return '<data><value>XML Data</value></data>';
}
}apis/jsonApi.ts:
// apis/jsonApi.ts
export class JsonApi {
fetchData(): object {
// Simulating JSON data retrieval
return { value: 'JSON Data' };
}
}adapters/xmlApiAdapter.ts:
// adapters/xmlApiAdapter.ts
import { XmlApi } from '../apis/xmlApi';
import { UnifiedData } from '../unifiedData';
export class XmlApiAdapter implements UnifiedData {
private xmlApi: XmlApi;
constructor(xmlApi: XmlApi) {
this.xmlApi = xmlApi;
}
getValue(): string {
const xmlData = this.xmlApi.fetchData();
// Logic to parse and extract value from XML data
return 'Adapted ' + xmlData;
}
}adapters/jsonApiAdapter.ts:
// adapters/jsonApiAdapter.ts
import { JsonApi } from '../apis/jsonApi';
import { UnifiedData } from '../unifiedData';
export class JsonApiAdapter implements UnifiedData {
private jsonApi: JsonApi;
constructor(jsonApi: JsonApi) {
this.jsonApi = jsonApi;
}
getValue(): string {
const jsonData = this.jsonApi.fetchData();
// Logic to extract value from JSON data
return 'Adapted ' + jsonData.value;
}
}unifiedData.ts:
// unifiedData.ts
export interface UnifiedData {
value: string;
}app.ts:
// app.ts
import express from 'express';
import { XmlApi } from './apis/xmlApi';
import { JsonApi } from './apis/jsonApi';
import { XmlApiAdapter } from './adapters/xmlApiAdapter';
import { JsonApiAdapter } from './adapters/jsonApiAdapter';
const app = express();
app.get('/xml', (req, res) => {
const xmlApi = new XmlApi();
const xmlAdapter = new XmlApiAdapter(xmlApi);
const value = xmlAdapter.getValue();
res.send(value);
});
app.get('/json', (req, res) => {
const jsonApi = new JsonApi();
const jsonAdapter = new JsonApiAdapter(jsonApi);
const value = jsonAdapter.getValue();
res.send(value);
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});Benefits and Drawbacks of the Adapter Pattern
The Adapter Pattern offers several advantages, making it a valuable tool in a developer’s toolkit. However, like any design pattern, it also comes with certain drawbacks.
Let’s take a closer look at the benefits and potential downsides of the Adapter Pattern.
Benefits
- Code Reusability: The Adapter Pattern facilitates the reuse of existing code. Instead of rewriting or modifying incompatible components, you can adapt them using adapters, preserving their functionality and minimizing the need for major changes.
- Interoperability: One of the primary benefits of the Adapter Pattern is enabling interoperability between components with disparate interfaces. It allows components developed independently to collaborate seamlessly, saving time and effort.
- Runtime Interface Adaptation: The Adapter Pattern allows for dynamic adaptation of interfaces during runtime. This is particularly useful when dealing with components that need to interact with various systems or libraries that have different interfaces.
- Open-Closed Principle: The pattern adheres to the Open-Closed Principle, a fundamental principle in software design. Adapters can be added or modified without altering the existing codebase, promoting code stability and maintainability.
Drawbacks
- Added Complexity: Introducing adapters adds an extra layer of complexity to the system. While they solve the interface incompatibility problem, they can introduce additional code that needs to be managed and understood.
- Maintenance Overhead: As the system evolves, adapters might need to be updated to accommodate changes in the adapted components or interfaces. This maintenance overhead can become significant in large systems with numerous adapters.
- Performance Considerations: Adapters may introduce a slight performance overhead due to the additional method calls and translation logic. While this overhead is usually negligible, it’s important to consider in performance-critical applications.
- Potential Overuse: It’s possible to overuse the Adapter Pattern, creating unnecessary layers of abstraction and making the codebase more complex than it needs to be. Careful consideration should be given to whether an adapter is truly necessary.
Conclusion
The Adapter Pattern is a powerful tool that solves the problem of integrating components with incompatible interfaces.
It enables code reusability, promotes interoperability, and offers the flexibility to adapt interfaces at runtime. However, developers should be mindful of the potential complexity and maintenance challenges introduced by the use of adapters.
As with any design pattern, it’s important to strike the right balance and apply the Adapter Pattern judiciously where it adds the most value.
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Dive Into Design Patterns, Retrieved from https://refactoring.guru/design-patterns/book
🧑💻 I’m Diego. As a Fullstack Developer with over 9 years of experience in the IT industry.
Let’s Connect!
📢 Follow me on social media for more insights, updates, and discussions:
I love sharing about:
📚Books
💻 Web Development
🌐 JavaScript, React & Node
🛠️ Tips for IT Interviews
See you!
Stackademic
Thank you for reading until the end. Before you go:
- Please consider clapping and following the writer! 👏
- Following us on Twitter(X), LinkedIn, and YouTube.
- Visiting Stackademic.com to find out more about how we are democratizing free programming education around the world.










