Adapter Design Pattern
Context
In software construction, we frequently encounter situations where an existing system needs to collaborate with a third-party library, a vendor class, or legacy code. However, these external components often have interfaces that do not match the specific “Target” interface our system was designed to use.
A classic real-world analogy is the power outlet adapter. If you take a US laptop to London, the laptop’s plug (the client) expects a US power interface, but the wall outlet (the adaptee) provides a European interface. To make them work together, you need an adapter that translates the interface of the wall outlet into one the laptop can plug into. In software, the Adapter pattern acts as this “middleman”, allowing classes to work together that otherwise couldn’t due to incompatible interfaces.
Problem
The primary challenge occurs when we want to use an existing class, but its interface does not match the one we need. This typically happens for several reasons:
- Legacy Code: We have code written a long time ago that we don’t want to (or can’t) change, but it must fit into a new, more modern architecture.
- Vendor Lock-in: We are using a vendor class that we cannot modify, yet its method names or parameters don’t align with our system’s requirements.
- Syntactic and Semantic Mismatches: Two interfaces might differ in syntax (e.g.,
getDistance()in inches vs.getLength()in meters) or semantics (e.g., a method that performs a similar action but with different side effects).
Without an adapter, we would be forced to rewrite our existing system code to accommodate every new vendor or legacy class, which violates the Open/Closed Principle and creates tight coupling.
Solution
The Adapter Pattern solves this by creating a class that converts the interface of an “Adaptee” class into the “Target” interface that the “Client” expects.
According to the GoF catalog, there are four key roles in this structure:
- Target: The domain-specific interface the Client wants to use (e.g., a
Duckinterface withquack()andfly()). In GoF’s motivating example, this isShape. - Adaptee: The existing class with an incompatible interface that needs adapting (e.g., a
WildTurkeyclass thatgobble()s instead ofquack()s). In GoF, this isTextView. - Adapter: The class that adapts the interface of Adaptee to the Target interface (e.g.,
TurkeyAdapter). In GoF, this isTextShape. - Client: The class that collaborates with objects conforming to the Target interface, remaining oblivious to the fact that it is communicating with an Adaptee through the Adapter.
In the “Turkey that wants to be a Duck” example, we create a TurkeyAdapter that implements the Duck interface. When the client calls quack() on the adapter, the adapter internally calls gobble() on the wrapped turkey object. Because turkeys can only fly short distances, the adapter calls the turkey’s fly() method five times to compensate when a duck-style fly() is requested. This syntactic translation effectively hides the underlying implementation from the client.
UML Role Diagram
UML Example Diagram
Sequence Diagram
Code Example
This example adapts a Turkey so client code that expects a Duck can keep using the same target interface.
Teaching example: These snippets are intentionally small. They show one reasonable mapping of the pattern roles, not a drop-in architecture. In production, always tailor the pattern to the concrete context: lifecycle, ownership, error handling, concurrency, dependency injection, language idioms, and team conventions.
interface Duck {
void quack();
void fly();
}
interface Turkey {
void gobble();
void fly();
}
final class WildTurkey implements Turkey {
public void gobble() {
System.out.println("Gobble gobble");
}
public void fly() {
System.out.println("I'm flying a short distance");
}
}
final class TurkeyAdapter implements Duck {
private final Turkey turkey;
TurkeyAdapter(Turkey turkey) {
this.turkey = turkey;
}
public void quack() {
turkey.gobble();
}
public void fly() {
for (int i = 0; i < 5; i++) {
turkey.fly();
}
}
}
public class Demo {
static void testDuck(Duck duck) {
duck.quack();
duck.fly();
}
public static void main(String[] args) {
testDuck(new TurkeyAdapter(new WildTurkey()));
}
}
#include <iostream>
struct Duck {
virtual ~Duck() = default;
virtual void quack() = 0;
virtual void fly() = 0;
};
struct Turkey {
virtual ~Turkey() = default;
virtual void gobble() = 0;
virtual void fly() = 0;
};
class WildTurkey : public Turkey {
public:
void gobble() override {
std::cout << "Gobble gobble\n";
}
void fly() override {
std::cout << "I'm flying a short distance\n";
}
};
class TurkeyAdapter : public Duck {
public:
explicit TurkeyAdapter(Turkey& turkey) : turkey_(turkey) {}
void quack() override {
turkey_.gobble();
}
void fly() override {
for (int i = 0; i < 5; ++i) {
turkey_.fly();
}
}
private:
Turkey& turkey_;
};
void testDuck(Duck& duck) {
duck.quack();
duck.fly();
}
int main() {
WildTurkey turkey;
TurkeyAdapter adapter(turkey);
testDuck(adapter);
}
from abc import ABC, abstractmethod
class Duck(ABC):
@abstractmethod
def quack(self) -> None:
pass
@abstractmethod
def fly(self) -> None:
pass
class Turkey(ABC):
@abstractmethod
def gobble(self) -> None:
pass
@abstractmethod
def fly(self) -> None:
pass
class WildTurkey(Turkey):
def gobble(self) -> None:
print("Gobble gobble")
def fly(self) -> None:
print("I'm flying a short distance")
class TurkeyAdapter(Duck):
def __init__(self, turkey: Turkey) -> None:
self._turkey = turkey
def quack(self) -> None:
self._turkey.gobble()
def fly(self) -> None:
for _ in range(5):
self._turkey.fly()
def test_duck(duck: Duck) -> None:
duck.quack()
duck.fly()
test_duck(TurkeyAdapter(WildTurkey()))
interface Duck {
quack(): void;
fly(): void;
}
interface Turkey {
gobble(): void;
fly(): void;
}
class WildTurkey implements Turkey {
gobble(): void {
console.log("Gobble gobble");
}
fly(): void {
console.log("I'm flying a short distance");
}
}
class TurkeyAdapter implements Duck {
constructor(private readonly turkey: Turkey) {}
quack(): void {
this.turkey.gobble();
}
fly(): void {
for (let i = 0; i < 5; i += 1) {
this.turkey.fly();
}
}
}
function testDuck(duck: Duck): void {
duck.quack();
duck.fly();
}
testDuck(new TurkeyAdapter(new WildTurkey()));
Consequences
Applying the Adapter pattern results in several significant architectural trade-offs:
- Loose Coupling: It decouples the client from the legacy or vendor code. The client only knows the Target interface, allowing the Adaptee to evolve independently without breaking the client code.
- Information Hiding: It follows the Information Hiding principle by concealing the “secret” that the system is using a legacy component.
- Flexibility vs. Complexity: While adapters make a system more flexible, they add a layer of indirection that can make it harder to trace the execution flow of the program since the client doesn’t know which object is actually receiving the call.
Design Decisions
Object Adapter vs. Class Adapter
- Object Adapter (via composition): The adapter wraps an instance of the Adaptee. This is the standard approach in Java and most modern languages. It can adapt an entire class hierarchy (any subclass of the Adaptee works), and the adaptation can be configured at runtime.
- Class Adapter (via inheritance): The adapter inherits from both the Target and the Adaptee simultaneously. This requires either multiple class inheritance (e.g., C++) or — in single-inheritance languages — the Target to be an interface, so the adapter can
extend Adapteeandimplements Target. It avoids the indirection overhead of delegation but ties the adapter to a single concrete Adaptee class.
Modern practice favors Object Adapters because they compose with any subclass of the Adaptee, can be reconfigured at runtime, and don’t require either party to be open for inheritance (see also Effective Java Item 18: Favor composition over inheritance).
Adaptation Scope
Not all adapters are created equal. The complexity of adaptation ranges widely:
- Simple rename:
quack()maps directly togobble(). Trivial and low-risk. - Data transformation: Converting units, reformatting data structures, or translating between protocols. Moderate complexity.
- Behavioral adaptation: The adaptee’s behavior is fundamentally different and the adapter must add logic to bridge the semantic gap. High complexity—and a warning sign that the adapter may be growing into a service.
If an adapter becomes “too thick” (containing significant business logic), it is no longer just translating an interface—it has become a separate component that happens to look like an adapter.
Adapter is a Family, Not a Single Pattern
Buschmann, Henney, and Schmidt observe in Pattern-Oriented Software Architecture, Volume 5: On Patterns and Pattern Languages (2007, p. 234) that “the notion that there is a single pattern called Adapter is in practice present nowhere except in the table of contents of the Gang-of-Four book.” A deconstruction of GoF’s pattern description reveals at least four quite distinct patterns:
- Object Adapter: Wraps an adaptee via composition; adaptation is encapsulated through forwarding via an additional level of indirection (the standard form, favored from a layered/encapsulated perspective).
- Class Adapter: Realized by subclassing both the adapter interface (Target) and the adaptee implementation to yield a single object — avoiding an additional level of indirection. Requires multiple inheritance, or — in single-inheritance languages — the Target being an interface.
- Two-Way Adapter: Conforms to both the target and adaptee interfaces (typically via multiple inheritance), so the adapter is usable wherever either interface is expected. GoF’s example is
ConstraintStateVariable, a subclass of both Unidraw’sStateVariableand QOCA’sConstraintVariable, that adapts each interface to the other so the same object works in either system. - Pluggable Adapter: A class with built-in interface adaptation. GoF describes three implementations: using abstract operations, using delegate objects, or using parameterized adapters (e.g., Smalltalk’s
PluggableAdaptor, which is parameterized with blocks).
The first two forms (Object Adapter, Class Adapter) are described together inside GoF’s Adapter entry, while Two-Way and Pluggable Adapter are surfaced in GoF’s Implementation discussion. This insight is educationally important: when a reference says “use the Adapter pattern”, you must clarify which form of adaptation is needed.
Adapter vs. Facade vs. Decorator
These three patterns all “wrap” another object, but with different intents:
| Pattern | Intent | Scope |
|---|---|---|
| Adapter | Convert one interface to match another | One-to-one: translates a single incompatible interface |
| Façade | Simplify a complex set of interfaces | Many-to-one: wraps an entire subsystem behind one interface |
| Decorator | Add behavior to an object without changing its interface | One-to-one: wraps a single object, preserving its interface |
The key discriminator: Adapter changes what the interface looks like. Facade changes how much of the interface you see. Decorator changes what the object does through the same interface.
Flashcards
Structural Pattern Flashcards
Key concepts for Adapter, Composite, and Facade patterns.
What problem does Adapter solve?
Object Adapter vs. Class Adapter?
Adapter vs. Facade vs. Decorator?
What does POSA5 say about ‘the Adapter pattern’?
What problem does Composite solve?
Composite: Transparent vs. Safe design?
Name three pattern compounds involving Composite.
What problem does Facade solve?
Facade vs. Mediator: what’s the communication direction?
Should the subsystem know about its Facade?
Quiz
Structural Patterns Quiz
Test your understanding of Adapter, Composite, and Facade — their distinctions, design decisions, and when to apply each.
A TurkeyAdapter implements the Duck interface. The fly() method calls turkey.fly() five times in a loop because a duck’s flight is much longer than a turkey’s short hop. What design concern does this raise?
A colleague says: “We should use an Adapter between our service and the database layer.” Your team wrote both the service and the database layer. What is the best response?
In a Composite pattern for a restaurant menu system, a developer declares add(MenuComponent) on the abstract MenuComponent class (inherited by both Menu and MenuItem). A tester calls menuItem.add(anotherItem). What happens, and what design trade-off does this illustrate?
All three patterns — Adapter, Facade, and Decorator — involve “wrapping” another object. What is the key distinction between them?
A HomeTheaterFacade exposes watchMovie(), endMovie(), listenToMusic(), stopMusic(), playGame(), setupKaraoke(), and calibrateSystem(). The class is growing difficult to maintain. What is the best architectural response?
The Facade’s communication is one-directional: the Facade calls subsystem classes, but the subsystem does not know about the Facade. The Mediator’s communication is bidirectional. Why does this distinction matter architecturally?