Adapter
Adapter Design Pattern?
Two incompatible interfaces or systems can cooperate by using the adapter design pattern, a structural design pattern. Because of incompatible interfaces, it serves as a bridge between two classes that would not otherwise be able to communicate. The adapter approach is very helpful when attempting to incorporate third-party libraries or legacy code into a new system.
Real-World Example of Adapter Design Pattern
Let’s understand this concept using a simple example:
Suppose you have two buddies, one of them speaks French exclusively and the other English exclusively. The language barrier prevents them from communicating the way you want them to.
- You act as an adapter, translating messages between them. Your role allows the English speaker to convey messages to you, and you convert those messages into French for the other person.
- In this way, despite the language difference, your adaptation enables smooth communication between your friends.
- This role you play is similar to the Adapter design pattern, bridging the gap between incompatible interfaces.
Different implementations of Adapter Design Pattern
The Adapter Design Pattern can be applied in various ways depending on the programming language and the specific context. Here are the primary implementations:
1. Class Adapter (Inheritance-based)
- In this approach, the adapter class inherits from both the target interface (the one the client expects) and the adaptee (the existing class needing adaptation).
- Programming languages that allow multiple inheritance, like C++, are more likely to use this technique.
- However, in languages like Java and C#, which do not support multiple inheritance, this approach is less frequently used.
2. Object Adapter (Composition-based)
- The object adapter employs composition instead of inheritance. In this implementation, the adapter holds an instance of the adaptee and implements the target interface.
- This approach is more flexible as it allows a single adapter to work with multiple adaptees and does not require the complexities of inheritance.
- The object adapter is widely used in languages like Java and C#.
3. Two-way Adapter
- A two-way adapter can function as both a target and an adaptee, depending on which interface is being invoked.
- This type of adapter is particularly useful when two systems need to work together and require mutual adaptation.
4. Interface Adapter (Default Adapter)
- When only a few methods from an interface are necessary, an interface adapter can be employed.
- This is especially useful in cases where the interface contains many methods, and the adapter provides default implementations for those that are not needed.
- This approach is often seen in languages like Java, where abstract classes or default method implementations in interfaces simplify the implementation process.
How Adapter Design Pattern works?
Below is how adapter design pattern works:
- Step 1: The client initiates a request by calling a method on the adapter via the target interface.
- Step 2: The adapter maps or transforms the client’s request into a format that the adaptee can understand using the adaptee’s interface.
- Step 3: The adaptee does the actual job based on the translated request from the adapter.
- Step 4: The client receives the results of the call, remaining unaware of the adapter’s presence or the specific details of the adaptee.
Java example of the Adapter Pattern: Object Adapter (Composition-based)
Scenario
You have a legacy OldPrinter
class that prints text. You want to integrate it into a new system that uses a Printer
interface.
// Step 1: Define the Target Interface
interface Printer {
void print(String message);
}
// Step 2: Create a Legacy Class (Adaptee)
class OldPrinter {
public void printOldWay(String text) {
System.out.println("OldPrinter: " + text);
}
}
// Step 3: Create the Adapter
class PrinterAdapter implements Printer {
private OldPrinter oldPrinter;
public PrinterAdapter(OldPrinter oldPrinter) {
this.oldPrinter = oldPrinter;
}
@Override
public void print(String message) {
// Adapt the old method to the new interface
oldPrinter.printOldWay(message);
}
}
// Step 4: Use the Adapter in the Client
public class AdapterPatternExample {
public static void main(String[] args) {
// Legacy printer
OldPrinter oldPrinter = new OldPrinter();
// Adapter to use OldPrinter as a Printer
Printer printer = new PrinterAdapter(oldPrinter);
// Client code using the Printer interface
printer.print("Hello, World!");
}
}
Explanation
- Printer Interface: Represents the standard interface the client expects.
- OldPrinter Class: A legacy class with a different method signature (
printOldWay
). - PrinterAdapter Class: Adapts the
OldPrinter
class to thePrinter
interface by implementing the expected method and internally calling the legacy method. - Client: Uses the
Printer
interface without worrying about the underlying implementation.
Output
OldPrinter: Hello, World!
This way, you seamlessly integrate legacy code (OldPrinter
) into a modern system using the Adapter Pattern.
Another Scenario:
- Class 1 (Client): A modern system uses
MediaPlayer
to play audio. - Class 2 (Adaptee): A legacy system only knows how to play
AdvancedMediaPlayer
formats (like MP4 and VLC files). - Problem: The
MediaPlayer
class andAdvancedMediaPlayer
class are incompatible.
We’ll create an adapter to make them work together.
// Step 1: Define the Target Interface
interface MediaPlayer {
void play(String audioType, String fileName);
}
// Step 2: Define the Adaptee (Legacy Class)
class AdvancedMediaPlayer {
public void playMp4(String fileName) {
System.out.println("Playing MP4 file: " + fileName);
}
public void playVlc(String fileName) {
System.out.println("Playing VLC file: " + fileName);
}
}
// Step 3: Create the Adapter Class
class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedMediaPlayer;
public MediaAdapter(String audioType) {
advancedMediaPlayer = new AdvancedMediaPlayer();
}
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("mp4")) {
advancedMediaPlayer.playMp4(fileName);
} else if (audioType.equalsIgnoreCase("vlc")) {
advancedMediaPlayer.playVlc(fileName);
} else {
System.out.println("Unsupported format: " + audioType);
}
}
}
// Step 4: Create the Client Class
class AudioPlayer implements MediaPlayer {
private MediaAdapter mediaAdapter;
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Playing MP3 file: " + fileName);
} else if (audioType.equalsIgnoreCase("mp4") || audioType.equalsIgnoreCase("vlc")) {
// Use adapter to handle advanced formats
mediaAdapter = new MediaAdapter(audioType);
mediaAdapter.play(audioType, fileName);
} else {
System.out.println("Invalid media type: " + audioType + ". Supported types are MP3, MP4, VLC.");
}
}
}
// Step 5: Test the Code
public class AdapterPatternExample {
public static void main(String[] args) {
MediaPlayer audioPlayer = new AudioPlayer();
audioPlayer.play("mp3", "song.mp3");
audioPlayer.play("mp4", "video.mp4");
audioPlayer.play("vlc", "movie.vlc");
audioPlayer.play("avi", "unsupported.avi");
}
}
Explanation:
-
MediaPlayer Interface (Target):
- Defines the modern audio player interface.
- Expected by the client (
AudioPlayer
).
-
AdvancedMediaPlayer Class (Adaptee):
- Legacy system with its own methods for playing MP4 and VLC files.
-
MediaAdapter Class (Adapter):
- Bridges the gap between the
MediaPlayer
interface andAdvancedMediaPlayer
class. - Converts
MediaPlayer
calls toAdvancedMediaPlayer
methods.
- Bridges the gap between the
-
AudioPlayer Class (Client):
- Uses
MediaAdapter
when encountering incompatible formats (MP4
,VLC
).
- Uses