Adapter Design Pattern

Allow for separate components of an application or system to communicate with each other through a shared interface.

When writing modular architectures, it is important to keep segregated interfaces that perform discrete functionality and communicate with each other effectively. As the number of components grows and the interconnection between them increases, it can become increasingly difficult for all the pieces to communicate with each other. This is when an Adapter becomes useful.

Definition

An adapter is a type of structural design pattern that acts as bridge between two incompatible interfaces. By wrapping the interface of a class with one a client can readily use, existing classes can collaborate without needing to modify their existing source code.

Introduction

You may already have worked with various types of adapters before without even realizing it! Lets examine some commonly used adapters.

Examples include:

  • Arrays.asList(new int[] {1,2}) – Allows you to input an array and adapt it to a list.
  • new InputStreamReader(inputStream) – A constructor to bridge between a byte stream an a character stream reader.
  • Collections.synchronizedList(nonSyncList) – A static collections method which allows for easy conversion between non-synchronized to thread-safe synchronized lists.

All of these methods provide a way for underlying classes to interact and adapt to each other without the need to modify the existing implementations. They all follow a similar pattern consisting of three key components

  1. Target: The interface that the client code expects to work with.
  2. Adaptee: The class containing an interface that needs to be adapted.
  3. Adaptor: The class that implements the target interface that a client will use.

Here is generic UML structure of between these components:

Examples

Lets take a look of example code implementing this design pattern:

// Target interface
interface Target {
    void requiredMethod();
}

// Adaptee class with a different interface
class Adaptee {
    void oldMethod() {
        System.out.println("Adaptee's specific request");
    }
}

// Adapter class that adapts Adaptee to Target
class Adapter implements Target {
    private Adaptee adaptee;

    Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    @Override
    public void request() {
        adaptee.oldMethod();
    }
}

// Client code that expects a Target interface
class Client {
    void operate(Target target) {
        target.requiredMethod();
    }
}

public class AdapterPatternExample {
    public static void main(String[] args) {
        Adaptee adaptee = new Adaptee();
        Target adapter = new Adapter(adaptee);

        Client client = new Client();
        client.operate(adapter);
    }
}

Here we can see that our original Adaptee class is created and then passed into our Adapter. The output of our adapter is the target interface our client wishes to use. The Client has an operate method that utilizes this adapted target, allowing for interactivity between our Client and Adapter classes.

Lets examine a more practical real world example of where the Adapter pattern can be used. Lets imagine we have two services that are involved in capturing temperatures through a type of IOT sensor. Our two classes are:

  1. Fahrenheit Service (Adaptee)
  2. Celsius Service (Adaptee)

Each of these service classes implement a getTemperature() method that returns the temperature in its corresponding units. Now imagine a requirement coming in that all temperature need to be returned in Kelvin. Since we are using an external library to measure temperatures, it is not possible for us to go in make modifications to the existing service classes ourselves. Instead we can created adapters for each of these. We can create two adapters as needed:

  1. Fahrenheit Adapter
  2. Celsius Adapter

Each of these adapters will accept a fahrenheit or celsius service as input and provide the related adapted target as output. We can call these targets WeatherInfo. Lets explore the Java code that can facilitate this for us.

// Target Interface
interface WeatherInfo {
    double getTemperature(); // Temperature in Kelvin
}

// Adaptee 1: API providing temperature in Fahrenheit
class FahrenheitWeatherService {
    double getTemperatureFahrenheit() {
        // Call to the Fahrenheit-based API
    }
}

// Adaptee 2: API providing temperature in Celsius
class CelsiusWeatherService {
    double getTemperatureCelsius() {
        // Call to the Celsius-based API
    }
}

// Adapter 1: Adapts Fahrenheit API to the target interface
class FahrenheitAdapter implements WeatherInfo {
    private FahrenheitWeatherService fahrenheitService;

    public FahrenheitAdapter(FahrenheitWeatherService fahrenheitService) {
        this.fahrenheitService = fahrenheitService;
    }

    @Override
    public double getTemperature() {
        // Convert Fahrenheit to Kelvin and return
        return (fahrenheitService.getTemperatureFahrenheit() + 459.67) * 5 / 9;
    }
}

// Adapter 2: Adapts Celsius API to the target interface
class CelsiusAdapter implements WeatherInfo {
    private CelsiusWeatherService celsiusService;

    public CelsiusAdapter(CelsiusWeatherService celsiusService) {
        this.celsiusService = celsiusService;
    }

    @Override
    public double getTemperature() {
        // Convert Celsius to Kelvin and return
        return celsiusService.getTemperatureCelsius() + 273.15;
    }
}

Using our adapters and WeatherInfo interface, we can now seamlessly use both API’s in our application to return temperatures in Kelvin.

public class WeatherApplication {
    public static void main(String[] args) {
        // Using Fahrenheit API
        FahrenheitWeatherService fahrenheitService = new FahrenheitWeatherService();
        WeatherInfo fahrenheitAdapter = new FahrenheitAdapter(fahrenheitService);
        System.out.println("Temperature in Kelvin: " + fahrenheitAdapter.getTemperature());

        // Using Celsius API
        CelsiusWeatherService celsiusService = new CelsiusWeatherService();
        WeatherInfo celsiusAdapter = new CelsiusAdapter(celsiusService);
        System.out.println("Temperature in Kelvin: " + celsiusAdapter.getTemperature());
    }
}

By employing the Adapter Design Pattern, you can integrate diverse third-party APIs seamlessly into your application, providing a consistent and unified interface. This promotes code flexibility and maintainability when dealing with external systems with different conventions.

Benefits

Code Reusability

By using adapters, existing code (the adaptee) can be reused even if it doesn’t conform to the desired interface. This promotes the reuse of legacy code and reduces redundancy.

Flexibility

The Adapter Pattern enhances code flexibility by allowing the integration of new functionality without modifying existing code. It acts as a layer that shields the client from the intricacies of the adaptee.

Interoperability

When working with third-party libraries or systems with different interfaces, adapters facilitate interoperability by making the components compatible.

Summary

Adapters are a common way for structuring classes and components in order to allow for seamless communication. Without the use of this pattern, your software will eventually become rigid as the different elements of your application will need to be modified in order to pass information back and forth. It also tightly couples these disparate pieces, making it harder to write unit tests and more difficult to make modifications without breaking the existing code base. Now you have a technique for handling data transfer through an independent intermediary and now you can use it to your advantage!

Index