DEV Community

Cover image for Unveiling Design Patterns: How an Adapter Can Make Your Code More Elegant? Let’s Talk About the Adapter Pattern
Andy Yang
Andy Yang

Posted on

Unveiling Design Patterns: How an Adapter Can Make Your Code More Elegant? Let’s Talk About the Adapter Pattern

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:

  1. Target Interface (Target): The interface you want to use, the standard you want to “adapt” to.
  2. Adaptee: The incompatible class that you want to reuse but can’t use directly.
  3. 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:

  • WavAdapter implements the MediaPlayer interface, meaning it can be treated as a MediaPlayer.
  • WavAdapter holds a WavPlayer instance (a has a relationship). This is the core of the adapter pattern: the adapter internally delegates calls to the adaptee.
  • AudioPlayer uses the MediaPlayer interface. It interacts only with MediaPlayer, unaware of whether the actual implementation is WavAdapter or 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 @Controller and @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 HandlerAdapter interface, which defines the handle() method. DispatcherServlet only talks to this interface.
  • Adaptee: The various controller implementations we write.
  • Adapter: Spring’s different HandlerAdapter implementations. For example, SimpleControllerHandlerAdapter handles controllers implementing Controller, while RequestMappingHandlerAdapter supports 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);
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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. WavPlayer plays WAV, AudioPlayer calls the MediaPlayer interface, 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)