Learnitweb

Introduction

The Adapter Design Pattern is a structural design pattern that allows objects with incompatible interfaces to work together.

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

This design pattern acts as a bridge between two incompatible interfaces, 1 making them compatible. This pattern is often used when you have an existing class that you can’t modify, but you need to make it work with another class or interface.

When to Use the Adapter Pattern

  • When you want to use an existing class, but its interface does not match the one you need.
  • To create a reusable class that can interact with classes with incompatible interfaces.
  • When you need to work with a legacy system alongside new components.

Key Participants

  • Target: Defines the domain-specific interface that the Client uses.
  • Client: The class that interacts with objects through the Target interface.
  • Adaptee: The existing class with an incompatible interface that needs adapting.
  • Adapter: Implements the Target interface and translates requests from the Client to the Adaptee.

Types of Adapter Pattern

  1. Class Adapter (via Inheritance)
    The Adapter inherits from both the Target and the Adaptee. This approach is limited in languages that do not support multiple inheritance, such as Java.
  2. Object Adapter (via Composition)
    The Adapter contains an instance of the Adaptee and delegates requests to it. This approach is more flexible and is widely used.

Example Scenario

Imagine you are building a system where you need to integrate a legacy service that processes payments (Adaptee) with a new e-commerce platform (Client). The interfaces are incompatible.

Legacy Payment Service (Adaptee):

public class LegacyPaymentService {
    public void processOldPayment(String customer, double amount) {
        System.out.println("Processing payment for " + customer + " of amount $" + amount);
    }
}

Target Interface:

public interface PaymentProcessor {
    void processPayment(String customerName, double amount);
}

Adapter:

public class PaymentAdapter implements PaymentProcessor {
    private final LegacyPaymentService legacyPaymentService;

    public PaymentAdapter(LegacyPaymentService legacyPaymentService) {
        this.legacyPaymentService = legacyPaymentService;
    }

    @Override
    public void processPayment(String customerName, double amount) {
        legacyPaymentService.processOldPayment(customerName, amount);
    }
}

Client:

public class EcommercePlatform {
    private final PaymentProcessor paymentProcessor;

    public EcommercePlatform(PaymentProcessor paymentProcessor) {
        this.paymentProcessor = paymentProcessor;
    }

    public void checkout(String customerName, double amount) {
        System.out.println("Initiating checkout...");
        paymentProcessor.processPayment(customerName, amount);
        System.out.println("Checkout complete.");
    }
}

Usage:

public class Main {
    public static void main(String[] args) {
        LegacyPaymentService legacyPaymentService = new LegacyPaymentService();
        PaymentProcessor adapter = new PaymentAdapter(legacyPaymentService);
        EcommercePlatform platform = new EcommercePlatform(adapter);

        platform.checkout("John Doe", 99.99);
    }
}

Advantages of the Adapter Pattern

  • Improved Reusability: Allows using existing functionality with a new interface.
  • Enhanced Flexibility: Decouples the Client from the Adaptee, enabling independent evolution.
  • Smooth Integration: Makes it easier to integrate legacy systems with new ones.

Disadvantages of the Adapter Pattern

  • Complexity: Adds an extra layer, which can increase complexity.
  • Performance Overhead: Can introduce slight overhead due to delegation in Object Adapters.

Real-World Examples

  • Java’s java.util.Arrays: The asList method adapts an array to the List interface.
  • Driver Adapters: JDBC drivers act as adapters between Java applications and databases.
  • Third-Party Libraries: Wrappers to adapt incompatible APIs.

Best Practices

  • Favor composition over inheritance when implementing adapters for better flexibility.
  • Use meaningful naming for Adapters to clarify their purpose.
  • Ensure the Adapter does not add unnecessary functionality, as it should only translate interfaces.
  • Test the Adapter thoroughly to ensure accurate delegation to the Adaptee.