Adapter
Allows objects with incompatible interfaces to work together.
Adapter Pattern: A Complete Guide with Examples in 11 Programming Languages
The Adapter Pattern is a structural design pattern that allows incompatible interfaces to work together by acting as a bridge between them. It converts the interface of a class into another interface that clients expect, enabling classes with incompatible interfaces to collaborate. Whether you're integrating third-party libraries, working with legacy code, or creating flexible APIs, the Adapter Pattern provides a clean solution for interface incompatibility without modifying existing code.
In this comprehensive guide, we'll explore the Adapter Pattern with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin - complete with language-specific best practices, common pitfalls, and when to use this essential pattern.
Table of Contents
- What is the Adapter Pattern?
- Why Use the Adapter Pattern?
- Adapter Pattern Comparison
- Adapter Pattern Explained
- Class Diagram
- Beginner-Friendly Example
- Production-Ready Example
- Real-World Use Cases
- Language-Specific Mistakes and Anti-Patterns
- Frequently Asked Questions
- Key Takeaways
What is the Adapter Pattern?
The Adapter Pattern allows objects with incompatible interfaces to collaborate. It wraps an object with an incompatible interface and exposes a compatible interface that clients can use. The adapter translates calls from the client into calls that the wrapped object understands.
Think of it like a power adapter for international travel. Your American device has a two-prong plug (interface), but European outlets have different holes (incompatible interface). The power adapter (Adapter Pattern) sits between them, letting your device work with European outlets without modifying either the device or the outlet.
Why Use the Adapter Pattern?
The Adapter Pattern offers several compelling benefits:
- Reusability: Use existing classes even if their interfaces don't match
- Open/Closed Principle: Add new adapters without modifying existing code
- Single Responsibility: Adapter handles interface conversion, not business logic
- Legacy Integration: Integrate old code with new systems without rewriting
- Third-Party Libraries: Wrap external libraries to match your interfaces
- Testability: Easy to mock adapters for testing
- Flexibility: Switch between different implementations through adapters
Adapter Pattern Comparison
Let's compare Adapter with related structural patterns:
| Pattern | Purpose | When to Use | Modifies Interface |
|---|---|---|---|
| Adapter | Make incompatible interfaces compatible | Integrate existing classes with different interfaces | Yes - converts interface |
| Facade | Simplify complex subsystem | Provide simple interface to complex system | No - adds new interface |
| Decorator | Add responsibilities dynamically | Extend functionality without subclassing | No - same interface |
| Proxy | Control access to object | Add access control, lazy loading, logging | No - same interface |
| Bridge | Separate abstraction from implementation | Vary abstraction and implementation independently | No - parallel hierarchies |
Key Distinction: Adapter changes an existing interface to make it compatible, while other patterns work with compatible interfaces.
Adapter Pattern Explained
The Adapter Pattern has two main variants:
1. Class Adapter (Inheritance-based)
Uses multiple inheritance to adapt one interface to another.
2. Object Adapter (Composition-based)
Uses composition - adapter contains an instance of the adapted class.
Note: Most languages don't support multiple inheritance, so Object Adapter is more common.
Core Components:
- Target Interface: The interface clients expect
- Adaptee: The existing class with incompatible interface
- Adapter: Converts Adaptee's interface to Target interface
- Client: Uses Target interface to interact with objects
Class Diagram
Here's the UML class diagram showing the Adapter structure:
Beginner-Friendly Example
Let's start with a simple example: payment gateway integration. Different payment processors have different APIs, but we want a unified interface.
from abc import ABC, abstractmethod
from typing import Dict
# Target interface - what our application expects
class PaymentProcessor(ABC):
"""Unified payment interface"""
@abstractmethod
def process_payment(self, amount: float, currency: str) -> Dict[str, any]:
"""Process a payment and return result"""
pass
# Adaptee 1 - Stripe API (third-party)
class StripeAPI:
"""Stripe's existing API with different interface"""
def create_charge(self, amount_cents: int, currency: str, token: str) -> Dict:
"""Stripe's method signature is different"""
print(f"Stripe: Charging {amount_cents/100} {currency}")
return {
'id': 'ch_stripe123',
'status': 'succeeded',
'amount': amount_cents
}
# Adaptee 2 - PayPal API (third-party)
class PayPalAPI:
"""PayPal's existing API with different interface"""
def make_payment(self, total: float, currency_code: str) -> Dict:
"""PayPal's method signature is different"""
print(f"PayPal: Processing payment of {total} {currency_code}")
return {
'transaction_id': 'pp_12345',
'state': 'approved',
'total': total
}
# Adapter 1 - Stripe Adapter
class StripeAdapter(PaymentProcessor):
"""Adapts Stripe API to our PaymentProcessor interface"""
def __init__(self, stripe_api: StripeAPI):
self.stripe = stripe_api
def process_payment(self, amount: float, currency: str) -> Dict[str, any]:
# Convert our interface to Stripe's interface
amount_cents = int(amount * 100)
token = "tok_visa" # Simplified
# Call Stripe's method
result = self.stripe.create_charge(amount_cents, currency, token)
# Convert Stripe's response to our format
return {
'success': result['status'] == 'succeeded',
'transaction_id': result['id'],
'amount': amount,
'provider': 'stripe'
}
# Adapter 2 - PayPal Adapter
class PayPalAdapter(PaymentProcessor):
"""Adapts PayPal API to our PaymentProcessor interface"""
def __init__(self, paypal_api: PayPalAPI):
self.paypal = paypal_api
def process_payment(self, amount: float, currency: str) -> Dict[str, any]:
# Call PayPal's method with their parameter names
result = self.paypal.make_payment(amount, currency)
# Convert PayPal's response to our format
return {
'success': result['state'] == 'approved',
'transaction_id': result['transaction_id'],
'amount': amount,
'provider': 'paypal'
}
# Client code
class PaymentService:
"""Client that uses PaymentProcessor interface"""
def __init__(self, processor: PaymentProcessor):
self.processor = processor
def checkout(self, amount: float, currency: str):
print(f"\nProcessing checkout: {amount} {currency}")
result = self.processor.process_payment(amount, currency)
if result['success']:
print(f"ā Payment successful!")
print(f" Transaction ID: {result['transaction_id']}")
print(f" Provider: {result['provider']}")
else:
print(f"ā Payment failed")
return result
# Usage
if __name__ == "__main__":
print("="*60)
print("Payment Gateway Adapter Pattern Demo")
print("="*60)
# Use Stripe
stripe_api = StripeAPI()
stripe_adapter = StripeAdapter(stripe_api)
payment_service = PaymentService(stripe_adapter)
payment_service.checkout(99.99, "USD")
# Switch to PayPal - same interface!
paypal_api = PayPalAPI()
paypal_adapter = PayPalAdapter(paypal_api)
payment_service = PaymentService(paypal_adapter)
payment_service.checkout(149.99, "USD")
Production-Ready Example
Now let's look at a more realistic, production-ready implementation: a multi-database adapter system that provides a unified interface for different database engines with connection pooling, error handling, and query optimization.
š Python (Production Example)
from abc import ABC, abstractmethod
from typing import List, Dict, Any, Optional
import logging
from dataclasses import dataclass
from enum import Enum
import time
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Enums
class DatabaseType(Enum):
POSTGRESQL = "postgresql"
MYSQL = "mysql"
MONGODB = "mongodb"
# Data classes
@dataclass
class QueryResult:
success: bool
rows: List[Dict[str, Any]]
affected_rows: int
execution_time: float
error: Optional[str] = None
@dataclass
class ConnectionConfig:
host: str
port: int
database: str
username: str
password: str
pool_size: int = 10
# Target interface - Unified database interface
class DatabaseAdapter(ABC):
"""Unified database interface that all adapters must implement"""
@abstractmethod
def connect(self) -> bool:
"""Establish database connection"""
pass
@abstractmethod
def disconnect(self) -> bool:
"""Close database connection"""
pass
@abstractmethod
def execute_query(self, query: str, params: Optional[Dict] = None) -> QueryResult:
"""Execute a query and return results"""
pass
@abstractmethod
def execute_batch(self, queries: List[str]) -> List[QueryResult]:
"""Execute multiple queries in a batch"""
pass
@abstractmethod
def get_connection_status(self) -> Dict[str, Any]:
"""Get connection status and statistics"""
pass
# Adaptee 1 - PostgreSQL Driver (third-party)
class PostgreSQLDriver:
"""Simulated PostgreSQL driver with different interface"""
def __init__(self, config: Dict):
self.config = config
self.connected = False
self.query_count = 0
def pg_connect(self, host: str, port: int, db: str, user: str, pwd: str):
logger.info(f"PostgreSQL: Connecting to {host}:{port}/{db}")
self.connected = True
return {"status": "connected", "connection_id": "pg_12345"}
def pg_query(self, sql: str, bind_vars: Dict = None):
self.query_count += 1
logger.info(f"PostgreSQL: Executing query - {sql[:50]}...")
time.sleep(0.01) # Simulate query time
return {
"cursor": [{"id": 1, "name": "PostgreSQL Result"}],
"rowcount": 1,
"time_ms": 10
}
def pg_close(self):
logger.info("PostgreSQL: Connection closed")
self.connected = False
# Adaptee 2 - MySQL Driver (third-party)
class MySQLDriver:
"""Simulated MySQL driver with different interface"""
def __init__(self):
self.connection = None
self.stats = {"queries": 0, "errors": 0}
def mysql_connect(self, **kwargs):
logger.info(f"MySQL: Connecting to {kwargs.get('host')}:{kwargs.get('port')}")
self.connection = {"id": "mysql_67890", "active": True}
return self.connection
def mysql_execute(self, statement: str, parameters: tuple = ()):
self.stats["queries"] += 1
logger.info(f"MySQL: Executing - {statement[:50]}...")
time.sleep(0.01)
return {
"data": [{"id": 2, "name": "MySQL Result"}],
"affected": 1,
"duration": 0.01
}
def mysql_disconnect(self):
logger.info("MySQL: Disconnected")
self.connection = None
# Adaptee 3 - MongoDB Driver (third-party)
class MongoDBDriver:
"""Simulated MongoDB driver with different interface"""
def __init__(self, connection_string: str):
self.conn_string = connection_string
self.db = None
self.operation_count = 0
def mongo_connect(self):
logger.info(f"MongoDB: Connecting to {self.conn_string}")
self.db = {"name": "mongodb", "collections": []}
return {"acknowledged": True}
def mongo_find(self, collection: str, filter_doc: Dict = None):
self.operation_count += 1
logger.info(f"MongoDB: Finding in {collection}")
time.sleep(0.01)
return {
"documents": [{"_id": "abc123", "name": "MongoDB Result"}],
"count": 1
}
def mongo_close(self):
logger.info("MongoDB: Connection closed")
self.db = None
# Adapter 1 - PostgreSQL Adapter
class PostgreSQLAdapter(DatabaseAdapter):
"""Adapts PostgreSQL driver to unified interface"""
def __init__(self, config: ConnectionConfig):
self.config = config
self.driver = PostgreSQLDriver({})
self.connected = False
def connect(self) -> bool:
try:
result = self.driver.pg_connect(
host=self.config.host,
port=self.config.port,
db=self.config.database,
user=self.config.username,
pwd=self.config.password
)
self.connected = result["status"] == "connected"
return self.connected
except Exception as e:
logger.error(f"PostgreSQL connection failed: {e}")
return False
def disconnect(self) -> bool:
try:
self.driver.pg_close()
self.connected = False
return True
except Exception as e:
logger.error(f"PostgreSQL disconnect failed: {e}")
return False
def execute_query(self, query: str, params: Optional[Dict] = None) -> QueryResult:
start_time = time.time()
try:
result = self.driver.pg_query(query, params or {})
execution_time = time.time() - start_time
return QueryResult(
success=True,
rows=result["cursor"],
affected_rows=result["rowcount"],
execution_time=execution_time
)
except Exception as e:
execution_time = time.time() - start_time
logger.error(f"PostgreSQL query failed: {e}")
return QueryResult(
success=False,
rows=[],
affected_rows=0,
execution_time=execution_time,
error=str(e)
)
def execute_batch(self, queries: List[str]) -> List[QueryResult]:
return [self.execute_query(q) for q in queries]
def get_connection_status(self) -> Dict[str, Any]:
return {
"type": "PostgreSQL",
"connected": self.connected,
"queries_executed": self.driver.query_count,
"host": self.config.host,
"database": self.config.database
}
# Adapter 2 - MySQL Adapter
class MySQLAdapter(DatabaseAdapter):
"""Adapts MySQL driver to unified interface"""
def __init__(self, config: ConnectionConfig):
self.config = config
self.driver = MySQLDriver()
self.connected = False
def connect(self) -> bool:
try:
self.driver.mysql_connect(
host=self.config.host,
port=self.config.port,
database=self.config.database,
user=self.config.username,
password=self.config.password
)
self.connected = True
return True
except Exception as e:
logger.error(f"MySQL connection failed: {e}")
return False
def disconnect(self) -> bool:
try:
self.driver.mysql_disconnect()
self.connected = False
return True
except Exception as e:
logger.error(f"MySQL disconnect failed: {e}")
return False
def execute_query(self, query: str, params: Optional[Dict] = None) -> QueryResult:
start_time = time.time()
try:
# Convert dict params to tuple for MySQL
param_tuple = tuple(params.values()) if params else ()
result = self.driver.mysql_execute(query, param_tuple)
execution_time = time.time() - start_time
return QueryResult(
success=True,
rows=result["data"],
affected_rows=result["affected"],
execution_time=execution_time
)
except Exception as e:
execution_time = time.time() - start_time
logger.error(f"MySQL query failed: {e}")
return QueryResult(
success=False,
rows=[],
affected_rows=0,
execution_time=execution_time,
error=str(e)
)
def execute_batch(self, queries: List[str]) -> List[QueryResult]:
return [self.execute_query(q) for q in queries]
def get_connection_status(self) -> Dict[str, Any]:
return {
"type": "MySQL",
"connected": self.connected,
"queries_executed": self.driver.stats["queries"],
"errors": self.driver.stats["errors"],
"host": self.config.host,
"database": self.config.database
}
# Adapter 3 - MongoDB Adapter
class MongoDBAdapter(DatabaseAdapter):
"""Adapts MongoDB driver to unified interface"""
def __init__(self, config: ConnectionConfig):
self.config = config
conn_string = f"mongodb://{config.username}:{config.password}@{config.host}:{config.port}/{config.database}"
self.driver = MongoDBDriver(conn_string)
self.connected = False
def connect(self) -> bool:
try:
self.driver.mongo_connect()
self.connected = True
return True
except Exception as e:
logger.error(f"MongoDB connection failed: {e}")
return False
def disconnect(self) -> bool:
try:
self.driver.mongo_close()
self.connected = False
return True
except Exception as e:
logger.error(f"MongoDB disconnect failed: {e}")
return False
def execute_query(self, query: str, params: Optional[Dict] = None) -> QueryResult:
"""
Translate SQL-like queries to MongoDB operations
This is simplified - real implementation would parse query
"""
start_time = time.time()
try:
# Simplified: assume query contains collection name
collection = "default_collection"
filter_doc = params or {}
result = self.driver.mongo_find(collection, filter_doc)
execution_time = time.time() - start_time
return QueryResult(
success=True,
rows=result["documents"],
affected_rows=result["count"],
execution_time=execution_time
)
except Exception as e:
execution_time = time.time() - start_time
logger.error(f"MongoDB query failed: {e}")
return QueryResult(
success=False,
rows=[],
affected_rows=0,
execution_time=execution_time,
error=str(e)
)
def execute_batch(self, queries: List[str]) -> List[QueryResult]:
return [self.execute_query(q) for q in queries]
def get_connection_status(self) -> Dict[str, Any]:
return {
"type": "MongoDB",
"connected": self.connected,
"operations": self.driver.operation_count,
"host": self.config.host,
"database": self.config.database
}
# Database Factory (uses adapters)
class DatabaseFactory:
"""Factory to create appropriate database adapter"""
@staticmethod
def create_adapter(db_type: DatabaseType, config: ConnectionConfig) -> DatabaseAdapter:
adapters = {
DatabaseType.POSTGRESQL: PostgreSQLAdapter,
DatabaseType.MYSQL: MySQLAdapter,
DatabaseType.MONGODB: MongoDBAdapter
}
adapter_class = adapters.get(db_type)
if not adapter_class:
raise ValueError(f"Unsupported database type: {db_type}")
return adapter_class(config)
# Client - Data Access Layer
class DataAccessLayer:
"""High-level data access layer using unified interface"""
def __init__(self, adapter: DatabaseAdapter):
self.db = adapter
def initialize(self):
"""Initialize database connection"""
if self.db.connect():
logger.info("Database connection established")
return True
else:
logger.error("Database connection failed")
return False
def get_users(self) -> List[Dict]:
"""Get all users - works with any database"""
query = "SELECT * FROM users"
result = self.db.execute_query(query)
if result.success:
logger.info(f"Retrieved {len(result.rows)} users in {result.execution_time:.3f}s")
return result.rows
else:
logger.error(f"Failed to retrieve users: {result.error}")
return []
def create_user(self, name: str, email: str) -> bool:
"""Create a user - works with any database"""
query = "INSERT INTO users (name, email) VALUES (:name, :email)"
params = {"name": name, "email": email}
result = self.db.execute_query(query, params)
if result.success:
logger.info(f"User created: {name}")
return True
else:
logger.error(f"Failed to create user: {result.error}")
return False
def close(self):
"""Close database connection"""
self.db.disconnect()
# Usage Example
def demonstrate_database_adapters():
"""Demonstrate multi-database adapter system"""
print("="*60)
print("Multi-Database Adapter System")
print("="*60 + "\n")
# Configuration for different databases
pg_config = ConnectionConfig(
host="localhost",
port=5432,
database="myapp",
username="postgres",
password="secret"
)
mysql_config = ConnectionConfig(
host="localhost",
port=3306,
database="myapp",
username="root",
password="secret"
)
mongo_config = ConnectionConfig(
host="localhost",
port=27017,
database="myapp",
username="admin",
password="secret"
)
# Test with PostgreSQL
print("Testing with PostgreSQL:")
print("-" * 40)
pg_adapter = DatabaseFactory.create_adapter(DatabaseType.POSTGRESQL, pg_config)
dal_pg = DataAccessLayer(pg_adapter)
dal_pg.initialize()
dal_pg.get_users()
dal_pg.create_user("Alice", "alice@example.com")
print(f"Status: {pg_adapter.get_connection_status()}")
dal_pg.close()
# Test with MySQL
print("\nTesting with MySQL:")
print("-" * 40)
mysql_adapter = DatabaseFactory.create_adapter(DatabaseType.MYSQL, mysql_config)
dal_mysql = DataAccessLayer(mysql_adapter)
dal_mysql.initialize()
dal_mysql.get_users()
dal_mysql.create_user("Bob", "bob@example.com")
print(f"Status: {mysql_adapter.get_connection_status()}")
dal_mysql.close()
# Test with MongoDB
print("\nTesting with MongoDB:")
print("-" * 40)
mongo_adapter = DatabaseFactory.create_adapter(DatabaseType.MONGODB, mongo_config)
dal_mongo = DataAccessLayer(mongo_adapter)
dal_mongo.initialize()
dal_mongo.get_users()
dal_mongo.create_user("Charlie", "charlie@example.com")
print(f"Status: {mongo_adapter.get_connection_status()}")
dal_mongo.close()
print("\n" + "="*60)
print("Same interface, different implementations!")
print("="*60)
if __name__ == "__main__":
demonstrate_database_adapters()
Key Production Features:
- Unified Interface: Single DatabaseAdapter interface for all databases
- Error Handling: Comprehensive try-catch with logging
- Connection Management: Proper connect/disconnect lifecycle
- Performance Tracking: Execution time measurement
- Statistics: Query counts and connection status
- Configuration: Centralized connection config
- Factory Pattern: Easy database selection
- Type Safety: Enums and dataclasses
- Batch Operations: Support for multiple queries
- Production-Ready: Logging, error messages, status reporting
Real-World Use Cases
The Adapter Pattern is essential in many real-world scenarios:
1. Payment Gateway Integration
Unify different payment processors:
StripeAdapter, PayPalAdapter, SquareAdapter ā Unified PaymentProcessor
Examples: E-commerce platforms, billing systems, fintech apps Industries: E-commerce, SaaS, financial services
2. Database Abstraction Layers
Support multiple database engines:
PostgreSQLAdapter, MySQLAdapter, MongoDBAdapter ā Unified DatabaseInterface
Examples: ORMs (Hibernate, SQLAlchemy), multi-tenant systems Industries: Enterprise software, cloud platforms, data analytics
3. Cloud Provider Abstraction
Work with multiple cloud providers:
AWSAdapter, AzureAdapter, GCPAdapter ā Unified CloudStorage
Examples: Multi-cloud platforms, cloud migration tools Industries: DevOps, cloud infrastructure, SaaS platforms
4. Logging Systems
Unify different logging backends:
FileLoggerAdapter, SyslogAdapter, CloudWatchAdapter ā Unified Logger
Examples: Centralized logging, monitoring platforms Industries: Observability, enterprise applications, microservices
5. API Versioning
Support multiple API versions:
APIv1Adapter, APIv2Adapter, APIv3Adapter ā Unified APIInterface
Examples: REST APIs, GraphQL gateways, API management Industries: Web services, mobile backends, integration platforms
6. Legacy System Integration
Integrate old code with new systems:
LegacySystemAdapter ā Modern interface
Examples: Mainframe integration, COBOL wrappers, legacy migration Industries: Banking, insurance, government systems
7. Third-Party Library Wrappers
Wrap external libraries for testability:
HttpClientAdapter wraps Axios/Fetch ā Unified HTTP interface
Examples: Testing, dependency isolation, library swapping Industries: All software development
8. Message Queue Integration
Support different message brokers:
KafkaAdapter, RabbitMQAdapter, SQSAdapter ā Unified MessageQueue
Examples: Event-driven architectures, microservices communication Industries: Distributed systems, real-time processing, IoT
Language-Specific Mistakes and Anti-Patterns
ā Mistake #1: Not Using ABC for Target Interface
# BAD: No enforcement
class PaymentProcessor:
def process(self, amount):
pass # Subclasses might forget
# GOOD: Use ABC
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def process(self, amount):
pass # Must be implemented
ā Mistake #2: Adapter Doing Business Logic
# BAD: Adapter contains business logic
class StripeAdapter:
def process(self, amount):
if amount > 1000: # Business logic!
amount *= 0.9 # Discount
return self.stripe.charge(amount)
# GOOD: Adapter only translates interface
class StripeAdapter:
def process(self, amount):
return self.stripe.charge(amount) # Just translation
ā Mistake #3: Tight Coupling to Adaptee
# BAD: Adapter creates adaptee
class StripeAdapter:
def __init__(self, api_key):
self.stripe = StripeAPI(api_key) # Creates dependency
# GOOD: Inject adaptee
class StripeAdapter:
def __init__(self, stripe_api):
self.stripe = stripe_api # Dependency injection
Frequently Asked Questions
Q1: Adapter vs Facade - What's the difference?
Adapter: Converts one interface to another to make incompatible interfaces work together. One-to-one relationship.
Facade: Simplifies a complex subsystem by providing a simple interface. One-to-many relationship.
# Adapter - interface conversion
class StripeAdapter(PaymentProcessor):
def process(self, amount):
return self.stripe.charge(amount * 100) # Converts interface
# Facade - simplification
class PaymentFacade:
def __init__(self):
self.stripe = Stripe()
self.validator = Validator()
self.logger = Logger()
def pay(self, amount):
self.validator.validate(amount)
result = self.stripe.charge(amount)
self.logger.log(result)
return result # Simplifies multiple subsystems
Q2: When should I use class adapter vs object adapter?
Object Adapter (Composition) - Preferred:
- More flexible (can adapt multiple adaptees)
- Works in languages without multiple inheritance
- Follows "composition over inheritance"
Class Adapter (Inheritance) - Rarely used:
- Only if language supports multiple inheritance
- Slightly more efficient (no indirection)
- Can override adaptee behavior
Recommendation: Always use object adapter unless you have a specific reason not to.
Q3: Can one adapter adapt multiple adaptees?
Yes! This is common for creating unified interfaces:
class UnifiedDatabaseAdapter:
def __init__(self, sql_db, nosql_db):
self.sql = sql_db
self.nosql = nosql_db
def query(self, query_type, data):
if query_type == "relational":
return self.sql.execute(data)
else:
return self.nosql.find(data)
Q4: Should adapters be stateless?
Prefer stateless when possible:
- Easier to test
- No shared state issues
- Can be reused
Stateful is okay when:
- Caching results
- Connection pooling
- Performance optimization
Just ensure thread safety if stateful.
Q5: How do I test code using adapters?
Strategy 1: Mock the adapter
def test_payment_service():
mock_adapter = Mock(spec=PaymentProcessor)
mock_adapter.process.return_value = PaymentResult(success=True)
service = PaymentService(mock_adapter)
result = service.checkout(99.99)
assert result.success
mock_adapter.process.assert_called_once()
Strategy 2: Create test adapter
class TestPaymentAdapter(PaymentProcessor):
def process(self, amount):
return PaymentResult(success=True, id="test_123")
def test_with_test_adapter():
service = PaymentService(TestPaymentAdapter())
assert service.checkout(99.99).success
Q6: Can adapters change behavior, not just interface?
Adapters should only translate interface, not add business logic:
# BAD: Adapter adds behavior
class DiscountAdapter(PaymentProcessor):
def process(self, amount):
discounted = amount * 0.9 # Business logic!
return self.stripe.charge(discounted)
# GOOD: Keep adapter simple
class StripeAdapter(PaymentProcessor):
def process(self, amount):
return self.stripe.charge(amount) # Just translation
# Put business logic in service
class PaymentService:
def checkout(self, amount, discount=0):
final_amount = amount * (1 - discount) # Business logic here
return self.adapter.process(final_amount)
Q7: How do I handle versioning with adapters?
Create separate adapters for each version:
class APIv1Adapter(APIInterface):
def get_user(self, user_id):
return self.api.users.get(id=user_id) # v1 syntax
class APIv2Adapter(APIInterface):
def get_user(self, user_id):
return self.api.fetch_user(user_id) # v2 syntax
# Factory selects appropriate adapter
def get_adapter(version):
if version == 1:
return APIv1Adapter(APIv1Client())
else:
return APIv2Adapter(APIv2Client())
Q8: Should I create adapters for all third-party libraries?
Not always. Create adapters when:
- Library's interface doesn't match your domain
- You might switch libraries later
- Testing requires isolation
- Library is complex or unstable
Skip adapters when:
- Library already matches your needs
- One-time use
- Library is stable and unlikely to change
- Performance is critical (avoid extra layer)
Q9: How do I handle errors from adaptees?
Translate exceptions to domain exceptions:
class PaymentAdapter:
def process(self, amount):
try:
return self.stripe.charge(amount)
except StripeNetworkError as e:
raise PaymentNetworkError("Network failed") from e
except StripeInvalidCard as e:
raise PaymentValidationError("Invalid card") from e
except Exception as e:
raise PaymentError("Payment failed") from e
This way clients don't depend on third-party exception types.
Q10: Can adapters be bidirectional?
Yes, though rare. Bidirectional adapters translate in both directions:
class BidirectionalAdapter:
def to_external(self, internal_data):
# Internal format ā External format
return ExternalFormat(internal_data.value)
def to_internal(self, external_data):
# External format ā Internal format
return InternalFormat(external_data.value)
Usually you create two separate adapters instead.
Key Takeaways
šÆ Core Concept: Adapter Pattern makes incompatible interfaces work together by converting one interface into another that clients expect. It's a bridge between incompatible systems.
š Key Benefits:
- Reusability: Use existing classes with incompatible interfaces
- Flexibility: Switch implementations easily
- Open/Closed: Add new adapters without modifying existing code
- Testability: Easy to mock adapters for testing
- Integration: Perfect for third-party libraries and legacy systems
š Implementation Variants:
- Object Adapter (composition): Preferred, more flexible
- Class Adapter (inheritance): Rarely used, requires multiple inheritance
š When to Use Adapter:
- Integrating third-party libraries
- Working with legacy code
- Creating reusable components
- Need to switch between implementations
- Different API versions
ā ļø When NOT to Use Adapter:
- Interfaces already compatible
- Can modify source code directly
- Simple one-off integration
- Performance is critical (extra layer adds overhead)
š ļø Best Practices:
- Keep adapters simple: Only translate interface, no business logic
- Use composition: Prefer object adapter over class adapter
- Translate exceptions: Convert third-party exceptions to domain exceptions
- Make stateless: Avoid adapter state when possible
- Document adaptation: Explain what's being adapted and why
- Test thoroughly: Test both adapter and integrated system
- Version carefully: Create separate adapters for different versions
š” Common Use Cases:
- Payment gateway integration
- Database abstraction layers
- Cloud provider abstraction
- API versioning
- Legacy system modernization
- Third-party library wrapping
- Message queue integration
- Logging system unification
The Adapter Pattern is one of the most practical structural patterns. Use it whenever you need to make incompatible interfaces work together, but remember to keep adapters focused on interface translation, not business logic.
Want to dive deeper into design patterns? Check out our comprehensive guides on:
- Bridge Pattern
- Decorator Pattern
- Facade Pattern
- Proxy Pattern
- Composite Pattern Each guide includes examples in all 11 programming languages with language-specific best practices.