The Adapter Pattern: Bridging Incompatible Interfaces for Seamless Integration

The Adapter Pattern: Bridging Incompatible Interfaces for Seamless Integration

The Adapter Pattern is a structural design pattern that allows objects with incompatible interfaces to collaborate. It acts as a bridge, converting the interface of one class (the Adaptee) into another interface (the Target) that the client expects.

Think of it this way:

  • The Target (Wall Socket): This represents the interface that the client (the appliance) already expects and is compatible with. It’s what the rest of your system is built to use.
  • The Adaptee (Device Cord/Foreign Plug): This represents the existing system or class with the incompatible interface. It’s the “legacy” or third-party code you want to use.
  • The Adapter (Travel Adapter): This is the new class you create that converts the Adaptee’s interface (foreign plug) into the Target’s interface (wall socket), allowing the two incompatible parts to communicate.

So, while the wall socket is an existing part of the system, in the context of the Adapter Pattern, the Device Cord/Foreign Plug is the element that represents the incompatible existing system (the Adaptee) that needs to be adapted

The Adapter Pattern example: Integration Challenge

Let’s say we have a Legacy customer class that returns customer data in an old format, and you want to change it to a modern format to work with a new reporting tool.

Old: {"customerName": "John Doe","customerID": "12345", "customerLocation": "New York"}

New: {"id": "12345", "name": "John Doe", "location": "New York"}

#legacy system class
class CustomerAdaptee:
    def customerData(self):
        return {
                    "customerName": "John Doe",
                    "customerID": "12345",
                    "customerLocation": "New York"
                }

Most of us will think, let’s change the customerData function to return the modern format, but what if this class is used in other parts of the system? Here, you have a ton of work to do, and here comes the adapter design pattern.

#legacy system class
class CustomerAdaptee:
    def customerData(self):
        return {
                    "customerName": "John Doe",
                    "customerID": "12345",
                    "customerLocation": "New York"
                }

#adapter class    
class CustomerAdapter:
    def __init__(self, adaptee):
        self.adaptee = adaptee

    def getCustomerInfo(self):
        data = self.adaptee.customerData()
        return {
                    "id": data["customerID"], 
                    "name": data["customerName"], 
                    "location": data["customerLocation"]
                }
    

# Usage - Target interface
adaptee = CustomerAdaptee()
adapter = CustomerAdapter(adaptee)
customer_info = adapter.getCustomerInfo()
print(customer_info)  
# Output: {'id': '12345', 'name': 'John Doe', 'location': 'New York'}

We created a bridge, or “adapter,” CustomerAdapter that translates calls and data between two incompatible interfaces: the legacy system’s output and the modern client’s input requirement.

The Adapter implements the Target Interface (getCustomerInfo()) expected by the client.

The __init__ method uses composition by storing a reference to the CustomerAdaptee instance (self.adaptee). This makes it an Object Adapter.

The getCustomerInfo() method is where the translation happens:

  1. It calls the Adaptee’s method: data = self.adaptee.customerData().
  2. It takes the returned data and maps the incompatible keys ("customerID", "customerName") to the compatible keys ("id", "name") before returning the result to the client.

The client code interacts only with the CustomerAdapter using the standardized method getCustomerInfo().

The client receives the data in the exact format it needs ('id', 'name', 'location') without having to know or care about the internal structure or method names of the CustomerAdaptee. This decouples the client from the legacy system.