Adapter Pattern - Making Incompatible Interfaces Work Together
Learning Objectives: By the end of this lesson, you will be able to:
- Understand the Adapter pattern and identify when interfaces are incompatible
- Implement both object and class adapter patterns in multiple programming languages
- Apply Adapter pattern to integrate third-party libraries and legacy systems
- Distinguish between Adapter, Facade, and Wrapper patterns
- Recognize adapter patterns in popular frameworks and libraries
1. Welcome and Introduction (5 minutes)
Icebreaker Activity
Have you ever traveled to a country with different electrical outlets and needed a plug adapter? Or tried to use a library that expects different data formats than what your application provides? These are perfect examples of adapter problems in real life!
Lesson Overview
- Duration: 120 minutes
- Prerequisites: Object-oriented programming, interface/abstract class concepts
- Materials Needed: Development environment with Java, Python, or JavaScript support
2. Learning Objectives and Session Outline (5 minutes)
What We’ll Cover Today:
- Interface Incompatibility Problems - When existing classes can’t work together
- Adapter Pattern Structure - Target, Adapter, Adaptee relationship
- Implementation Types - Object adapter vs Class adapter approaches
- Real-world Applications - Payment gateways, data format conversion, legacy integration
- Design Considerations - Performance, two-way adapters, when not to use
- Modern Usage - API wrappers, microservice integration, data transformation
Success Criteria:
By the end of this session, you should be able to create adapters that seamlessly integrate incompatible systems and APIs.
3. Foundation Knowledge (15-20 minutes)
Core Concepts
What is the Adapter Pattern?
The Adapter pattern allows objects with incompatible interfaces to collaborate. It wraps an existing class with a new interface so that it becomes compatible with another class.
Real-World Analogy: Think of a USB-C to USB-A adapter. Your new laptop has USB-C ports, but your old device uses USB-A. The adapter makes them compatible without changing either device.
Why This Matters: In software development, you often need to integrate third-party libraries, legacy systems, or external APIs that don’t match your application’s expected interfaces. The Adapter pattern solves this without modifying existing code.
Problem Without Adapter
// Legacy payment system
class LegacyPaymentGateway {
public void makePayment(String accountNumber, double amount) {
System.out.println("Processing payment: $" + amount);
}
}
// Modern payment interface expected by your application
interface PaymentProcessor {
void processPayment(PaymentRequest request);
}
// Problem: Can't use LegacyPaymentGateway directly!
class PaymentService {
private PaymentProcessor processor;
public PaymentService(PaymentProcessor processor) {
this.processor = processor; // LegacyPaymentGateway doesn't implement this!
}
}
Key Terminology
- Target Interface: The interface your application expects to work with
- Adaptee: The existing class that needs to be adapted
- Adapter: The class that bridges the gap between Target and Adaptee
- Client: The code that uses the Target interface
4. Detailed Explanation and Examples (25-30 minutes)
Understanding Adapter Pattern Structure
The Adapter pattern involves three main components:
- Target Interface: Defines the domain-specific interface that Client uses
- Adapter: Adapts the interface of Adaptee to the Target interface
- Adaptee: Existing class that needs adapting
Practical Applications
The Adapter pattern is widely used for:
- API Integration: Wrapping REST APIs with domain-specific interfaces
- Data Format Conversion: Converting between XML, JSON, CSV formats
- Legacy System Integration: Making old systems work with new architectures
- Third-party Library Wrapping: Providing consistent interfaces across different vendors
- Database Access: Creating uniform interfaces for different database types
Common Misconceptions
-
Misconception: Adapter pattern is the same as Wrapper pattern Reality: Adapter specifically deals with interface incompatibility, while Wrapper may simplify or add functionality
-
Misconception: Adapters should convert all methods Reality: Adapters only need to implement the Target interface methods that clients actually use
-
Misconception: Adapter pattern is only for legacy integration Reality: It’s useful whenever you need to integrate any incompatible interfaces, including modern APIs
5. Live Coding Demonstration (20-25 minutes)
Example 1: Payment Gateway Adapter (Java)
// Target interface that our application expects
interface PaymentProcessor {
PaymentResult processPayment(PaymentRequest request);
boolean validatePayment(PaymentRequest request);
}
// PaymentRequest and PaymentResult classes
class PaymentRequest {
private String customerEmail;
private double amount;
private String currency;
public PaymentRequest(String email, double amount, String currency) {
this.customerEmail = email;
this.amount = amount;
this.currency = currency;
}
// Getters
public String getCustomerEmail() { return customerEmail; }
public double getAmount() { return amount; }
public String getCurrency() { return currency; }
}
class PaymentResult {
private boolean success;
private String transactionId;
private String message;
public PaymentResult(boolean success, String transactionId, String message) {
this.success = success;
this.transactionId = transactionId;
this.message = message;
}
public boolean isSuccess() { return success; }
public String getTransactionId() { return transactionId; }
public String getMessage() { return message; }
}
// Legacy payment system (Adaptee)
class LegacyPaymentGateway {
public String makePayment(String accountNumber, double amount) {
if (amount > 0 && accountNumber != null) {
String txnId = "TXN_" + System.currentTimeMillis();
System.out.println("Legacy payment processed: $" + amount + " -> " + txnId);
return txnId;
}
return null;
}
public boolean checkAccount(String accountNumber) {
return accountNumber != null && accountNumber.length() > 5;
}
}
// Adapter that makes LegacyPaymentGateway compatible with PaymentProcessor
class LegacyPaymentAdapter implements PaymentProcessor {
private LegacyPaymentGateway legacyGateway;
public LegacyPaymentAdapter(LegacyPaymentGateway legacyGateway) {
this.legacyGateway = legacyGateway;
}
@Override
public PaymentResult processPayment(PaymentRequest request) {
try {
// Convert PaymentRequest to legacy format
String accountNumber = extractAccountFromEmail(request.getCustomerEmail());
// Validate first
if (!validatePayment(request)) {
return new PaymentResult(false, null, "Validation failed");
}
// Call legacy system
String transactionId = legacyGateway.makePayment(accountNumber, request.getAmount());
if (transactionId != null) {
return new PaymentResult(true, transactionId, "Payment successful");
} else {
return new PaymentResult(false, null, "Payment failed");
}
} catch (Exception e) {
return new PaymentResult(false, null, "Error: " + e.getMessage());
}
}
@Override
public boolean validatePayment(PaymentRequest request) {
if (request.getAmount() <= 0) return false;
if (request.getCustomerEmail() == null) return false;
String accountNumber = extractAccountFromEmail(request.getCustomerEmail());
return legacyGateway.checkAccount(accountNumber);
}
private String extractAccountFromEmail(String email) {
// Simple extraction logic - in real world, this would be more sophisticated
return email.replaceAll("[@.]", "").toUpperCase();
}
}
Explanation:
- The
LegacyPaymentAdapterimplements thePaymentProcessorinterface - It wraps the
LegacyPaymentGatewayand translates method calls - Data conversion happens inside the adapter (email to account number)
- Error handling is added to make the legacy system more robust
Example 2: Data Format Adapter (Python)
from abc import ABC, abstractmethod
import json
import xml.etree.ElementTree as ET
from typing import Dict, Any
# Target interface
class DataProcessor(ABC):
@abstractmethod
def process_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
pass
@abstractmethod
def validate_data(self, data: Dict[str, Any]) -> bool:
pass
# Legacy XML processor (Adaptee)
class LegacyXMLProcessor:
def process_xml_string(self, xml_data: str) -> str:
"""Processes XML and returns XML result"""
try:
root = ET.fromstring(xml_data)
# Simulate processing - add timestamp
timestamp = ET.SubElement(root, "processed_at")
timestamp.text = "2024-01-01T10:00:00Z"
return ET.tostring(root, encoding='unicode')
except Exception as e:
return f"<error>Processing failed: {str(e)}</error>"
def validate_xml_string(self, xml_data: str) -> bool:
"""Validates XML format"""
try:
ET.fromstring(xml_data)
return True
except ET.ParseError:
return False
# Adapter for XML processor
class XMLDataAdapter(DataProcessor):
def __init__(self, xml_processor: LegacyXMLProcessor):
self.xml_processor = xml_processor
def process_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
try:
# Convert dict to XML
xml_string = self._dict_to_xml(data)
# Process with legacy system
result_xml = self.xml_processor.process_xml_string(xml_string)
# Convert back to dict
return self._xml_to_dict(result_xml)
except Exception as e:
return {"error": f"Processing failed: {str(e)}"}
def validate_data(self, data: Dict[str, Any]) -> bool:
try:
xml_string = self._dict_to_xml(data)
return self.xml_processor.validate_xml_string(xml_string)
except:
return False
def _dict_to_xml(self, data: Dict[str, Any]) -> str:
"""Convert dictionary to XML string"""
root = ET.Element("data")
self._build_xml_element(root, data)
return ET.tostring(root, encoding='unicode')
def _build_xml_element(self, parent: ET.Element, data: Any):
"""Recursively build XML elements from dictionary"""
if isinstance(data, dict):
for key, value in data.items():
child = ET.SubElement(parent, str(key))
self._build_xml_element(child, value)
elif isinstance(data, list):
for i, item in enumerate(data):
child = ET.SubElement(parent, f"item_{i}")
self._build_xml_element(child, item)
else:
parent.text = str(data)
def _xml_to_dict(self, xml_string: str) -> Dict[str, Any]:
"""Convert XML string back to dictionary"""
try:
root = ET.fromstring(xml_string)
return {root.tag: self._xml_element_to_dict(root)}
except Exception as e:
return {"error": f"XML conversion failed: {str(e)}"}
def _xml_element_to_dict(self, element: ET.Element) -> Any:
"""Recursively convert XML element to dictionary"""
result = {}
# Add text content if present
if element.text and element.text.strip():
if len(element) == 0: # Leaf element
return element.text.strip()
result['text'] = element.text.strip()
# Add child elements
for child in element:
child_data = self._xml_element_to_dict(child)
if child.tag in result:
# Multiple elements with same tag - convert to list
if not isinstance(result[child.tag], list):
result[child.tag] = [result[child.tag]]
result[child.tag].append(child_data)
else:
result[child.tag] = child_data
return result if result else None
# Usage example
def demonstrate_xml_adapter():
# Create legacy XML processor
legacy_processor = LegacyXMLProcessor()
# Create adapter
adapter = XMLDataAdapter(legacy_processor)
# Test data
test_data = {
"user": {
"name": "John Doe",
"email": "john@example.com",
"orders": ["order1", "order2"]
}
}
print("Original data:", json.dumps(test_data, indent=2))
# Validate
if adapter.validate_data(test_data):
print("✓ Data validation passed")
# Process
result = adapter.process_data(test_data)
print("Processed result:", json.dumps(result, indent=2))
else:
print("✗ Data validation failed")
if __name__ == "__main__":
demonstrate_xml_adapter()
Example 3: Multiple API Adapter (JavaScript)
// Target interface
class PaymentGateway {
async processPayment(amount, currency, customerInfo) {
throw new Error("processPayment must be implemented");
}
async refundPayment(transactionId, amount) {
throw new Error("refundPayment must be implemented");
}
}
// Third-party API #1 - Stripe-like (Adaptee)
class StripeAPI {
async createCharge(amountInCents, currency, source, description) {
// Simulate API call
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: `ch_${Date.now()}`,
amount: amountInCents,
currency: currency,
status: 'succeeded',
source: source
});
}, 100);
});
}
async createRefund(chargeId, amountInCents) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: `re_${Date.now()}`,
charge: chargeId,
amount: amountInCents,
status: 'succeeded'
});
}, 100);
});
}
}
// Third-party API #2 - PayPal-like (Adaptee)
class PayPalAPI {
async makePayment(paymentData) {
// Simulate API call
return new Promise((resolve) => {
setTimeout(() => {
resolve({
paymentId: `PAY-${Date.now()}`,
state: 'approved',
total: paymentData.total,
currency: paymentData.currency
});
}, 150);
});
}
async voidPayment(paymentId, refundAmount) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
refundId: `REF-${Date.now()}`,
parentPayment: paymentId,
refundAmount: refundAmount,
state: 'completed'
});
}, 150);
});
}
}
// Adapter for Stripe API
class StripeAdapter extends PaymentGateway {
constructor(stripeAPI) {
super();
this.stripeAPI = stripeAPI;
}
async processPayment(amount, currency, customerInfo) {
try {
// Convert amount to cents (Stripe expects cents)
const amountInCents = Math.round(amount * 100);
// Create charge using Stripe API
const charge = await this.stripeAPI.createCharge(
amountInCents,
currency,
customerInfo.paymentToken,
`Payment for ${customerInfo.email}`
);
return {
success: charge.status === 'succeeded',
transactionId: charge.id,
amount: charge.amount / 100, // Convert back to dollars
currency: charge.currency,
message: charge.status === 'succeeded' ? 'Payment successful' : 'Payment failed'
};
} catch (error) {
return {
success: false,
transactionId: null,
amount: amount,
currency: currency,
message: `Stripe error: ${error.message}`
};
}
}
async refundPayment(transactionId, amount) {
try {
const amountInCents = Math.round(amount * 100);
const refund = await this.stripeAPI.createRefund(transactionId, amountInCents);
return {
success: refund.status === 'succeeded',
refundId: refund.id,
amount: refund.amount / 100,
message: refund.status === 'succeeded' ? 'Refund successful' : 'Refund failed'
};
} catch (error) {
return {
success: false,
refundId: null,
amount: amount,
message: `Stripe refund error: ${error.message}`
};
}
}
}
// Adapter for PayPal API
class PayPalAdapter extends PaymentGateway {
constructor(paypalAPI) {
super();
this.paypalAPI = paypalAPI;
}
async processPayment(amount, currency, customerInfo) {
try {
const paymentData = {
total: amount,
currency: currency,
payerEmail: customerInfo.email,
description: 'Online payment'
};
const payment = await this.paypalAPI.makePayment(paymentData);
return {
success: payment.state === 'approved',
transactionId: payment.paymentId,
amount: payment.total,
currency: payment.currency,
message: payment.state === 'approved' ? 'Payment successful' : 'Payment failed'
};
} catch (error) {
return {
success: false,
transactionId: null,
amount: amount,
currency: currency,
message: `PayPal error: ${error.message}`
};
}
}
async refundPayment(transactionId, amount) {
try {
const refund = await this.paypalAPI.voidPayment(transactionId, amount);
return {
success: refund.state === 'completed',
refundId: refund.refundId,
amount: refund.refundAmount,
message: refund.state === 'completed' ? 'Refund successful' : 'Refund failed'
};
} catch (error) {
return {
success: false,
refundId: null,
amount: amount,
message: `PayPal refund error: ${error.message}`
};
}
}
}
// Payment service that can work with any adapter
class PaymentService {
constructor(paymentGateway) {
this.gateway = paymentGateway;
}
async makePayment(amount, currency, customerInfo) {
console.log(`Processing payment: $${amount} ${currency}`);
const result = await this.gateway.processPayment(amount, currency, customerInfo);
console.log('Payment result:', result);
return result;
}
async refund(transactionId, amount) {
console.log(`Processing refund: $${amount} for transaction ${transactionId}`);
const result = await this.gateway.refundPayment(transactionId, amount);
console.log('Refund result:', result);
return result;
}
}
// Usage demonstration
async function demonstrateAdapters() {
const stripeAPI = new StripeAPI();
const paypalAPI = new PayPalAPI();
const stripeAdapter = new StripeAdapter(stripeAPI);
const paypalAdapter = new PayPalAdapter(paypalAPI);
const customerInfo = {
email: 'customer@example.com',
paymentToken: 'tok_visa_4242'
};
// Test with Stripe
console.log('=== Testing Stripe Adapter ===');
const stripeService = new PaymentService(stripeAdapter);
const stripePayment = await stripeService.makePayment(99.99, 'USD', customerInfo);
if (stripePayment.success) {
await stripeService.refund(stripePayment.transactionId, 50.00);
}
// Test with PayPal - same interface!
console.log('\n=== Testing PayPal Adapter ===');
const paypalService = new PaymentService(paypalAdapter);
const paypalPayment = await paypalService.makePayment(149.99, 'USD', customerInfo);
if (paypalPayment.success) {
await paypalService.refund(paypalPayment.transactionId, 75.00);
}
}
// Run demonstration
demonstrateAdapters().catch(console.error);
6. Hands-On Practice (30-40 minutes)
Exercise 1: Database Adapter (15 minutes)
Objective: Create an adapter to make a legacy database class compatible with a modern interface
Instructions:
- Create a
DatabaseConnectioninterface with methods:query(sql),insert(table, data),update(table, data, condition) - You have a legacy
OldDatabaseclass with methods:executeSQL(statement),addRecord(tableName, fields),modifyRecord(tableName, newFields, whereClause) - Create an adapter that implements
DatabaseConnectionand wrapsOldDatabase
Starter Code:
interface DatabaseConnection {
List<Map<String, Object>> query(String sql);
boolean insert(String table, Map<String, Object> data);
boolean update(String table, Map<String, Object> data, String condition);
}
class OldDatabase {
public String executeSQL(String statement) {
// Returns CSV format: "id,name,email\n1,John,john@example.com"
return "id,name,email\n1,John Doe,john@example.com\n2,Jane Smith,jane@example.com";
}
public boolean addRecord(String tableName, String fields) {
// Expects format: "name='John',email='john@example.com'"
System.out.println("Adding to " + tableName + ": " + fields);
return true;
}
public boolean modifyRecord(String tableName, String newFields, String whereClause) {
// Updates records based on where clause
System.out.println("Updating " + tableName + " SET " + newFields + " WHERE " + whereClause);
return true;
}
}
// Your task: Create OldDatabaseAdapter class here
Solution:
import java.util.*;
class OldDatabaseAdapter implements DatabaseConnection {
private OldDatabase oldDatabase;
public OldDatabaseAdapter(OldDatabase oldDatabase) {
this.oldDatabase = oldDatabase;
}
@Override
public List<Map<String, Object>> query(String sql) {
String csvResult = oldDatabase.executeSQL(sql);
return parseCsvToMapList(csvResult);
}
@Override
public boolean insert(String table, Map<String, Object> data) {
String fields = convertMapToFieldsString(data);
return oldDatabase.addRecord(table, fields);
}
@Override
public boolean update(String table, Map<String, Object> data, String condition) {
String fields = convertMapToFieldsString(data);
return oldDatabase.modifyRecord(table, fields, condition);
}
private List<Map<String, Object>> parseCsvToMapList(String csv) {
List<Map<String, Object>> result = new ArrayList<>();
String[] lines = csv.split("\n");
if (lines.length < 2) return result;
String[] headers = lines[0].split(",");
for (int i = 1; i < lines.length; i++) {
String[] values = lines[i].split(",");
Map<String, Object> row = new HashMap<>();
for (int j = 0; j < headers.length && j < values.length; j++) {
row.put(headers[j], values[j]);
}
result.add(row);
}
return result;
}
private String convertMapToFieldsString(Map<String, Object> data) {
StringBuilder sb = new StringBuilder();
boolean first = true;
for (Map.Entry<String, Object> entry : data.entrySet()) {
if (!first) sb.append(",");
sb.append(entry.getKey()).append("='").append(entry.getValue()).append("'");
first = false;
}
return sb.toString();
}
}
Exercise 2: Configuration Adapter (20 minutes)
Objective: Create adapters for different configuration file formats (JSON, XML, Properties)
Requirements:
- Create a unified
ConfigurationProviderinterface - Implement adapters for JSON, XML, and Properties file formats
- Each adapter should convert its format to a common
Map<String, String>structure
Challenge: Create a configuration manager that can seamlessly switch between different file formats
Extension Ideas:
- Add support for nested configuration values
- Implement configuration writing capabilities
- Add validation for required configuration keys
7. Common Pitfalls and Troubleshooting (10 minutes)
Frequent Mistakes
-
Problem: Creating unnecessary adapters for simple type conversions Cause: Over-engineering simple compatibility issues Solution: Use adapters only when dealing with true interface incompatibility, not just data type differences
-
Problem: Adapter introduces performance overhead Cause: Complex data conversions or multiple adaptations in sequence Solution: Consider caching converted data or combining multiple adapters into one
-
Problem: Adapter becomes too complex with business logic Cause: Adding validation, transformation, and business rules to the adapter Solution: Keep adapters focused on interface translation only; delegate business logic elsewhere
-
Problem: Two-way adapters that maintain state inconsistently Cause: Attempting to synchronize bidirectional data flow Solution: Prefer unidirectional adapters or use more sophisticated synchronization patterns
Debugging Tips
- Log data transformations to understand what’s being converted
- Test with edge cases like null values, empty collections, malformed data
- Monitor performance especially with large data sets or frequent conversions
- Use type-safe adapters when possible to catch errors at compile time
8. Best Practices and Professional Tips (5 minutes)
Industry Standards
- Keep adapters stateless when possible for thread safety
- Use composition over inheritance for adapter implementations
- Implement proper error handling for malformed or unexpected data
- Document data transformation logic clearly for maintainability
Design Considerations
- Single Responsibility: Adapters should only handle interface translation
- Interface Segregation: Create small, focused target interfaces
- Dependency Inversion: Depend on abstractions, not concrete adapter implementations
When to Use Alternatives
- Facade Pattern: When you need to simplify a complex subsystem interface
- Wrapper Pattern: When you need to add functionality, not just adapt interfaces
- Proxy Pattern: When you need to control access or add cross-cutting concerns
Next Steps in Your Learning Journey
- Study Facade Pattern for simplifying complex interfaces
- Learn Bridge Pattern for separating abstractions from implementations
- Explore Strategy Pattern for algorithmic adaptation
- Practice with real-world API integration projects
9. Summary and Review (5 minutes)
Key Takeaways
- Adapter Pattern bridges incompatible interfaces without modifying existing code
- Object adapters use composition and are more flexible than class adapters
- Keep adapters focused on interface translation, not business logic
- Real-world usage is everywhere - API wrappers, legacy integration, data format conversion
Quick Review Questions
- When would you choose Adapter pattern over direct modification of existing classes?
- What’s the difference between object adapter and class adapter approaches?
- How does Adapter pattern differ from Facade pattern?
For Instructors
Teaching Tips
- Start with real examples from popular APIs that students may have used
- Show both successful and problematic adapter implementations
- Emphasize the importance of keeping adapters simple and focused
- Use visual diagrams to show the relationship between Target, Adapter, and Adaptee
- Demonstrate refactoring existing tightly-coupled code to use adapters
Common Student Questions
- “When should I use Adapter vs Facade?” - Adapter for incompatible interfaces, Facade for simplification
- “Can adapters contain business logic?” - No, keep them focused on interface translation only
- “Should I always prefer object adapters?” - Yes, they’re more flexible and support composition
Assessment Ideas
- Code review exercises identifying where adapters would improve design
- Integration projects requiring students to wrap real third-party APIs
- Refactoring challenges converting tightly-coupled code to use adapter pattern
- Performance analysis comparing direct integration vs adapter approaches
Adaptation Notes
- For beginners: Focus on simple one-to-one interface adaptation
- For advanced students: Explore complex data transformations and error handling strategies
- Language-specific considerations: Discuss interfaces vs duck typing in different languages
- Time management: The hands-on exercises can be extended with additional file format adapters