Python Adapter Pattern

The Adapter Pattern is a structural design pattern that allows incompatible interfaces to collaborate. Just as a power adapter in real life allows electrical plugs from different countries to connect to local sockets, in programming, the Adapter Pattern acts as a bridge between two incompatible interfaces.

Core Concepts

The Adapter Pattern mainly solves the problem of interface mismatch. When we need to use an existing class but its interface does not meet our requirements, the Adapter Pattern comes in handy. It converts the interface by creating an intermediate layer (adapter), enabling originally incompatible classes to work together.

Pattern Structure

The Adapter Pattern consists of three main roles:

  • Target Interface (Target): The interface expected by the client
  • Adapter (Adapter): Implements the target interface and wraps the adaptee
  • Adaptee (Adaptee): The existing class that needs to be adapted

Why Do We Need the Adapter Pattern?

Real-World Problem Scenario

Imagine you have an existing logging system that uses a specific method to record logs:

Example

class LegacyLogger:
    def write_log(self, message):
        print(f"[LEGACY] {message}")

But now you want to use a new logging interface:

Example

class NewLogger:
    def log(self, level, message):
        print(f"[{level.upper()}] {message}")

Without an adapter, you would need to modify all code that calls the old logging system, which is time-consuming and error-prone.

Advantages of the Adapter

  1. Code reuse: Reuse existing classes without modifying source code
  2. Decoupling: Separate interface conversion logic from business logic
  3. Flexibility: Easily switch between different implementations
  4. Adheres to the Open-Closed Principle: Open for extension, closed for modification

Implementation Approaches of the Adapter Pattern

Class Adapter (Using Inheritance)

Class adapter implements interface adaptation through multiple inheritance:

Example

class LegacyLogger:
    """Legacy log class being adapted"""
    def write_log(self, message):
        print(f"[LEGACY] {message}")

class NewLoggerInterface:
    """Target interface"""
    def log(self, level, message):
        pass

class LoggerAdapter(NewLoggerInterface, LegacyLogger):
    """Class adapter - implemented through inheritance"""
    def log(self, level, message):
        # Convert the new interface to the old interface
        formatted_message = f"{level}: {message}"
        self.write_log(formatted_message)

# Usage example
logger = LoggerAdapter()
logger.log("INFO", "System started successfully")
logger.log("ERROR", "Database connection failed")

Output:

[LEGACY] INFO: 系统启动成功
[LEGACY] ERROR: 数据库连接失败

Object Adapter (Using Composition)

Object adapter is implemented through composition, which is the more common approach:

Example

class LegacyPayment:
    """Legacy payment system being adapted"""
    def process_payment(self, amount_in_dollars):
        print(f"Processing payment: ${amount_in_dollars}")
        return True

class NewPaymentInterface:
    """New payment interface"""
    def pay(self, amount, currency="CNY"):
        pass

class PaymentAdapter(NewPaymentInterface):
    """Object adapter - implemented through composition"""
   
    def __init__(self, legacy_payment):
        self.legacy_payment = legacy_payment
   
    def pay(self, amount, currency="CNY"):
        # Currency conversion logic
        if currency == "CNY":
            amount_in_dollars = amount / 7.0  # Simplified exchange rate conversion
        else:
            amount_in_dollars = amount
       
        # Call the old system's interface
        return self.legacy_payment.process_payment(amount_in_dollars)

# Usage example
legacy_payment = LegacyPayment()
payment = PaymentAdapter(legacy_payment)

# Use the new interface to call the old system
payment.pay(700, "CNY")  # Pay 700 RMB
payment.pay(100, "USD")  # Pay 100 USD

Output:

处理支付: $100.0
处理支付: $100

Real-World Application Cases

Case 1: Data Format Adapter

Example

import json

class XMLData:
    """XML data source"""
    def get_xml_data(self):
        return """
        <users>
            <user>
<name>Zhang San</name>
                <age>25</age>
            </user>
            <user>
<name>Li Si</name>
                <age>30</age>
            </user>
        </users>
        """


class JSONProcessor:
    """JSON processor (expects JSON format)"""
    def process_json(self, json_data):
        data = json.loads(json_data)
        for user in data['users']:
            print(f"Name: {user['name']}, Age: {user['age']}")

class XMLToJSONAdapter:
    """XML to JSON adapter"""
   
    def __init__(self, xml_data):
        self.xml_data = xml_data
   
    def convert_to_json(self):
        # Simplified XML to JSON conversion
        xml_content = self.xml_data.get_xml_data()
        # In real applications, you should use libraries such as xml.etree.ElementTree for parsing
        json_data = {
            "users": [
                {"name": "Zhang San", "age": 25},
                {"name": "Li Si", "age": 30}
            ]
        }
        return json.dumps(json_data, ensure_ascii=False)

# Use the adapter
xml_source = XMLData()
adapter = XMLToJSONAdapter(xml_source)
json_processor = JSONProcessor()

json_data = adapter.convert_to_json()
json_processor.process_json(json_data)

Output:

姓名: 张三, 年龄: 25
姓名: 李四, 年龄: 30

Case 2: Third-Party Service Adapter

Example

class WeChatPay:
    """WeChat Pay SDK"""
    def wechat_pay(self, amount, user_id):
        print(f"WeChat Pay: User {user_id} paid {amount} yuan")
        return {"status": "success", "transaction_id": "wx_123456"}

class AliPay:
    """Alipay SDK"""
    def alipay(self, money, user_account):
        print(f"Alipay: Account {user_account} paid {money} yuan")
        return {"code": 200, "trade_no": "ali_789012"}

