Low-level designCore7 min

Adapter

When a class you need has the wrong interface, and you can't change it, wrap it in a small class that implements the interface your code expects and translates each call. Your code stays as it is; the translation lives in one place.

Watch it evolve1 / 8
The problem

A frost alarm reads a sensor

FrostAlarm reads tenths of a degree Celsius through TemperatureSensor and warns at zero or below.

80
Start
Stage 2
Final

1 · The situation

A frost alarm for a home weather station

A home weather station warns gardeners about frost. FrostAlarm reads a TemperatureSensor and reports the temperature, adding a warning at zero or below. Readings are whole tenths of a degree Celsius, so 35 means 3.5 °C. The station's own sensors implement the interface directly.

Python writes tenths_celsius() and, later, read_tenths_f(); the other names are the same in all four languages.

The first versionJava
interface TemperatureSensor {
    int tenthsCelsius();
}

class LocalSensor implements TemperatureSensor {
    private final int tenths;

    LocalSensor(int tenths) {
        this.tenths = tenths;
    }

    public int tenthsCelsius() {
        return tenths;
    }
}

class FrostAlarm {
    private final String place;
    private final TemperatureSensor sensor;

    FrostAlarm(String place, TemperatureSensor sensor) {
        this.place = place;
        this.sensor = sensor;
    }

    String check() {
        int tenths = sensor.tenthsCelsius();
        String reading = String.format("%s: %.1f °C", place, tenths / 10.0);
        return tenths <= 0 ? reading + ", frost warning" : reading;
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(new FrostAlarm("Porch", new LocalSensor(35)).check());
    }
}
«interface»TemperatureSensor+ tenthsCelsius(): intLocalSensor− tenths: int+ tenthsCelsius(): intFrostAlarm− place: String− sensor: TemperatureSensor~ check(): String

The class diagram of this version, drawn from its code: the alarm depends only on the sensor interface.

Porch: 3.5 °C

2 · The change

A cheaper probe with its own interface

The station starts selling a garden probe from another manufacturer. It's cheaper and waterproof, and it comes with the maker's library, which the station's code can't change. The library's AcmeProbe reports tenths of a degree Fahrenheit, through a method called readTenthsF(). It doesn't implement TemperatureSensor.

The vendor's classJava
class AcmeProbe {
    private final int tenthsF;

    AcmeProbe(int tenthsF) {
        this.tenthsF = tenthsF;
    }

    int readTenthsF() {
        return tenthsF;
    }
}

The quickest way to support it is inside the alarm: a second constructor for the probe, and a conversion where the reading is taken.

The alarm learns the vendor's APIJava
class FrostAlarm {
    private final String place;
    private final TemperatureSensor sensor;
    private final AcmeProbe probe;

    FrostAlarm(String place, TemperatureSensor sensor) {
        this.place = place;
        this.sensor = sensor;
        this.probe = null;
    }

    FrostAlarm(String place, AcmeProbe probe) {
        this.place = place;
        this.sensor = null;
        this.probe = probe;
    }

    String check() {
        int tenths = sensor != null ? sensor.tenthsCelsius() : (probe.readTenthsF() - 320) * 5 / 9;
        String reading = String.format("%s: %.1f °C", place, tenths / 10.0);
        return tenths <= 0 ? reading + ", frost warning" : reading;
    }
}
Porch: 3.5 °C
Garden: -2.0 °C, frost warning
Greenhouse: 5.0 °C

It works, and it puts two foreign things into the alarm: the vendor's class and method names, and the unit conversion. Every other class that reads temperatures, a chart or a daily log, needs the same special case. The next vendor adds a third branch to each.

What the alarm now knows

Why it shouldn't

The AcmeProbe class and its method name

A vendor detail; it changes when the vendor does

How to convert Fahrenheit tenths to Celsius tenths

Unit conversion for one device, repeated in every reader

Which constructor was used, through a null field

Two code paths in a class whose job is deciding about frost

3 · The fix

Translate at the boundary

Start from the first version. The alarm already has the interface it wants. Rather than teach it a second one, make the probe look like a TemperatureSensor:

AcmeProbeAdapter implements TemperatureSensor and holds an AcmeProbe. Its tenthsCelsius() reads the probe and converts the units. All knowledge of the vendor lives here.

Java
class AcmeProbeAdapter implements TemperatureSensor {
    private final AcmeProbe probe;

    AcmeProbeAdapter(AcmeProbe probe) {
        this.probe = probe;
    }

