In software development, you’ll often encounter a situation where two modules can’t work together because their interfaces are incompatible. One system expects a logMessage method, while the other provides a writeLog method. You can’t change the source code for one or both of them. What do you do?

This is where the Adapter pattern comes in. It acts as a translator, wrapping one object to give it an interface another object can understand.

The Problem: Incompatible Interfaces

Imagine you have a modern application that uses a Logger interface with a specific method signature.

// Our application's expected interface
interface Logger {
  log(level: 'info' | 'warn' | 'error', message: string): void;
}

class App {
  constructor(private logger: Logger) {}

  doSomething() {
    this.logger.log('info', 'Application is doing something.');
    // ...
  }
}

Now, you need to integrate a third-party logging library. Unfortunately, this library has a completely different interface.

// Third-party library (cannot be modified)
class LegacyLogger {
  public write(category: number, text: string): void {
    // 1 = INFO, 2 = WARNING, 3 = ERROR
    console.log(`[Legacy Log - Category ${category}]: ${text}`);
  }
}

Our App class cannot use LegacyLogger directly. Their contracts don’t match.

The Solution: The Adapter

We create an Adapter class that implements our application’s Logger interface but internally uses the LegacyLogger.

class LegacyLoggerAdapter implements Logger {
  private legacyLogger: LegacyLogger;

  constructor() {
    this.legacyLogger = new LegacyLogger();
  }

  public log(level: 'info' | 'warn' | 'error', message: string): void {
    let category: number;
    switch (level) {
      case 'info':
        category = 1;
        break;
      case 'warn':
        category = 2;
        break;
      case 'error':
        category = 3;
        break;
    }
    this.legacyLogger.write(category, message);
  }
}

// Now our app can use the legacy logger without knowing it!
const loggerAdapter = new LegacyLoggerAdapter();
const app = new App(loggerAdapter);
app.doSomething();
// Output: [Legacy Log - Category 1]: Application is doing something.

The LegacyLoggerAdapter translates the method calls, making the two incompatible classes work together seamlessly.

Python Example: Adapting a Dictionary API

Let’s see the same concept in Python. Suppose our system works with a Notifier that expects a send_notification method.

# Our system's target interface (conceptual in Python)
class Notifier:
    def send_notification(self, message: str, user_id: int):
        raise NotImplementedError

class ReportGenerator:
    def __init__(self, notifier: Notifier):
        self._notifier = notifier

    def generate(self):
        print("Generating report...")
        # ... logic ...
        self._notifier.send_notification("Report generated successfully", 123)

Now, we want to use a new SlackService that sends messages, but its method is called post_to_channel and it takes a dictionary.

# The service we need to adapt to (the "Adaptee")
class SlackService:
    def post_to_channel(self, channel: str, payload: dict):
        print(f"Posting to Slack channel '{channel}': {payload['text']}")

We create an adapter to bridge the gap.

class SlackNotifierAdapter(Notifier):
    def __init__(self, slack_service: SlackService, channel: str):
        self._slack_service = slack_service
        self._channel = channel

    def send_notification(self, message: str, user_id: int):
        payload = {
            "text": message,
            "user": user_id,
            "source": "ReportGenerator"
        }
        self._slack_service.post_to_channel(self._channel, payload)


# Putting it all together
slack_service = SlackService()
# The adapter wraps the incompatible service
adapter = SlackNotifierAdapter(slack_service, "#reports")

# Our system uses the adapter, thinking it's a standard Notifier
report_generator = ReportGenerator(notifier=adapter)
report_generator.generate()
# Output:
# Generating report...
# Posting to Slack channel '#reports': Report generated successfully

When to Use the Adapter Pattern

  1. Integrating Legacy Code: When you need to use an existing class, but its interface is not compatible with the rest of your code.
  2. Third-Party Libraries: When a library’s interface doesn’t match your application’s architecture, an adapter can insulate your code from the library’s specific implementation.
  3. Interface Unification: When you have multiple subclasses with different interfaces and you want to create a single, unified interface for them.

Key Benefits

  • Decoupling: The adapter separates your client code from the implementation of the adapted class. You can swap out the adaptee (e.g., change logging libraries) by just creating a new adapter, without changing any of your application’s core logic.
  • Reusability: You can reuse existing functionality from classes that would otherwise be incompatible.
  • Single Responsibility Principle: The adapter’s sole responsibility is to convert an interface. This keeps both the client and the adaptee focused on their own tasks.

Adapter vs. Facade vs. Decorator

It’s easy to confuse the Adapter with other patterns. Here’s a quick breakdown:

  • Adapter: Changes an interface to make it compatible with another. Its goal is to solve incompatibility.
  • Facade: Simplifies a complex subsystem by providing a single, high-level interface. Its goal is to reduce complexity.
  • Decorator: Adds functionality to an object dynamically without changing its interface. Its goal is to extend behavior.

The Adapter pattern is a simple but powerful tool for building clean, maintainable systems. It allows you to integrate disparate parts of a system cleanly, ensuring that components can collaborate even when they weren’t originally designed to.