class UnifiedPaymentAdapter:
    """Unified payment adapter"""
   
    def __init__(self, payment_service):
        self.payment_service = payment_service
   
    def pay(self, amount, user_info):
        # Unified payment interface
        if isinstance(self.payment_service, WeChatPay):
            return self.payment_service.wechat_pay(amount, user_info)
        elif isinstance(self.payment_service, AliPay):
            return self.payment_service.alipay(amount, user_info)
        else:
            raise ValueError("Unsupported payment method")

# Client code can make unified calls
def process_order(payment_adapter, amount, user_info):
    """Process order payment"""
    result = payment_adapter.pay(amount, user_info)
    print(f"Payment result: {result}")
    return result

# Usage example
wechat_pay = WeChatPay()
ali_pay = AliPay()

wechat_adapter = UnifiedPaymentAdapter(wechat_pay)
ali_adapter = UnifiedPaymentAdapter(ali_pay)

# Unified calling approach
process_order(wechat_adapter, 100, "user_001")
process_order(ali_adapter, 200, "user_002")

Output:

微信支付: 用户user_001支付100元
支付结果: {'status': 'success', 'transaction_id': 'wx_123456'}
支付宝: 账户user_002支付200元
支付结果: {'code': 200, 'trade_no': 'ali_789012'}

Best Practices for the Adapter Pattern

1. Choose the Right Implementation Approach

Implementation Approach Applicable Scenario Advantages Disadvantages
Class Adapter Few adaptee classes and simple target interface Simple implementation, less code Requires multiple inheritance, may complicate class hierarchy
Object Adapter Most scenarios, especially when needing to adapt multiple classes More flexible, follows composition over inheritance principle Requires creating adapter objects

2. Keep the Adapter's Single Responsibility

Example

# Good practice: single responsibility
class DataFormatAdapter:
    """Only responsible for data format conversion"""
    def convert_format(self, data):
        pass

# Bad practice: too many responsibilities
class BadAdapter:
    """Handles format conversion, validation, caching, etc."""
    def convert_format(self, data):
        pass
   
    def validate_data(self, data):
        pass
   
    def cache_data(self, data):
        pass

3. Use Interface Abstraction

Example

from abc import ABC, abstractmethod

class PaymentInterface(ABC):
    """Payment interface abstraction"""
    @abstractmethod
    def pay(self, amount, user_info):
        pass

class LoggerInterface(ABC):
    """Logging interface abstraction"""
    @abstractmethod
    def log(self, level, message):
        pass

Common Problems and Solutions

Problem 1: Too Many Adapters Increase Complexity

Solution: Use the Factory Pattern to create adapters

Example

class AdapterFactory:
    """Adapter factory"""
   
    @staticmethod
    def create_payment_adapter(service_type):
        if service_type == "wechat":
            return UnifiedPaymentAdapter(WeChatPay())
        elif service_type == "alipay":
            return UnifiedPaymentAdapter(AliPay())
        else:
            raise ValueError(f"Unsupported payment type: {service_type}")

# Use the factory to create adapters
wechat_adapter = AdapterFactory.create_payment_adapter("wechat")
ali_adapter = AdapterFactory.create_payment_adapter("alipay")

Problem 2: Performance Considerations

Solution: Implement lazy loading and caching

Example

class CachingAdapter:
    """Adapter with caching"""
   
    def __init__(self, adaptee):
        self.adaptee = adaptee
        self._cache = {}
   
    def process_data(self, key):
        if key not in self._cache:
            # Simulate time-consuming operation
            result = self.adaptee.expensive_operation(key)
            self._cache[key] = result
        return self._cache[key]

Practice Exercises

Exercise 1: Create a File Format Adapter

Implement a file format adapter that converts CSV format data to JSON format:

Example

class CSVReader:
    """CSV file reader"""
    def read_csv(self, filepath):
        # Simulate reading a CSV file
        return [
            ["name", "age", "city"],
            ["Alice", "25", "Beijing"],
            ["Bob", "30", "Shanghai"]
        ]

class JSONExporter:
    """JSON exporter"""
    def export_json(self, data):
        import json
        print("Export JSON data:")
        print(json.dumps(data, indent=2, ensure_ascii=False))

# Please implement the CSVToJSONAdapter class here
# Your code...

# Test code
csv_reader = CSVReader()
json_exporter = JSONExporter()

# Create the adapter and test
adapter = CSVToJSONAdapter(csv_reader)
json_data = adapter.convert_to_json()
json_exporter.export_json(json_data)

Exercise 2: Design Thinking

Consider the following scenario: How would you use the Adapter Pattern?

  1. Your application needs to support multiple databases (MySQL, PostgreSQL, SQLite), but each database has a different connection method.
  2. You need to integrate multiple third-party APIs, but their authentication methods and return formats are all different.
  3. Your system needs to support multiple message queues (RabbitMQ, Kafka, Redis).

Please design an adapter pattern solution for one of these scenarios.


Summary

The adapter pattern is an effective tool for solving interface incompatibility issues. Through this article, you should master:

  • The basic concepts and applicable scenarios of the adapter pattern.
  • Implementation approaches of class adapters and object adapters.
  • Application techniques in real projects.
  • Best practices and precautions for the adapter pattern.

Remember, the core idea of the adapter pattern is "conversion" rather than "modification". It helps us integrate new functionality without changing existing code, and it is an important design pattern for maintaining large systems.

In actual development, using the adapter pattern appropriately can make your code more flexible, maintainable, and easy to extend.

Other Extensions