Adapter

Adapter Pattern

Introduction

The Adapter pattern, also known as the Wrapper pattern, allows for the interface of an existing class to be used as another interface. It’s particularly useful when making existing classes work with others without modifying their source code.

Purpose

  • To convert the interface of a class into another interface clients expect.
  • To enable classes with incompatible interfaces to work together.

Use Cases

  • When there is a need to use an existing class, and its interface does not match the one you need.
  • When you want to create a reusable class that cooperates with classes which don’t necessarily have compatible interfaces.
  • In legacy code integration, where the code cannot be modified.

Advantages/Disadvantages

Advantages:

  • Increases compatibility between classes with different interfaces.
  • Helps to achieve the Open/Closed Principle by allowing new adapter classes without altering existing code.

Disadvantages:

  • Increases overall complexity by introducing a set of new abstractions.
  • Sometimes overuse can lead to a system that’s hard to understand due to many layers of indirection.

Implementation in JavaScript

javascript
	class Target {
	  request() {
	    return 'Target: Default behavior';
	  }
	}
	
	class Adaptee {
	  specificRequest() {
	    return 'Adaptee: Specific behavior';
	  }
	}
	
	class Adapter extends Target {
	  constructor(adaptee) {
	    super();
	    this.adaptee = adaptee;
	  }
	
	  request() {
	    return `Adapter: (Translated) ${this.adaptee.specificRequest()}`;
	  }
	}
	
	// Usage
	const adaptee = new Adaptee();
	const adapter = new Adapter(adaptee);
	
	console.log(adapter.request()); // "Adapter: (Translated) Adaptee: Specific behavior"

Practical Example

Imagine integrating a new logging system into an existing application. The new logger has a different interface from what the application expects. We can use the Adapter pattern to bridge this gap.

javascript
	class LegacyLogger {
	  logMessage(msg) {
	    console.log(`Legacy Logger: ${msg}`);
	  }
	}
	
	class NewLogger {
	  output(msg) {
	    console.log(`New Logger: ${msg}`);
	  }
	}
	
	class LoggerAdapter {
	  constructor(newLogger) {
	    this.newLogger = newLogger;
	  }
	
	  logMessage(msg) {
	    // Adapting the interface of NewLogger to LegacyLogger
	    this.newLogger.output(msg);
	  }
	}
	
	// Usage
	const oldLogger = new LegacyLogger();
	const newLogger = new NewLogger();
	const adaptedLogger = new LoggerAdapter(newLogger);
	
	oldLogger.logMessage('Message for the old logger'); // Legacy Logger: Message for the old logger
	adaptedLogger.logMessage('Message for the new logger'); // New Logger: Message for the new logger

In this practical example, LoggerAdapter adapts NewLogger’s interface to the interface expected by the existing application (LegacyLogger). This allows the application to use the new logging system without changing its existing code.