Last Updated on September 24, 2023 by KnownSense
An Adapter pattern allows incompatible interfaces to work together, acting as a bridge. It converts the interface of one class into another interface that clients expect, enabling collaboration between classes with differing interfaces. The Adapter Pattern is also known as Wrapper.
Imagine you are developing a music streaming application that plays audio files using a new audio playback engine that only supports the “MP3” format. However, your application needs to play older audio files in the “WAV” format, which the new engine doesn’t directly support.
Here’s where the Adapter pattern comes in. You can create an adapter that allows the new audio playback engine to work with the old “WAV” audio files or convert the .wav to .mp3 before streaming.
Benefits
- Compatibility: It allows the integration of components with incompatible interfaces, facilitating cooperation between different systems or classes that wouldn’t normally work together.
- Reusability: Adapters can be reused to enable multiple incompatible components to interact with a common interface. This reduces the need to modify existing code to accommodate new components.
- Maintainability: Instead of modifying existing code to adapt to new interfaces, the Adapter pattern encapsulates the adaptation logic within the adapter class, making future changes or updates easier to manage.
- Solves Legacy Issues: Adapters are especially useful for incorporating legacy code or systems into modern applications, allowing the new codebase to utilize existing functionalities without requiring extensive changes.
- Encapsulation: The Adapter pattern encapsulates the logic required to translate between interfaces, keeping the adaptation process centralized and organized.
- Open-Closed Principle: The Adapter pattern follows the open-closed principle by allowing new classes or components to be introduced without modifying the existing codebase.
- Improves Testing: Adapters can help in unit testing by providing a consistent interface for testing purposes, even when dealing with components that have different interfaces.
- Clearer Design: It can improve the overall design by separating concerns between components that handle different tasks. The adapter’s responsibility is to bridge the gap between these components.
- Interoperability: The Adapter pattern promotes interoperability between software components developed by different teams or organizations, each with their own interfaces.
- Eases Transition: During system migrations or upgrades, the Adapter pattern can facilitate the transition by allowing new and old components to work together until the legacy components are fully replaced.
UML for Adapter Pattern
The main components of the Adapter design pattern are:
- Target Interface: This is the interface that the client code expects to interact with. It defines the methods that the client code uses to perform its tasks.
- Adaptee: This is the existing class or component with an incompatible interface that you want to integrate into your system. It contains the functionality that you want to reuse.
- Adapter: The adapter class implements the target interface and holds an instance of the adaptee. It acts as a bridge between the target interface and the adaptee, translating calls from the target interface to the adaptee’s interface.
- Client: The client code is the code that wants to use the adaptee’s functionality through the target interface. It interacts with the adapter to access the adaptee’s methods.
Implementation
In this implementation, we will build the music streaming application that supports .mp3 format and then create adapter to support .wav format
Step1: Create a interface NewAudioEngine that supports only mp3 format.
public interface NewAudioEngine {
void playMP3(String filename);
}
Step2: Create a class that represent a incompatible audio format
public class OldWAVPlayer {
public void playWAV(String filename) {
System.out.println("Playing WAV file: " + filename);
}
}
Step3: Create a adapter class that implements the NewAudioEngine interface and has instance of OldWAVPlayer
public class WAVAdapterImpl implements NewAudioEngine {
private OldWAVPlayer oldWAVPlayer;
public WAVAdapterImpl(OldWAVPlayer oldWAVPlayer) {
this.oldWAVPlayer = oldWAVPlayer;
}
@Override
public void playMP3(String filename) {
oldWAVPlayer.playWAV(filename);
}
}
Step4: In main class, use WAVAdapterImpl to run the audio file of wav.format
public class MusicApp {
public static void main(String[] args) {
WAVAdapterImpl wavAdapter= new WAVAdapterImpl(new OldWAVPlayer());
wavAdapter.playMP3("old_song.wav");
}
}
OUTPUT:
Playing WAV file: old_song.wav
Conclusion
The Adapter Pattern facilitates communication between incompatible interfaces, enhancing interoperability and reusability. It encapsulates integration logic, promoting code maintainability and adhering to the open-closed principle. However, it may introduce slight overhead and complexity in the codebase due to abstraction layers.