Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on the Adapter Pattern! ๐ Have you ever tried to plug a European phone charger into an American outlet? You need an adapter! Thatโs exactly what weโll learn to do with TypeScript code today.
Youโll discover how the Adapter Pattern can help you connect incompatible interfaces, making different parts of your code work together harmoniously. Whether youโre integrating third-party libraries ๐, working with legacy code ๐๏ธ, or building flexible APIs ๐, mastering the Adapter Pattern is essential for writing maintainable, scalable TypeScript applications.
By the end of this tutorial, youโll be creating adapters like a pro! Letโs dive in! ๐โโ๏ธ
๐ Understanding the Adapter Pattern
๐ค What is the Adapter Pattern?
The Adapter Pattern is like a universal translator ๐ for your code. Think of it as the friend who helps two people who speak different languages communicate with each other. Itโs a bridge that allows classes with incompatible interfaces to work together!
In TypeScript terms, the Adapter Pattern creates a wrapper that translates one interface into another. This means you can:
- โจ Use existing code without modification
- ๐ Integrate third-party libraries seamlessly
- ๐ก๏ธ Maintain loose coupling between components
๐ก Why Use the Adapter Pattern?
Hereโs why developers love the Adapter Pattern:
- Flexibility ๐: Connect different systems without changing their code
- Reusability โป๏ธ: Use existing classes with new interfaces
- Maintainability ๐ง: Keep your code modular and easy to update
- Integration ๐ค: Work with external APIs and libraries smoothly
Real-world example: Imagine building a payment system ๐ณ. You need to support PayPal, Stripe, and Square. Each has different APIs, but with adapters, your code can use them all through a single interface!
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example:
// ๐ฏ Target interface - what our app expects
interface MediaPlayer {
play(filename: string): void;
stop(): void;
}
// ๐ฑ Existing class with different interface
class AdvancedMusicPlayer {
playMusic(track: string): void {
console.log(`๐ต Playing music: ${track}`);
}
halt(): void {
console.log("โน๏ธ Music stopped");
}
}
// ๐ Adapter to make them work together!
class MusicPlayerAdapter implements MediaPlayer {
private player: AdvancedMusicPlayer;
constructor() {
this.player = new AdvancedMusicPlayer();
}
play(filename: string): void {
// ๐ฏ Adapt the method call
this.player.playMusic(filename);
}
stop(): void {
// ๐ Translate to the expected method
this.player.halt();
}
}
// ๐ฎ Using the adapter
const player: MediaPlayer = new MusicPlayerAdapter();
player.play("awesome-song.mp3"); // Works perfectly! ๐
๐ก Explanation: The adapter wraps the AdvancedMusicPlayer
and translates its methods to match the MediaPlayer
interface. Magic! โจ
๐ฏ Common Patterns
Here are patterns youโll use daily:
// ๐๏ธ Pattern 1: Class Adapter
class LegacyPrinter {
printOldWay(text: string): void {
console.log(`๐ Legacy print: ${text}`);
}
}
interface ModernPrinter {
print(content: string): void;
}
class PrinterAdapter extends LegacyPrinter implements ModernPrinter {
print(content: string): void {
this.printOldWay(content); // ๐ Adapting the call
}
}
// ๐จ Pattern 2: Object Adapter
class EmailService {
sendEmail(to: string, subject: string, body: string): void {
console.log(`๐ง Sending email to ${to}`);
}
}
interface NotificationService {
notify(user: string, message: string): void;
}
class EmailNotificationAdapter implements NotificationService {
constructor(private emailService: EmailService) {}
notify(user: string, message: string): void {
// ๐ฏ Adapt parameters to match expected interface
this.emailService.sendEmail(user, "Notification", message);
}
}
๐ก Practical Examples
๐ Example 1: Payment Gateway Adapter
Letโs build something real:
// ๐ณ Our app's payment interface
interface PaymentGateway {
processPayment(amount: number, currency: string): Promise<boolean>;
refund(transactionId: string): Promise<boolean>;
}
// ๐ฆ External payment provider with different API
class StripePaymentAPI {
chargeCard(cents: number, currencyCode: string): Promise<{success: boolean, id: string}> {
console.log(`๐ฐ Stripe: Charging ${cents} cents in ${currencyCode}`);
return Promise.resolve({ success: true, id: "stripe_123" });
}
issueRefund(chargeId: string): Promise<{refunded: boolean}> {
console.log(`๐ธ Stripe: Refunding charge ${chargeId}`);
return Promise.resolve({ refunded: true });
}
}
// ๐ Adapter to make Stripe work with our interface
class StripeAdapter implements PaymentGateway {
private stripe: StripePaymentAPI;
private lastTransactionId: string = "";
constructor() {
this.stripe = new StripePaymentAPI();
}
async processPayment(amount: number, currency: string): Promise<boolean> {
// ๐ก Convert dollars to cents for Stripe
const cents = Math.round(amount * 100);
const result = await this.stripe.chargeCard(cents, currency.toUpperCase());
this.lastTransactionId = result.id;
return result.success;
}
async refund(transactionId: string): Promise<boolean> {
const result = await this.stripe.issueRefund(transactionId);
return result.refunded;
}
}
// ๐ฎ Using our adapter
const paymentGateway: PaymentGateway = new StripeAdapter();
await paymentGateway.processPayment(99.99, "usd"); // Works seamlessly! ๐
๐ฏ Try it yourself: Create a PayPalAdapter that adapts a different payment API!
๐ฎ Example 2: Game Controller Adapter
Letโs make it fun:
// ๐ฎ Our game's expected controller interface
interface GameController {
movePlayer(direction: "up" | "down" | "left" | "right"): void;
jump(): void;
attack(): void;
}
// ๐น๏ธ Old-school joystick with different methods
class RetroJoystick {
pushStick(angle: number): void {
console.log(`๐น๏ธ Joystick pushed at ${angle}ยฐ`);
}
pressButtonA(): void {
console.log("๐
ฐ๏ธ Button A pressed!");
}
pressButtonB(): void {
console.log("๐
ฑ๏ธ Button B pressed!");
}
}
// ๐ Adapter to use retro joystick in modern game
class JoystickAdapter implements GameController {
private joystick: RetroJoystick;
constructor() {
this.joystick = new RetroJoystick();
}
movePlayer(direction: "up" | "down" | "left" | "right"): void {
// ๐ฏ Convert direction to angle
const angleMap = {
up: 0,
right: 90,
down: 180,
left: 270
};
this.joystick.pushStick(angleMap[direction]);
}
jump(): void {
this.joystick.pressButtonA(); // ๐ฆ A button for jump!
}
attack(): void {
this.joystick.pressButtonB(); // โ๏ธ B button for attack!
}
}
// ๐โโ๏ธ Game logic using our adapter
class Game {
constructor(private controller: GameController) {}
playSequence(): void {
console.log("๐ฎ Starting epic game sequence!");
this.controller.movePlayer("right");
this.controller.jump();
this.controller.attack();
console.log("๐ Combo completed!");
}
}
// ๐ฏ Play with any controller!
const retroController = new JoystickAdapter();
const game = new Game(retroController);
game.playSequence();
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Two-Way Adapters
When youโre ready to level up, try this advanced pattern:
// ๐ฏ Bidirectional adapter for data transformation
interface ModernUser {
fullName: string;
email: string;
isActive: boolean;
}
interface LegacyUser {
firstName: string;
lastName: string;
emailAddress: string;
status: "active" | "inactive";
}
class UserAdapter {
// โจ Modern to Legacy
static toModern(legacy: LegacyUser): ModernUser {
return {
fullName: `${legacy.firstName} ${legacy.lastName}`,
email: legacy.emailAddress,
isActive: legacy.status === "active"
};
}
// ๐ Legacy to Modern
static toLegacy(modern: ModernUser): LegacyUser {
const [firstName, ...lastNameParts] = modern.fullName.split(" ");
return {
firstName,
lastName: lastNameParts.join(" "),
emailAddress: modern.email,
status: modern.isActive ? "active" : "inactive"
};
}
}
// ๐ช Using the two-way adapter
const legacyData: LegacyUser = {
firstName: "Sarah",
lastName: "Johnson",
emailAddress: "[email protected]",
status: "active"
};
const modernUser = UserAdapter.toModern(legacyData);
console.log("โจ Modern format:", modernUser);
๐๏ธ Advanced Topic 2: Generic Adapters
For the brave developers:
// ๐ Generic adapter factory
abstract class GenericAdapter<TSource, TTarget> {
constructor(protected source: TSource) {}
abstract adapt(): TTarget;
}
// ๐ Example: Data source adapters
interface ChartData {
labels: string[];
values: number[];
}
class CSVAdapter extends GenericAdapter<string[][], ChartData> {
adapt(): ChartData {
const labels = this.source.map(row => row[0]);
const values = this.source.map(row => parseFloat(row[1]));
return { labels, values };
}
}
class JSONAdapter extends GenericAdapter<{name: string, count: number}[], ChartData> {
adapt(): ChartData {
const labels = this.source.map(item => item.name);
const values = this.source.map(item => item.count);
return { labels, values };
}
}
// ๐ Using generic adapters
const csvData = [["Monday", "10"], ["Tuesday", "15"]];
const csvAdapter = new CSVAdapter(csvData);
const chartData = csvAdapter.adapt();
console.log("๐ Chart ready:", chartData);
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Forgetting to Handle All Methods
// โ Wrong way - incomplete adapter!
class BadAdapter implements MediaPlayer {
play(filename: string): void {
console.log("Playing...");
}
// ๐ฅ Forgot to implement stop()! TypeScript will catch this!
}
// โ
Correct way - implement all methods!
class GoodAdapter implements MediaPlayer {
play(filename: string): void {
console.log(`๐ต Playing: ${filename}`);
}
stop(): void {
console.log("โน๏ธ Stopped"); // โ
All methods implemented!
}
}
๐คฏ Pitfall 2: Tight Coupling in Adapters
// โ Dangerous - adapter knows too much!
class TightlyCoupledAdapter {
private api = new ExternalAPI();
doSomething(): void {
// ๐ฐ Directly accessing internal properties
this.api.internalState.privateMethod();
}
}
// โ
Safe - use only public interface!
class LooseCoupledAdapter {
constructor(private api: ExternalAPI) {} // ๐ Dependency injection
doSomething(): void {
// โ
Use only public methods
this.api.publicMethod();
}
}
๐ ๏ธ Best Practices
- ๐ฏ Single Responsibility: Each adapter should adapt one interface only
- ๐ Clear Naming: Use descriptive names like
PayPalPaymentAdapter
- ๐ก๏ธ Dependency Injection: Pass adaptees through constructor
- ๐จ Keep It Simple: Donโt add extra functionality in adapters
- โจ Type Safety: Always implement interfaces completely
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Social Media Adapter System
Create adapters for different social media platforms:
๐ Requirements:
- โ Common interface for posting to social media
- ๐ท๏ธ Support for Twitter, Facebook, and Instagram APIs
- ๐ค Handle different post formats (text, images, videos)
- ๐ Schedule posts for later
- ๐จ Each platform has its own emoji style!
๐ Bonus Points:
- Add character limit handling for Twitter
- Implement hashtag conversion
- Create a multi-platform poster
๐ก Solution
๐ Click to see solution
// ๐ฏ Common social media interface
interface SocialMediaPlatform {
post(content: string, media?: string[]): Promise<boolean>;
schedule(content: string, datetime: Date): Promise<string>;
getCharLimit(): number;
}
// ๐ฆ Twitter API (different interface)
class TwitterAPI {
tweet(text: string, images: string[] = []): Promise<{id: string}> {
console.log(`๐ฆ Tweeting: ${text.substring(0, 280)}`);
return Promise.resolve({ id: "tweet_123" });
}
scheduleTweet(text: string, time: number): Promise<{scheduled_id: string}> {
console.log(`โฐ Scheduled tweet for ${new Date(time)}`);
return Promise.resolve({ scheduled_id: "scheduled_456" });
}
}
// ๐ Facebook API (different interface)
class FacebookAPI {
createPost(message: string, attachments?: {type: string, url: string}[]): Promise<boolean> {
console.log(`๐ Facebook post: ${message}`);
return Promise.resolve(true);
}
schedulePost(message: string, publishTime: string): Promise<boolean> {
console.log(`๐
Scheduled for ${publishTime}`);
return Promise.resolve(true);
}
}
// ๐ฆ Twitter Adapter
class TwitterAdapter implements SocialMediaPlatform {
private twitter = new TwitterAPI();
async post(content: string, media?: string[]): Promise<boolean> {
const result = await this.twitter.tweet(content, media || []);
return !!result.id;
}
async schedule(content: string, datetime: Date): Promise<string> {
const result = await this.twitter.scheduleTweet(content, datetime.getTime());
return result.scheduled_id;
}
getCharLimit(): number {
return 280; // ๐ฆ Twitter's character limit
}
}
// ๐ Facebook Adapter
class FacebookAdapter implements SocialMediaPlatform {
private facebook = new FacebookAPI();
async post(content: string, media?: string[]): Promise<boolean> {
const attachments = media?.map(url => ({ type: "image", url }));
return await this.facebook.createPost(content, attachments);
}
async schedule(content: string, datetime: Date): Promise<string> {
await this.facebook.schedulePost(content, datetime.toISOString());
return `fb_scheduled_${Date.now()}`;
}
getCharLimit(): number {
return 63206; // ๐ Facebook's generous limit
}
}
// ๐ฏ Multi-platform poster
class SocialMediaManager {
private platforms: Map<string, SocialMediaPlatform> = new Map();
addPlatform(name: string, platform: SocialMediaPlatform): void {
this.platforms.set(name, platform);
console.log(`โ
Added ${name} platform`);
}
async postToAll(content: string): Promise<void> {
console.log("๐ข Posting to all platforms...");
for (const [name, platform] of this.platforms) {
const limit = platform.getCharLimit();
const trimmedContent = content.substring(0, limit);
try {
await platform.post(trimmedContent);
console.log(`โ
Posted to ${name}`);
} catch (error) {
console.log(`โ Failed to post to ${name}`);
}
}
console.log("๐ Multi-platform post complete!");
}
}
// ๐ฎ Test it out!
const manager = new SocialMediaManager();
manager.addPlatform("Twitter", new TwitterAdapter());
manager.addPlatform("Facebook", new FacebookAdapter());
await manager.postToAll("Check out the Adapter Pattern in TypeScript! It's amazing! ๐ #coding #typescript");
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create adapters to bridge incompatible interfaces ๐ช
- โ Integrate third-party libraries smoothly ๐ก๏ธ
- โ Apply the pattern in real-world scenarios ๐ฏ
- โ Avoid common adapter pitfalls like a pro ๐
- โ Build flexible, maintainable systems with TypeScript! ๐
Remember: The Adapter Pattern is your Swiss Army knife for making different parts of your code work together harmoniously! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered the Adapter Pattern!
Hereโs what to do next:
- ๐ป Practice with the social media exercise above
- ๐๏ธ Build adapters for your current projectโs integrations
- ๐ Move on to our next tutorial: Abstract Factory Pattern
- ๐ Share your adapter implementations with the community!
Remember: Every time you successfully connect two incompatible interfaces, youโre making the coding world a better place. Keep adapting, keep learning, and most importantly, have fun! ๐
Happy coding! ๐๐โจ