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.
A frost alarm reads a sensor
FrostAlarm reads tenths of a degree Celsius through TemperatureSensor and warns at zero or below.
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.
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());
}
}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.
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.
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 | 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.
class AcmeProbeAdapter implements TemperatureSensor {
private final AcmeProbe probe;
AcmeProbeAdapter(AcmeProbe probe) {
this.probe = probe;
}
public int tenthsCelsius() {
return (probe.readTenthsF() - 320) * 5 / 9;
}
}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.
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 |
| The interface the client is written against |
Client |
| Uses the target, and only the target |
Adaptee |
| The useful class with the wrong interface |
Adapter |
| 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 |
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
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.