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:

  1. Interface Incompatibility Problems - When existing classes can’t work together
  2. Adapter Pattern Structure - Target, Adapter, Adaptee relationship
  3. Implementation Types - Object adapter vs Class adapter approaches
  4. Real-world Applications - Payment gateways, data format conversion, legacy integration
  5. Design Considerations - Performance, two-way adapters, when not to use
  6. 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:

  1. Target Interface: Defines the domain-specific interface that Client uses
  2. Adapter: Adapts the interface of Adaptee to the Target interface
  3. 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

  1. Misconception: Adapter pattern is the same as Wrapper pattern Reality: Adapter specifically deals with interface incompatibility, while Wrapper may simplify or add functionality

  2. Misconception: Adapters should convert all methods Reality: Adapters only need to implement the Target interface methods that clients actually use

  3. 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 LegacyPaymentAdapter implements the PaymentProcessor interface
  • It wraps the LegacyPaymentGateway and 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:

  1. Create a DatabaseConnection interface with methods: query(sql), insert(table, data), update(table, data, condition)
  2. You have a legacy OldDatabase class with methods: executeSQL(statement), addRecord(tableName, fields), modifyRecord(tableName, newFields, whereClause)
  3. Create an adapter that implements DatabaseConnection and wraps OldDatabase

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 ConfigurationProvider interface
  • 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

  1. 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

  2. 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

  3. 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

  4. 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

  1. Adapter Pattern bridges incompatible interfaces without modifying existing code
  2. Object adapters use composition and are more flexible than class adapters
  3. Keep adapters focused on interface translation, not business logic
  4. Real-world usage is everywhere - API wrappers, legacy integration, data format conversion

Quick Review Questions

  1. When would you choose Adapter pattern over direct modification of existing classes?
  2. What’s the difference between object adapter and class adapter approaches?
  3. 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