    public int tenthsCelsius() {
        return (probe.readTenthsF() - 320) * 5 / 9;
    }
}
«interface»TemperatureSensor+ tenthsCelsius(): intLocalSensor− tenths: int+ tenthsCelsius(): intAcmeProbe− tenthsF: int~ readTenthsF(): intAcmeProbeAdapter− probe: AcmeProbe+ tenthsCelsius(): intFrostAlarm− place: String− sensor: TemperatureSensor~ check(): String
Porch: 3.5 °C
Garden: -2.0 °C, frost warning
Greenhouse: 5.0 °C

The same readings, with the conversion done in one place.

4 · The pattern

Adapter: make a class fit the interface you have

Adapter converts the interface of a class into the interface a client expects, so classes that couldn't work together can. The client keeps calling its own interface; the adapter turns each call into calls on the adaptee, translating names, arguments, units or results on the way.

«interface»TemperatureSensor+ tenthsCelsius(): intLocalSensor− tenths: int+ tenthsCelsius(): intAcmeProbe− tenthsF: int~ readTenthsF(): intAcmeProbeAdapter− probe: AcmeProbe+ tenthsCelsius(): intFrostAlarm− place: String− sensor: TemperatureSensor~ check(): String

The roles in the weather station: the alarm uses the target interface, and the adapter wraps the vendor's probe.

Role

In the weather station

Its job

Target

TemperatureSensor

The interface the client is written against

Client

FrostAlarm

Uses the target, and only the target

Adaptee

AcmeProbe

The useful class with the wrong interface

Adapter

AcmeProbeAdapter

Implements the target by calling the adaptee

One reading: the alarm asks in its terms, and the adapter asks the probe in the vendor's.

This is the object adapter: it holds the adaptee. A class adapter inherits from the adaptee instead, which needs multiple inheritance in C++ or an adaptee that isn't final in Java, and ties the adapter to one concrete class. Holding is the usual choice.

5 · What it costs

One more class per mismatch, and translations that can lose detail

  • A class per adaptee. Each vendor or library gets its own adapter.
  • A layer to read through. A reading now passes through the adapter on its way to the alarm.
  • Lossy translation. If the target interface can't express something the adaptee offers, such as the probe's battery level, the client can't reach it through the adapter.

When you own both sides, it's often simpler to change one of them to match. Adapter earns its place when one side can't change: a third-party library, a generated client, or an old module that other code still relies on.

6 · Getting it right

Keep the adapter thin and the target yours

  • Translate, don't decide. The adapter converts units and names; whether there's frost is the alarm's business.
  • Define the target in your own terms. The interface belongs to the client's side, as in Dependency inversion; the adapter is how outside code meets it.
  • One adapter per adaptee. Don't grow one adapter that handles several vendors with a switch.
  • Translate errors too. Turn the vendor's exceptions or error codes into what the client expects.

Language

The usual shape

Java

A class that implements the target interface and holds the adaptee

C++

A class derived from the target's abstract class, holding a reference or std::unique_ptr to the adaptee

JavaScript

Any object with the target's methods; often a small object literal or a function

Python

A class with the protocol's methods; for one method, a function or lambda does the job

7 · Look-alikes

Wrappers with different purposes

Several patterns wrap an object. What tells them apart is what the wrapper changes.

Pattern

What the wrapper changes

Adapter

The interface: makes an object fit a different one

Decorator

The behavior: adds to an object while keeping its interface

Proxy

The access: controls when and whether calls reach the object, same interface

Facade

The complexity: offers a simpler interface to a whole subsystem

Bridge

Nothing after the fact: it separates two hierarchies by design, from the start

The checks tell Adapter from Decorator and Proxy the same way: an adapter holds an object of an unrelated type and translates calls to its differently named methods, while a decorator or proxy holds an object of its own interface.

8 · In practice

Adapters in the standard libraries

Standard libraries are full of them. Java's Arrays.asList() returns a fixed-size List backed by an array, so array data can go wherever a list is expected. C++ calls std::stack and std::queue container adaptors: they present a stack or queue interface over another container. Python's io.TextIOWrapper turns a binary stream into a text stream.

The practice checks recognize an Adapter when a class implements an interface, holds an object of an unrelated type, and forwards to that object's differently named methods. Design the parking lot: a payment terminal from a third party is where an adapter would belong.

Check yourself

4 questions

1. A second vendor's probe reports kelvins through getTemp(). What changes in the Adapter version?
2. Where should the frost threshold be checked?
3. You own both FrostAlarm and the sensor code, and nothing else uses the sensor. Is an adapter the best move?
4. Which wraps an object to give it a different interface?

Takeaways

Remember this

  • Adapter makes a class with the wrong interface usable through the interface your code expects.
  • It holds the adaptee and translates each call: names, arguments, units, results and errors.
  • Keep adapters thin: translate, don't make decisions.
  • Use it when one side can't change; when you own both, change one to match.
  • Adapter changes the interface; Decorator and Proxy keep it.