Adapter Pattern

Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.


Problem

You have existing code that uses one interface, but you need to integrate it with another component that expects a different interface. You don't want to modify the existing code.

Solution

The Adapter pattern converts one interface to another by wrapping the incompatible component.


Implementation

from abc import ABC, abstractmethod

# ============ OLD INTERFACE (Existing code) ============

class LegacyPaymentProcessor:
    """Old payment system with different interface"""

    def process_payment(self, amount: float) -> dict:
        # Returns dict instead of boolean
        return {
            "status": "completed",
            "amount": amount,
            "transaction_id": "TXN-12345"
        }

# ============ NEW INTERFACE (What we want) ============

class PaymentProcessor(ABC):
    @abstractmethod
    def pay(self, amount: float) -> bool:
        pass

# ============ CLASS ADAPTER (Inheritance) ============

class LegacyPaymentAdapter(PaymentProcessor, LegacyPaymentProcessor):
    """Inherits from both old implementation and new interface"""

    def pay(self, amount: float) -> bool:
        # Adapt old method to new interface
        result = self.process_payment(amount)
        return result["status"] == "completed"

# ============ OBJECT ADAPTER (Composition) ============

class PaymentAdapterComposition(PaymentProcessor):
    """Uses composition - more flexible"""

    def __init__(self, legacy_processor: LegacyPaymentProcessor):
        self.legacy_processor = legacy_processor

    def pay(self, amount: float) -> bool:
        result = self.legacy_processor.process_payment(amount)
        return result["status"] == "completed"

# ============ TWO-WAY ADAPTER ============

class StandardPaymentGateway(PaymentProcessor):
    def pay(self, amount: float) -> bool:
        print(f"Processing payment of ${amount}")
        return True

class TwoWayAdapter(PaymentProcessor):
    """Adapts between StandardPaymentGateway and LegacyPaymentProcessor"""

    def __init__(self, target=None):
        self.target = target

    def pay(self, amount: float) -> bool:
        if isinstance(self.target, LegacyPaymentProcessor):
            result = self.target.process_payment(amount)
            return result["status"] == "completed"
        elif isinstance(self.target, StandardPaymentGateway):
            return self.target.pay(amount)
        return False

# ============ REAL-WORLD EXAMPLE: USB Adapters ============

class ElectricDevice(ABC):
    @abstractmethod
    def use_electricity(self, voltage: int):
        pass

class EuropeanSocket:
    """European 220V socket"""
    def provide_electricity(self):
        return 220

class AmericanSocket:
    """American 110V socket"""
    def provide_electricity(self):
        return 110

class Laptop(ElectricDevice):
    def use_electricity(self, voltage: int):
        if voltage in [100, 240]:  # Accepts 100-240V
            print(f"Laptop charging at {voltage}V")
        else:
            print(f"Laptop cannot charge at {voltage}V")

class Adapter110to220(ElectricDevice):
    """Adapts 110V American to 220V European requirement"""

    def __init__(self, american_socket: AmericanSocket):
        self.american_socket = american_socket

    def use_electricity(self, voltage: int):
        original_voltage = self.american_socket.provide_electricity()
        # Step up voltage
        adapted_voltage = original_voltage * 2
        print(f"Adapter converting {original_voltage}V to {adapted_voltage}V")
        return adapted_voltage

# ============ MULTIPLE INHERITANCE ADAPTER ============

class OldLogSystem:
    def log(self, msg: str):
        print(f"[OLD LOG] {msg}")

class NewLogInterface(ABC):
    @abstractmethod
    def debug(self, msg: str):
        pass

    @abstractmethod
    def error(self, msg: str):
        pass

class LogAdapter(NewLogInterface):
    def __init__(self, old_logger: OldLogSystem):
        self.old_logger = old_logger

    def debug(self, msg: str):
        self.old_logger.log(f"DEBUG: {msg}")

    def error(self, msg: str):
        self.old_logger.log(f"ERROR: {msg}")

# ============ ADAPTER FOR COLLECTIONS ============

class OldDataStore:
    """Returns data as list"""
    def get_data(self):
        return [1, 2, 3, 4, 5]

class CollectionInterface(ABC):
    @abstractmethod
    def iterate(self):
        pass

class CollectionAdapter(CollectionInterface):
    def __init__(self, old_store: OldDataStore):
        self.old_store = old_store

    def iterate(self):
        # Adapt list to iterable interface
        for item in self.old_store.get_data():
            yield item

