Adapter Pattern: Making Incompatible Interfaces Work Together

Learn how the Adapter Pattern bridges mismatched interfaces to enable seamless integration. Includes C#, Python examples, UML, and real-world use cases.

Introduction

The Adapter Pattern is all about making incompatible interfaces work together without modifying their source code. Think of it like hiring a translator instead of rewriting someone's thoughts in a different language or a power plug adapter that lets one interface "fit" into another without modifying either.

Intent & Motivation

  • Intent: Convert the interface of a class into another interface clients expect.
  • Use case: Often used when integrating legacy systems or external libraries.

Real-world analogy: Using a USB-C to USB-A adapter to connect modern devices to old laptops.

Real-World Use Cases

Common Use Cases for the Adapter Pattern
Category Use Case Description
Legacy System Integration Legacy Payment Gateway Adapter enables a new app to interface with an old gateway without modifying its API.
Legacy Database Access Layer Connects a legacy database layer to a modern ORM by translating calls and responses.
Expose SOAP via REST Wraps SOAP endpoints in a REST interface for modern application compatibility.
Microsoft Dynamics ↔ SAP via BTP Adapter layer translates data formats and API calls, enabling seamless communication.
API and Format Bridging Third-party API Integration Adapts an external API with incompatible data formats to match internal expectations.
XML ↔ JSON Translation Adapter converts XML service responses into JSON to maintain frontend compatibility.
Hardware & Physical Analogies USB-C to USB-A Adapter Physical adapter enables incompatible ports to connect—like classes with mismatched interfaces.
Universal Remote Control Translates commands for different devices—similar to adapting method calls in code.
Enterprise Software Abstractions Logging Framework Adapter Unifies logging calls across diverse libraries with differing interfaces.
Vendor SDK Wrappers Encapsulates SDK functionality with a consistent internal interface for maintainability.

Code Example

Scenario: Adapting a legacy printer class to a new printer interface.


// Target interface
public interface IPrinter
{
    void Print(string content);
}

// Adaptee
public class LegacyPrinter
{
    public void PrintText(string text)
    {
        Console.WriteLine("Legacy Printer Output: " + text);
    }
}

// Adapter
public class PrinterAdapter : IPrinter
{
    private readonly LegacyPrinter _legacyPrinter;

    public PrinterAdapter(LegacyPrinter legacyPrinter)
    {
        _legacyPrinter = legacyPrinter;
    }

    public void Print(string content)
    {
        _legacyPrinter.PrintText(content);
    }
}

// Client
public class ReportService
{
    private readonly IPrinter _printer;

    public ReportService(IPrinter printer)
    {
        _printer = printer;
    }

    public void Generate()
    {
        _printer.Print("Monthly Report Content");
    }
}
        

# Target interface
class Printer:
    def print(self, content):
        raise NotImplementedError

# Adaptee
class LegacyPrinter:
    def print_text(self, text):
        print(f"Legacy Printer Output: {text}")

# Adapter
class PrinterAdapter(Printer):
    def __init__(self, legacy_printer):
        self.legacy_printer = legacy_printer

    def print(self, content):
        self.legacy_printer.print_text(content)

# Client
class ReportService:
    def __init__(self, printer):
        self.printer = printer

    def generate(self):
        self.printer.print("Monthly Report Content")
        

UML Diagram: Adapter Pattern – Legacy Printer Example

Adapter Pattern UML Diagram showing IPrinter, NewPrinter, LegacyPrinter, and LegacyPrinterAdaptor

This UML diagram illustrates how the Adapter Pattern is used to integrate a legacy printing system into a new architecture using a common interface.

đź§© Components

  • IPrinter (Interface)
    Defines a common ColorPrint() and BlackAndWhitePrint() method that all printers must implement. This is the Target Interface expected by the client.
  • NewPrinter (Concrete Class)
    Implements IPrinter directly. Represents a modern, compatible printer.
  • LegacyPrinter (Adaptee)
    An existing legacry printer that supports only BlackAndWhitePrint(). It is not compatible with IPrinter.
  • LegacyPrinterAdaptor (Adapter)
    Implements IPrinter and internally uses a LegacyPrinter instance. Translates ColorPrint() to BlackAndWhitePrint() to bridge compatibility.

🔄 Flow

  • The client uses the IPrinter interface for loose coupling.
  • NewPrinter and LegacyPrinterAdaptor are interchangeable from the client’s perspective.
  • The adapter makes the LegacyPrinter usable without modifying its code.

When to Use the Adapter Pattern

Common Situations for Using the Adapter Pattern
Situation Adapter Benefit
Legacy code Wrap old APIs to fit new interface
Vendor SDKs Align different library interfaces
Microservices Translate between incompatible contracts

Pros and Cons

âś… Pros:

  • Promotes reuse of existing code
  • Encapsulates changes in a single class
  • Decouples systems from legacy interfaces

❌ Cons:

  • Can add unnecessary layers if overused
  • May hide complexity that should be refactored

Quiz

Test your understanding of the Adapter Pattern:

Conclusion

The Adapter Pattern is a powerful tool for bridging gaps between old and new systems. It allows systems to evolve without breaking existing code and is especially useful when working with third-party APIs or legacy code.

Next up in the series: Decorator Pattern – Learn how to dynamically extend behavior without altering the original class.