In software design, the Adapter Pattern is one of the Structural Patterns that allows incompatible interfaces to work together by converting the interface of a class into another interface clients expect. This pattern is particularly useful when you need to integrate existing classes with new ones without modifying their source code.
The Adapter Pattern involves creating an adapter class that acts as a mediator between two incompatible interfaces, enabling them to collaborate seamlessly. This pattern promotes loose coupling and enhances reusability by allowing objects to interact through a common interface.
The core idea behind the Adapter Pattern is to create a wrapper or intermediary class that translates calls from one interface into another. This adapter class acts as a bridge between two incompatible interfaces, ensuring that the client code can work with the target object without needing to know its underlying implementation.
Let's explore a practical example to understand how the Adapter Pattern works.
Suppose you have an audio player application that supports only MP3 files. However, you want to extend its functionality to support WAV files as well. The existing MP3Player class has a method playMp3, but there is no corresponding method for WAV files. We can use the Adapter Pattern to create a new interface that supports both MP3 and WAV files.
First, let's define the target interface that our adapter will implement:
1interface MediaPlayer {2play(file: string): void;3}
Next, we have an existing class MP3Player that can only play MP3 files:
1class MP3Player {2playMp3(mp3File: string) {3console.log(`Playing MP3 file: ${mp3File}`);4}5}
Now, let's create an adapter class MP3ToWAVAdapter that implements the MediaPlayer interface and adapts calls to the MP3Player class:
1class MP3ToWAVAdapter implements MediaPlayer {2private mp3Player: MP3Player;34constructor(mp3Player: MP3Player) {5this.mp3Player = mp3Player;6}78play(file: string) {9if (file.endsWith('.mp3')) {10this.mp3Player.playMp3(file);11} else if (file.endsWith('.wav')) {12console.log(`Playing WAV file: ${file}`);13} else {14throw new Error('Unsupported file format');15}16}17}
Finally, we can use the adapter to play both MP3 and WAV files:
1const mp3Player = new MP3Player();2const mediaPlayer: MediaPlayer = new MP3ToWAVAdapter(mp3Player);34mediaPlayer.play('song.mp3'); // Output: Playing MP3 file: song.mp35mediaPlayer.play('sound.wav'); // Output: Playing WAV file: sound.wav
Another common use case for the Adapter Pattern is integrating different payment gateways. Suppose you have a PayPal class that provides a method payWithPayPal, but your application needs to support both PayPal and Stripe payments.
First, let's define the target interface:
1interface PaymentGateway {2pay(amount: number): void;3}
Next, we have an existing class PayPal that can only process payments through PayPal:
1class PayPal {2payWithPayPal(amount: number) {3console.log(`Paid ${amount} using PayPal`);4}5}
Now, let's create an adapter class PayPalToStripeAdapter that implements the PaymentGateway interface and adapts calls to the PayPal class:
1class PayPalToStripeAdapter implements PaymentGateway {2private payPal: PayPal;34constructor(payPal: PayPal) {5this.payPal = payPal;6}78pay(amount: number) {9// Simulate payment processing through Stripe10console.log(`Paid ${amount} using Stripe`);11}12}
Finally, we can use the adapter to process payments through both PayPal and Stripe:
1const payPal = new PayPal();2const paymentGateway: PaymentGateway = new PayPalToStripeAdapter(payPal);34paymentGateway.pay(100); // Output: Paid 100 using Stripe
In the next section, we will explore another Structural Pattern called the Bridge Pattern. The Bridge Pattern is used to separate an abstraction from its implementation so that both can be varied independently.
Stay tuned for more insights into design patterns and how they can help you build robust and maintainable software systems!