The adapter pattern acts as a bridge between two incompatible interfaces, enabling them to work together. It is a structural design pattern that wraps an existing class with a new interface to make it compatible with another system.
Consider a video player that natively supports only MP4 files, but needs to play AVI or RMVB formats. An adapter—like a format converter—can translate these formats into MP4 so the player can handle them.
There are two common implementations: class-based and object-based adapters.
In the class adapter, inheritance is used:
interface Mp4Player {
void playMp4();
}
interface AviPlayer {
void playAvi();
}
class NativeMp4Player implements Mp4Player {
public void playMp4() {
System.out.println("Playing MP4 file.");
}
}
class AviToMp4Adapter extends NativeMp4Player implements AviPlayer {
public void playAvi() {
System.out.println("Converting AVI to MP4...");
playMp4();
}
}
However, the object adapter is generally preferred because it uses composition, adhering to the composition-over-inheritance principle:
interface RmvbPlayer {
void playRmvb();
}
class RmvbToMp4Adapter implements RmvbPlayer {
private final Mp4Player mp4Player;
public RmvbToMp4Adapter(Mp4Player player) {
this.mp4Player = player;
}
public void playRmvb() {
System.out.println("Converting RMVB to MP4...");
mp4Player.playMp4();
}
}
// Usage
public class Main {
public static void main(String[] args) {
Mp4Player mp4 = new NativeMp4Player();
AviPlayer avi = new AviToMp4Adapter();
RmvbPlayer rmvb = new RmvbToMp4Adapter(mp4);
avi.playAvi(); // Converts and plays
rmvb.playRmvb(); // Converts and plays
}
}
Output:
Converting AVI to MP4...
Playing MP4 file.
Converting RMVB to MP4...
Playing MP4 file.
The adapter pattern enhances reusability and flexibility but should be introduced only when integrating legacy or third-party components—not during initial design.
The bridge pattern decouples an abstraction from its implementation so both can vary independently. It’s useful when a class has multiple orthogonal dimensions of variation.
For example, different types of pens (red, black) can write on different types of paper (exam sheets, newspapers). The pen type and paper type evolve separately, yet they collaborate during writing.
Implemantation steps:
- Define an interface for the implementor (e.g.,
Pen). - Create concrete implementors (
RedPen,BlackPen). - Define an abstract abstraction (
Paper) that holds a reference to the implementor. - Create refined abstractions (
ExamPaper,NewsPaper).
interface Pen {
void write();
}
class RedPen implements Pen {
public void write() {
System.out.println("Red ink");
}
}
class BlackPen implements Pen {
public void write() {
System.out.println("Black ink");
}
}
abstract class Paper {
protected Pen pen;
public void setPen(Pen pen) {
this.pen = pen;
}
public abstract void produce();
}
class ExamPaper extends Paper {
public void produce() {
System.out.print("Grading exam with ");
pen.write();
}
}
class Newspaper extends Paper {
public void produce() {
System.out.print("Printing news with ");
pen.write();
}
}
// Usage
public class Main {
public static void main(String[] args) {
Paper exam = new ExamPaper();
exam.setPen(new RedPen());
exam.produce();
Paper news = new Newspaper();
news.setPen(new BlackPen());
news.produce();
}
}
Output:
Grading exam with Red ink
Printing news with Black ink
This structure allows adding new pen types or paper types without modifying existing code, promoting extensibility. However, it introduces additional layers of abstraction, which may complicate simple systems.