The Adapter Pattern is a Structural Design Pattern that allows two incompatible interfaces to work together. In other words, the Adapter acts as a bridge between two objects, enabling them to communicate without modifying their existing code. This pattern is especially useful when you want to integrate or use a class or module that has an interface different from the one you need.
The Adapter Pattern is commonly used to make existing classes work with others without changing their source code. It’s often used when integrating legacy systems or third-party libraries that don’t match the interface of the current application.
Key Characteristics of Adapter Pattern
- Bridging Incompatible Interfaces:
- The primary purpose of the Adapter Pattern is to make two incompatible interfaces compatible. The Adapter wraps an existing class and provides an alternative interface that the client can work with.
- Class Adapter vs Object Adapter:
- Class Adapter: This approach uses inheritance to implement the Adapter. However, JavaScript (and Node.js) uses prototypal inheritance, which makes class adapters less common in JavaScript.
- Object Adapter: This is more commonly used in Node.js. The adapter is implemented by composition, where one object wraps another, providing the desired interface.
- Reusability:
- The Adapter Pattern promotes code reuse by allowing you to reuse existing classes without modifying their implementation. This makes it easier to integrate new or existing classes into a system without breaking the system’s functionality.
- Single Responsibility:
- The Adapter Pattern follows the Single Responsibility Principle by isolating the logic of adapting interfaces in a separate class, leaving the existing components unchanged.
- Loose Coupling:
- Adapters decouple clients from the concrete implementations they rely on. This makes the system more flexible and easier to modify or extend in the future.
- Improves Legacy Code Integration:
- The Adapter Pattern is particularly helpful when working with legacy code or third-party libraries that you cannot modify directly but need to work within a modern system.
Adapter Pattern in Node.js
In Node.js, the Adapter Pattern can be especially useful when integrating modules or libraries that have interfaces that are incompatible with your application’s current requirements. Instead of modifying the module or library directly, you create an adapter that provides the interface needed by the application.
Example Scenario: Payment Gateway Adapter
Consider a scenario where your application supports multiple payment gateways (e.g., PayPal and Stripe). Each payment gateway has its own interface for processing payments. You want to standardize this interface so that your application can work with any payment gateway seamlessly.
Step 1: Define the Target Interface
This interface defines how your application expects to process payments.
// The target interface that the application uses
class PaymentProcessor {
processPayment(amount) {
throw new Error('processPayment() must be implemented');
}
}
Step 2: Create Adapters for Different Payment Gateways
We will create adapters for PayPal and Stripe to conform to the PaymentProcessor interface.
// PayPal API (third-party library)
class PayPal {
constructor() {
this.name = 'PayPal';
}
makePayment(amountInDollars) {
console.log(`Processing payment of $${amountInDollars} via PayPal`);
}
}
// PayPal Adapter to match PaymentProcessor interface
class PayPalAdapter extends PaymentProcessor {
constructor(paypal) {
super();
this.paypal = paypal;
}
processPayment(amount) {
this.paypal.makePayment(amount);
}
}
// Stripe API (third-party library)
class Stripe {
constructor() {
this.name = 'Stripe';
}
charge(amountInCents) {
console.log(`Processing payment of $${amountInCents / 100} via Stripe`);
}
}
// Stripe Adapter to match PaymentProcessor interface
class StripeAdapter extends PaymentProcessor {
constructor(stripe) {
super();
this.stripe = stripe;
}
processPayment(amount) {
this.stripe.charge(amount * 100); // Stripe uses cents
}
}
Step 3: Using the Adapters
Now that we have adapters for PayPal and Stripe, the application can use the PaymentProcessor interface to interact with both payment gateways seamlessly.
// Using PayPal adapter
const paypal = new PayPal();
const paypalAdapter = new PayPalAdapter(paypal);
paypalAdapter.processPayment(50); // Processing payment of $50 via PayPal
// Using Stripe adapter
const stripe = new Stripe();
const stripeAdapter = new StripeAdapter(stripe);
stripeAdapter.processPayment(75); // Processing payment of $75 via Stripe
Output:
Processing payment of $50 via PayPal
Processing payment of $75 via Stripe
Explanation:
- The
PayPalandStripeclasses have different interfaces for processing payments (makePaymentfor PayPal andchargefor Stripe). - The
PayPalAdapterandStripeAdapteradapt these interfaces to thePaymentProcessorinterface expected by the application. - The application can now process payments through both PayPal and Stripe without needing to worry about their different interfaces.
Real-World Examples of Adapter Pattern
Database Access:
In a multi-database application, the Adapter Pattern can be used to standardize access to different types of databases (e.g., MySQL, MongoDB, PostgreSQL). Each database type has its own API for querying data, but the Adapter Pattern can wrap these APIs in a unified interface so that the application can interact with them in a consistent manner.
class MySQLAdapter {
constructor(mysqlClient) {
this.mysqlClient = mysqlClient;
}
fetchData(query) {
return this.mysqlClient.executeQuery(query);
}
}
class MongoDBAdapter {
constructor(mongoClient) {
this.mongoClient = mongoClient;
}
fetchData(query) {
return this.mongoClient.runQuery(query);
}
}
File System Integration:
When integrating with different file systems (e.g., AWS S3, local file system, or Google Cloud Storage), the Adapter Pattern can be used to standardize the file system interactions, such as reading, writing, or deleting files.
class S3Adapter {
constructor(s3Client) {
this.s3Client = s3Client;
}
saveFile(filePath, data) {
return this.s3Client.upload(filePath, data);
}
}
class LocalFileSystemAdapter {
saveFile(filePath, data) {
const fs = require('fs');
return fs.writeFileSync(filePath, data);
}
}
Third-Party API Wrappers:
When integrating with third-party APIs that don’t match your application’s required interface, the Adapter Pattern can act as a wrapper. For instance, if an application needs to use a third-party social media API to post updates, but each API (Facebook, Twitter, Instagram) has different interfaces, adapters can provide a unified posting interface.
class FacebookAdapter {
constructor(facebookAPI) {
this.facebookAPI = facebookAPI;
}
postUpdate(status) {
return this.facebookAPI.createPost(status);
}
}
class TwitterAdapter {
constructor(twitterAPI) {
this.twitterAPI = twitterAPI;
}
postUpdate(status) {
return this.twitterAPI.tweet(status);
}
}
GUI Frameworks:
In GUI frameworks, the Adapter Pattern is often used to integrate new components with older legacy components. For example, if you’re integrating a modern UI framework (like React) with a legacy system, you might need an adapter to translate between the new system’s state management and the legacy system’s interface.
Legacy System Integration:
Many businesses rely on legacy systems, and integrating these systems with modern technology can be challenging due to differences in data formats or APIs. The Adapter Pattern allows legacy systems to be integrated without modifying their source code, making it easier to connect them with modern applications.
Conclusion
The Adapter Pattern is a valuable tool in modern software design that bridges the gap between incompatible interfaces, allowing disparate systems to work together seamlessly. By encapsulating the logic required to adapt one interface to another, the Adapter Pattern promotes code reuse, loose coupling, and easy integration of third-party libraries or legacy systems.
In Node.js, this pattern is commonly used when integrating different modules, libraries, or APIs that don’t follow the same interface. Whether it’s payment gateways, database connections, or file system management, the Adapter Pattern enables developers to standardize interactions between components, reducing the need for redundant code or complex refactoring.








Leave a comment