Java > Design Patterns in Java > Structural Patterns > Adapter Pattern

Adapter Pattern Example: Converting Celsius to Fahrenheit

The Adapter pattern allows incompatible interfaces to work together. This example demonstrates adapting a Celsius temperature sensor to a system that expects Fahrenheit.

Concepts Behind the Snippet

The Adapter pattern essentially wraps an existing class with a new interface that the client expects. This involves creating an 'adapter' class that implements the target interface and internally uses the adaptee (the class being adapted). It promotes reusability and flexibility by allowing existing, potentially legacy, code to be integrated into new systems without modification.

Celsius Temperature Sensor (Adaptee)

This is the class we want to adapt. It provides temperature readings in Celsius, but our client needs Fahrenheit.

public class CelsiusTemperatureSensor {
    public double getCelsiusTemperature() {
        // Simulate reading temperature from a sensor
        return Math.random() * 40; // Temperature between 0 and 40 Celsius
    }
}

Fahrenheit Temperature Interface (Target)

This interface represents the temperature system our client expects. It defines a method to retrieve the temperature in Fahrenheit.

public interface FahrenheitTemperature {
    double getFahrenheitTemperature();
}

Temperature Adapter (Adapter)

This is the adapter class. It implements the `FahrenheitTemperature` interface, but internally uses a `CelsiusTemperatureSensor` to get the temperature in Celsius and then converts it to Fahrenheit.

public class TemperatureAdapter implements FahrenheitTemperature {
    private CelsiusTemperatureSensor celsiusSensor;

    public TemperatureAdapter(CelsiusTemperatureSensor celsiusSensor) {
        this.celsiusSensor = celsiusSensor;
    }

    @Override
    public double getFahrenheitTemperature() {
        double celsius = celsiusSensor.getCelsiusTemperature();
        return celsiusToFahrenheit(celsius);
    }

    private double celsiusToFahrenheit(double celsius) {
        return (celsius * 9 / 5) + 32;
    }
}

Client Code

This is the client code that uses the `FahrenheitTemperature` interface. It doesn't need to know anything about the `CelsiusTemperatureSensor` or the conversion process.

public class Client {
    public static void main(String[] args) {
        CelsiusTemperatureSensor celsiusSensor = new CelsiusTemperatureSensor();
        FahrenheitTemperature adapter = new TemperatureAdapter(celsiusSensor);

        double fahrenheit = adapter.getFahrenheitTemperature();
        System.out.println("Temperature in Fahrenheit: " + fahrenheit);
    }
}

Real-Life Use Case

Consider integrating a legacy payment processing system (which only supports one currency) with a modern e-commerce platform that supports multiple currencies. You can use an adapter to convert the e-commerce platform's currency requests into the currency supported by the legacy system, allowing the two systems to work together seamlessly.

Best Practices

  • Favor composition over inheritance: The adapter typically uses composition (holding a reference to the adaptee) rather than inheritance.
  • Keep the adapter simple: The adapter should primarily focus on interface conversion and avoid complex logic.
  • Document the adapter: Clearly document the purpose of the adapter and the target interface it provides.

Interview Tip

Be prepared to explain the difference between the Adapter and Decorator patterns. While both can wrap an existing object, the Adapter changes the interface of the object, while the Decorator adds responsibilities without changing the interface.

When to use them

Use the Adapter pattern when:

  • You want to use an existing class, and its interface does not match the one you need.
  • You want to create a reusable class that cooperates with unrelated or unforeseen classes, that is, classes that don't necessarily have compatible interfaces.
  • You need to use several existing subclasses, but it's impractical to adapt their interface by subclassing every one. An object adapter can adapt the interface of its parent class.

Memory Footprint

The adapter pattern adds a small memory footprint due to the adapter object itself. The memory used is primarily determined by the fields held by the adapter (e.g., the reference to the adaptee) and the code within the adapter methods. The impact is generally negligible compared to the overall application size.

Alternatives

Alternatives to the adapter pattern include:

  • Refactoring: Modifying the code of the adaptee to directly conform to the target interface. This is only feasible if you have control over the adaptee's code and the change is relatively small.
  • Bridge Pattern: If the problem is about separating an abstraction from its implementation, the bridge pattern might be more suitable.

Pros

  • Increased Reusability: Adapters allow you to reuse existing classes that would otherwise be unusable.
  • Improved Flexibility: Adapters decouple the client from the concrete adaptee, allowing you to easily switch between different adaptees.
  • Single Responsibility Principle: The adapter focuses on interface conversion, keeping the adaptee's code clean.

Cons

  • Increased Complexity: The introduction of adapters can add complexity to the design.
  • Potential Performance Overhead: The adapter adds an extra layer of indirection, which can introduce a slight performance overhead.

FAQ

  • What is the key benefit of using the Adapter pattern?

    The key benefit is enabling collaboration between classes with incompatible interfaces without modifying their existing code.
  • When should I *not* use the Adapter pattern?

    If the interfaces are only slightly different, and you have control over the adaptee's code, refactoring the adaptee might be a simpler solution.