- What is It?
- Where and Why do We Use It?
- Key Components
- Principle Method
- Examples of Real-World Scenario
- Code without Pattern
- Code with Pattern
- Use cases of
- Advantages & Disadvantages
The Adapter Design Pattern acts as a bridge between two incompatible interfaces, allowing them to work together. It converts one interface into another interface that a client expects. Think of it as a translator who helps two people speaking different languages understand each other.
Analogy 2: Client needs yo get the service from Adaptee, which is incompatible & cannot interact directly.
Analogy 3: Think of it as a translator who helps two people speaking different languages understand each other.
-
Where:
- When you have an existing class or library that you want to use, but its interface is not compatible with your system.
- When working with legacy code and modern code together.
-
Why?
- To make unrelated interfaces work together.
- To reuse existing functionality in a new system without modifying its original code.
- Target: The interface or abstract class expected by the client.
- Adaptee: The existing class or interface that needs to be adapted.
- Adapter: A class that implements the target interface and translates calls to the adaptee.
- Client: The code that uses the target interface to interact with objects. It remains unaware of the specific implementation details of the adaptee and the adapter. It’s the code that benefits from the integration of the adaptee into the system through the adapter.
The Adapter Pattern works by:
- Creating an adapter class that wraps the incompatible object (adaptee).
- The adapter class implements the target interface and translates the requests into something the adaptee can understand.
- You have a mobile phone with a Type-C port (client).
- Your charger has a USB port (adaptee).
- You use a Type-C to USB adapter to connect the charger to your phone. The adapter allows two incompatible devices to work together.
Example: Suppose we have a MediaPlayer system that plays audio files, but we also want it to play video files.
Suppose we have a MediaPlayer system that plays audio files, but we also want it to play video files. Without an adapter, the code would require modifications to handle both audio and video.
// Existing class to play audio
class AudioPlayer {
public void playAudio(String fileName) {
System.out.println("Playing audio: " + fileName);
}
}
// Client code
public class MediaPlayerWithoutAdapter {
public static void main(String[] args) {
AudioPlayer audioPlayer = new AudioPlayer();
audioPlayer.playAudio("song.mp3"); // This will work fine
// Cannot play video files!
// audioPlayer.playVideo("movie.mp4"); -> This won't work
}
}
Problem: We cannot play video files without modifying the AudioPlayer class, which violates the Open/Closed Principle.
Using the Adapter Design Pattern, we can solve this problem by creating an adapter that makes the AudioPlayer and VideoPlayer work together.
// Step 1: Target Interface
interface MediaPlayer {
void play(String mediaType, String fileName);
}
// Step 2: Adaptee (Existing classes)
class AudioPlayer {
public void playAudio(String fileName) {
System.out.println("Playing audio: " + fileName);
}
}
class VideoPlayer {
public void playVideo(String fileName) {
System.out.println("Playing video: " + fileName);
}
}
// Step 3: Adapter Class
class MediaAdapter implements MediaPlayer {
private AudioPlayer audioPlayer = new AudioPlayer();
private VideoPlayer videoPlayer = new VideoPlayer();
@Override
public void play(String mediaType, String fileName) {
if (mediaType.equalsIgnoreCase("audio")) {
audioPlayer.playAudio(fileName);
} else if (mediaType.equalsIgnoreCase("video")) {
videoPlayer.playVideo(fileName);
} else {
System.out.println("Invalid media type: " + mediaType);
}
}
}
// Step 4: Client Code
public class MediaPlayerWithAdapter {
public static void main(String[] args) {
MediaPlayer mediaPlayer = new MediaAdapter();
mediaPlayer.play("audio", "song.mp3");
mediaPlayer.play("video", "movie.mp4");
}
}
- Integration of Legacy Code: When old code needs to work with a new system.
- Third-Party Libraries: Adapting external libraries to match your project’s requirements.
- Device Compatibility: Making different hardware or APIs compatible, like adapters in Java java.io.InputStreamReader.
- Gaming Engines: Making old game mechanics work in newer engines.
• Reusability: Allows the use of existing code without modification.
• Flexibility: Makes incompatible interfaces work together seamlessly.
• Separation of Concerns: Keeps the adaptee and client independent of each other.
• Complexity: Increases the number of classes in the system.
• Performance: Slight overhead due to translation in the adapter class.