One-line Definition
Encapsulates an incompatible interface behind a compatible one, allowing classes to work together without source code modifications.
Problem
Suppose you're building a reporting app that expects data in XML, but you need to integrate a third-party analytics library that only accepts JSON. You cannot change the third-party library, and changing your entire app to use JSON would be a massive rewrite. The Adapter Pattern solves this by creating a middleman class that translates requests from your app into a format the library understands.
Real-world Analogy
You travel from India to Europe and your laptop charger has a round-pin plug. European outlets use a different shape. You don't rewire your charger or the wall — you use a power adapter that sits between the two and translates one plug shape into the other.
Structure
- Client
- Works with the Target interface and never knows an Adapter is involved.
- Target
- The interface the client expects and understands.
- Adaptee
- The existing class or third-party library with an incompatible interface.
- Adapter
- Implements the Target interface and wraps the Adaptee, translating calls from one to the other.
Diagram
classDiagram
class Client {
}
class Target {
<<interface>>
+request()
}
class Adapter {
-Adaptee adaptee
+request()
}
class Adaptee {
+specificRequest()
}
Client --> Target : uses
Adapter ..|> Target : implements
Adapter --> Adaptee : wraps
Code Walkthrough
Notice how the MediaAdapter implements the MediaPlayer interface that the client expects, but internally translates the play call to the specific methods of the VLC and MP4 players.
interface MediaPlayer {
void play(String audioType, String fileName);
}
class VlcPlayer {
public void playVlc(String fileName) {
System.out.println("Playing VLC file: " + fileName);
}
}
class Mp4Player {
public void playMp4(String fileName) {
System.out.println("Playing MP4 file: " + fileName);
}
}
class MediaAdapter implements MediaPlayer {
private VlcPlayer vlcPlayer;
private Mp4Player mp4Player;
public MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("vlc")) {
vlcPlayer = new VlcPlayer();
} else if (audioType.equalsIgnoreCase("mp4")) {
mp4Player = new Mp4Player();
}
}
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
vlcPlayer.playVlc(fileName);
} else if (audioType.equalsIgnoreCase("mp4")) {
mp4Player.playMp4(fileName);
}
}
}
class AudioPlayer implements MediaPlayer {
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Playing MP3 file: " + fileName);
} else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
MediaAdapter adapter = new MediaAdapter(audioType);
adapter.play(audioType, fileName);
} else {
System.out.println("Unsupported format: " + audioType);
}
}
}
class Main {
public static void main(String[] args) {
AudioPlayer player = new AudioPlayer();
player.play("mp3", "song.mp3");
player.play("mp4", "video.mp4");
player.play("vlc", "movie.vlc");
}
}
Bad vs Good
Bad Approach
Problems
- Client code must know about every third-party class and its unique methods.
- Tight coupling to third-party APIs makes testing impossible without the real library.
class PaymentService {
public void processPayment(String gateway, double amount) {
if (gateway.equals("stripe")) {
StripeAPI stripe = new StripeAPI();
stripe.makeCharge(amount, "USD");
} else if (gateway.equals("paypal")) {
PayPalAPI paypal = new PayPalAPI();
paypal.sendPayment(amount);
}
}
}
Better Approach
Improvements
- Client code works entirely through the expected
PaymentGatewayinterface. - Adding a new integration means writing a new Adapter, keeping the business logic untouched.
interface PaymentGateway {
void pay(double amount);
}
class StripeAdapter implements PaymentGateway {
private final StripeAPI stripe = new StripeAPI();
@Override
public void pay(double amount) {
stripe.makeCharge(amount, "USD");
}
}
class PayPalAdapter implements PaymentGateway {
private final PayPalAPI paypal = new PayPalAPI();
@Override
public void pay(double amount) {
paypal.sendPayment(amount);
}
}
class PaymentService {
private final PaymentGateway gateway;
public PaymentService(PaymentGateway gateway) {
this.gateway = gateway;
}
public void processPayment(double amount) {
gateway.pay(amount);
}
}
Pros vs Cons
| Pros | Cons |
|---|---|
| Integrates incompatible classes without source code changes | Increases overall code complexity |
| Enforces the Single Responsibility Principle (translation is isolated) | Can require writing many adapter methods for large interfaces |
| Follows the Open/Closed Principle | Sometimes wrapping a wrapper feels like over-engineering |
| Makes third-party dependencies easily swappable | Adds slight overhead due to indirection |
| Simplifies client code by providing a uniform interface |
When to Use
- You need to use an existing class, but its interface doesn't match the rest of your code.
- You are integrating a legacy system with a modern application.
- You want to reuse several existing subclasses that lack common functionality, and you can't add it to their superclass.
When Not to Use
- You own the source code for both the client and the adaptee and can simply refactor one to match the other.
- The interface gap is so large that the adapter would need complex business logic to bridge it.
Real-world Examples
java.util.Arrays#asList()(adapts an array to a List)java.io.InputStreamReader(adapts a byte stream to a character stream)- SLF4J (acts as a logging facade/adapter over Log4j, Logback, etc.)
Key Takeaway
The Adapter Pattern is a translator. It encapsulates an incompatible class behind an interface the client expects. Use it to integrate legacy code or third-party libraries without polluting your business logic, but avoid it if you can easily change the underlying classes to match directly.