# Demo
if __name__ == "__main__":
    print("=== Class Adapter (Inheritance) ===")
    legacy = LegacyPaymentProcessor()
    adapter = LegacyPaymentAdapter()
    result = adapter.pay(100.0)
    print(f"Payment processed: {result}\n")

    print("=== Object Adapter (Composition) ===")
    legacy = LegacyPaymentProcessor()
    adapter = PaymentAdapterComposition(legacy)
    result = adapter.pay(100.0)
    print(f"Payment processed: {result}\n")

    print("=== Two-Way Adapter ===")
    legacy = LegacyPaymentProcessor()
    adapter = TwoWayAdapter(legacy)
    result = adapter.pay(100.0)
    print(f"Payment processed: {result}\n")

    print("=== Voltage Adapter ===")
    american_socket = AmericanSocket()
    adapter = Adapter110to220(american_socket)
    voltage = adapter.use_electricity(110)

    laptop = Laptop()
    laptop.use_electricity(voltage)
    print()

    print("=== Log Adapter ===")
    old_logger = OldLogSystem()
    log_adapter = LogAdapter(old_logger)
    log_adapter.debug("This is a debug message")
    log_adapter.error("This is an error message")
    print()

    print("=== Collection Adapter ===")
    store = OldDataStore()
    collection = CollectionAdapter(store)
    print("Iterating adapted collection:")
    for item in collection.iterate():
        print(f"  Item: {item}")

Key Concepts

  • Class Adapter: Uses inheritance to adapt incompatible interfaces
  • Object Adapter: Uses composition (wrapper) for flexibility
  • Two-Way Adapter: Adapts between two incompatible interfaces
  • Transparent: Client doesn't know about adaptation

When to Use

✅ Need to integrate legacy code with new code
✅ Incompatible interfaces need to work together
✅ Want to decouple client from incompatible components
✅ Reuse existing classes with incompatible interfaces


Interview Q&A

Q1: What's the difference between Adapter and Decorator?

A: - Adapter: Converts incompatible interfaces. Interface mismatch problem. - Decorator: Adds new behavior without changing interface. Enhancement.

# Adapter: Changes interface
class LegacyAdapter(PaymentProcessor):
    def pay(self, amount):
        return legacy.process_payment(amount)  # Different interface

# Decorator: Same interface, adds behavior
class LoggingDecorator(PaymentProcessor):
    def __init__(self, processor):
        self.processor = processor

    def pay(self, amount):
        print(f"Logging payment of {amount}")  # Added behavior
        return self.processor.pay(amount)  # Same interface

Q2: When would you use Class Adapter vs Object Adapter?

A: - Class Adapter: Simpler, single inheritance. Not extensible. - Object Adapter: More flexible, can adapt multiple types.

# Class Adapter (single inheritance)
class PaymentAdapter(PaymentProcessor, LegacyPaymentProcessor):
    pass

# Object Adapter (composition)
class PaymentAdapter:
    def __init__(self, legacy):
        self.legacy = legacy

Q3: How would you handle a many-to-one adapter?

A:

class UniversalAdapter:
    """Adapts multiple old interfaces to one new interface"""

    def __init__(self, adaptee):
        self.adaptee = adaptee

    def pay(self, amount: float) -> bool:
        if hasattr(self.adaptee, 'process_payment'):  # Type 1
            result = self.adaptee.process_payment(amount)
            return result.get("status") == "completed"
        elif hasattr(self.adaptee, 'charge'):  # Type 2
            return self.adaptee.charge(amount)
        elif hasattr(self.adaptee, 'transfer'):  # Type 3
            return self.adaptee.transfer(amount)
        return False

Q4: How would you adapt a database driver?

A:

class OldDatabaseDriver:
    def executeQuery(self, query):
        return {"results": []}

class DatabaseInterface(ABC):
    @abstractmethod
    def execute(self, query: str):
        pass

class DatabaseAdapter(DatabaseInterface):
    def __init__(self, old_driver):
        self.old_driver = old_driver

    def execute(self, query: str):
        return self.old_driver.executeQuery(query).get("results")

Q5: How would you test Adapter code?

A:

class MockLegacyProcessor:
    def process_payment(self, amount):
        return {"status": "completed", "amount": amount}

# Test adapter
adapter = PaymentAdapterComposition(MockLegacyProcessor())
assert adapter.pay(100.0) == True

# Test with new interface
new_payment = StandardPaymentGateway()
adapter2 = TwoWayAdapter(new_payment)
assert adapter2.pay(100.0) == True

Trade-offs

Pros: Integrates incompatible components, doesn't modify existing code, reusable
Cons: Extra complexity, inheritance issues (with class adapter)


Real-World Examples

  • USB adapters (USB-C to USB-A)
  • Database drivers (adapting different DB APIs)
  • Payment gateways (Stripe, PayPal, Square)
  • Legacy code integration (mainframe to cloud)