Unveiling Design Patterns: How an Adapter Can Make Your Code More Elegant? Let’s Talk About the Adapter Pattern
Have you ever experienced this? You buy a new computer, but its port doesn’t fit your old monitor. Or you’re traveling abroad and your phone charger won’t fit into the local power socket. At times like this, an adapter solves everything.
In the world of software development, we often face similar problems: you have a powerful class at hand, but your new system can’t directly call it because their interfaces “don’t speak the same language.” Should you modify the old code or compromise your new system?
Don’t worry — the answer lies in a design pattern known as the Adapter Pattern, often called the “adapter plug” of the code world. It allows two incompatible interfaces to work together seamlessly — all without changing the existing code.
What is the Adapter Pattern?
In simple terms, the Adapter Pattern converts the interface of a class into another interface that clients expect, enabling classes that otherwise couldn’t work together due to incompatible interfaces to collaborate smoothly.
It involves three main roles:
- Target Interface (Target): The interface you want to use, the standard you want to “adapt” to.
- Adaptee: The incompatible class that you want to reuse but can’t use directly.
- Adapter: The “plug adapter” that implements the target interface while holding an instance of the adaptee, translating calls from the client into calls to the adaptee.
Class Diagram
To better understand the relationships, let’s visualize them with a class diagram.
Relationship Analysis:
-
WavAdapterimplements theMediaPlayerinterface, meaning it can be treated as aMediaPlayer. -
WavAdapterholds aWavPlayerinstance (ahas arelationship). This is the core of the adapter pattern: the adapter internally delegates calls to the adaptee. -
AudioPlayeruses theMediaPlayerinterface. It interacts only withMediaPlayer, unaware of whether the actual implementation isWavAdapteror another player.
Adapter Pattern in Mainstream Frameworks
This all sounds good in theory, but how does the Adapter Pattern help in everyday development? Beyond system integration or third-party service compatibility, you’re already using adapters daily in frameworks like Spring.
A classic example is the HandlerAdapter in Spring MVC.
Spring MVC’s core is the DispatcherServlet. When an HTTP request arrives, it must find the right controller to handle it. But developers write controllers in many different styles:
- Some implement a specific interface like
Controller. - Some use annotations like
@Controllerand@RequestMapping. - Others may use custom handler types.
If DispatcherServlet had to handle all these variations directly, it would become bloated and messy. Every time a new type of controller appeared, you’d have to modify its core code.
Spring’s solution: HandlerAdapter.
-
Target Interface: The
HandlerAdapterinterface, which defines thehandle()method.DispatcherServletonly talks to this interface. - Adaptee: The various controller implementations we write.
-
Adapter: Spring’s different
HandlerAdapterimplementations. For example,SimpleControllerHandlerAdapterhandles controllers implementingController, whileRequestMappingHandlerAdaptersupports those annotated with@RequestMapping.
When a request arrives, DispatcherServlet asks each HandlerAdapter: “Can you handle this controller?” Once it finds a match, it delegates the call.
This is the essence of the adapter pattern: turning incompatible interfaces (different controllers) into a unified interface (HandlerAdapter). This design keeps the DispatcherServlet simple, extensible, and open to new handler types without internal changes.
Java Example
Suppose we’re building an audio player that currently supports only MP3. We define a MediaPlayer interface as our target:
// Target interface
public interface MediaPlayer {
void play(String audioType, String fileName);
}
Now we get a new requirement: support WAV files. We already have a WavPlayer class, but its interface is incompatible.
// Adaptee
public class WavPlayer {
public void playWav(String fileName) {
System.out.println("Playing WAV file: " + fileName);
}
}
We can’t modify MediaPlayer or WavPlayer. The best solution is to create an adapter: WavAdapter.
// Adapter
public class WavAdapter implements MediaPlayer {
private WavPlayer wavPlayer;
public WavAdapter(WavPlayer wavPlayer) {
this.wavPlayer = wavPlayer;
}
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("wav")) {
this.wavPlayer.playWav(fileName);
} else {
System.out.println("Invalid audio type: " + audioType);
}
}
}
Now, the main AudioPlayer can use it without caring about the details:
public class AudioPlayer {
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Playing MP3 file: " + fileName);
} else if (audioType.equalsIgnoreCase("wav")) {
MediaPlayer mediaPlayer = new WavAdapter(new WavPlayer());
mediaPlayer.play(audioType, fileName);
} else {
System.out.println("Invalid audio type: " + audioType);
}
}
public static void main(String[] args) {
AudioPlayer audioPlayer = new AudioPlayer();
audioPlayer.play("mp3", "beyond_the_horizon.mp3");
audioPlayer.play("wav", "my_new_song.wav");
}
}
The Real Value of the Adapter Pattern
The true power of the adapter pattern lies in its elegant solution for interface incompatibility:
- Open/Closed Principle: To support a new service, just add a new adapter — no need to change existing code.
-
Separation of Concerns: Each class does its own job.
WavPlayerplays WAV,AudioPlayercalls theMediaPlayerinterface, and the adapter just translates between them. Clean and maintainable. - Seamless Integration: Legacy systems and new modules can work together smoothly.
So next time you face two incompatible interfaces, pause and ask yourself: do I just need an “adapter” to let them shake hands?

Top comments (0)