# Adapter Pattern

1. Giới Thiệu

1.1 Định Nghĩa

Adapter Pattern là một structural pattern cho phép các objects với interfaces không tương thích có thể làm việc cùng nhau. Nó hoạt động như một wrapper, chuyển đổi interface của một class thành một interface khác mà client mong đợi.

1.2 Mục Đích

  • Cho phép các classes không liên quan làm việc cùng nhau
  • Chuyển đổi interface của một class thành interface khác
  • Tái sử dụng classes hiện có với interface không tương thích
  • Cung cấp interface thống nhất cho các hành vi tương tự

2. Các Loại Adapter

2.1 Class Adapter (Inheritance)

// Target interface
public interface USPlug {
    void supplyPower();
}

// Adaptee
public class EUPlug {
    public void providePower() {
        System.out.println("Supplying power through EU socket");
    }
}

// Class Adapter
public class EUToUSPlugAdapter extends EUPlug implements USPlug {
    @Override
    public void supplyPower() {
        providePower();
    }
}

2.2 Object Adapter (Composition)

// Target interface
public interface PaymentProcessor {
    void processPayment(double amount);
}

// Adaptee
public class LegacyPaymentSystem {
    public void makePayment(String amount, String currency) {
        System.out.println("Processing payment of " + 
            amount + " " + currency);
    }
}

// Object Adapter
public class PaymentSystemAdapter implements PaymentProcessor {
    private LegacyPaymentSystem legacySystem;

    public PaymentSystemAdapter(LegacyPaymentSystem legacySystem) {
        this.legacySystem = legacySystem;
    }

    @Override
    public void processPayment(double amount) {
        String formattedAmount = String.format("%.2f", amount);
        legacySystem.makePayment(formattedAmount, "USD");
    }
}

3. Ví Dụ Thực Tế

3.1 XML to JSON Adapter

public class XMLData {
    private String xmlContent;

    public String getXmlContent() {
        return xmlContent;
    }

    public void setXmlContent(String xmlContent) {
        this.xmlContent = xmlContent;
    }
}

public interface JSONConverter {
    JSONObject convertToJson();
}

public class XMLToJSONAdapter implements JSONConverter {
    private XMLData xmlData;

    public XMLToJSONAdapter(XMLData xmlData) {
        this.xmlData = xmlData;
    }

    @Override
    public JSONObject convertToJson() {
        try {
            // Convert XML to JSONObject
            JSONObject jsonObject = XML.toJSONObject(
                xmlData.getXmlContent());
            return jsonObject;
        } catch (JSONException e) {
            throw new ConversionException(
                "Failed to convert XML to JSON", e);
        }
    }
}

// Usage
XMLData xmlData = new XMLData();
xmlData.setXmlContent("<user><name>John</name></user>");

JSONConverter converter = new XMLToJSONAdapter(xmlData);
JSONObject jsonObject = converter.convertToJson();

3.2 Database Adapter

public interface DatabaseAdapter {
    void connect();
    void executeQuery(String query);
    void disconnect();
}

public class MySQLDatabase {
    public void establishConnection() {
        System.out.println("Connecting to MySQL");
    }

    public void runQuery(String sql) {
        System.out.println("Running MySQL query: " + sql);
    }

    public void closeConnection() {
        System.out.println("Closing MySQL connection");
    }
}

public class PostgreSQLDatabase {
    public void openConnection() {
        System.out.println("Opening PostgreSQL connection");
    }

    public void executeSQL(String sql) {
        System.out.println("Executing PostgreSQL query: " + sql);
    }

    public void terminateConnection() {
        System.out.println("Terminating PostgreSQL connection");
    }
}

public class MySQLAdapter implements DatabaseAdapter {
    private MySQLDatabase mysql;

    public MySQLAdapter(MySQLDatabase mysql) {
        this.mysql = mysql;
    }

    @Override
    public void connect() {
        mysql.establishConnection();
    }

    @Override
    public void executeQuery(String query) {
        mysql.runQuery(query);
    }

    @Override
    public void disconnect() {
        mysql.closeConnection();
    }
}

public class PostgreSQLAdapter implements DatabaseAdapter {
    private PostgreSQLDatabase postgresql;

    public PostgreSQLAdapter(PostgreSQLDatabase postgresql) {
        this.postgresql = postgresql;
    }

    @Override
    public void connect() {
        postgresql.openConnection();
    }

    @Override
    public void executeQuery(String query) {
        postgresql.executeSQL(query);
    }

    @Override
    public void disconnect() {
        postgresql.terminateConnection();
    }
}

4. Spring Framework Integration

4.1 REST API Adapter

@RestController
@RequestMapping("/api")
public class PaymentController {
    private final PaymentProcessor paymentProcessor;

    public PaymentController(PaymentProcessor paymentProcessor) {
        this.paymentProcessor = paymentProcessor;
    }

    @PostMapping("/payments")
    public ResponseEntity<PaymentResponse> processPayment(
            @RequestBody PaymentRequest request) {
        try {
            paymentProcessor.processPayment(
                request.getAmount());
            return ResponseEntity.ok(
                new PaymentResponse("Success"));
        } catch (Exception e) {
            return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new PaymentResponse(
                    "Payment failed: " + e.getMessage()));
        }
    }
}

@Configuration
public class PaymentConfig {
    @Bean
    public PaymentProcessor paymentProcessor(
            LegacyPaymentSystem legacySystem) {
        return new PaymentSystemAdapter(legacySystem);
    }
}

4.2 Data Access Adapter

public interface UserRepository {
    User findById(Long id);
    List<User> findAll();
    void save(User user);
}

@Repository
public class JpaUserRepositoryAdapter implements UserRepository {
    private final JpaUserRepository jpaRepository;

    public JpaUserRepositoryAdapter(JpaUserRepository jpaRepository) {
        this.jpaRepository = jpaRepository;
    }

    @Override
    public User findById(Long id) {
        return jpaRepository.findById(id)
            .map(this::convertToUser)
            .orElse(null);
    }

    @Override
    public List<User> findAll() {
        return jpaRepository.findAll().stream()
            .map(this::convertToUser)
            .collect(Collectors.toList());
    }

    @Override
    public void save(User user) {
        UserEntity entity = convertToEntity(user);
        jpaRepository.save(entity);
    }

    private User convertToUser(UserEntity entity) {
        return User.builder()
            .id(entity.getId())
            .username(entity.getUsername())
            .email(entity.getEmail())
            .build();
    }

    private UserEntity convertToEntity(User user) {
        return UserEntity.builder()
            .id(user.getId())
            .username(user.getUsername())
            .email(user.getEmail())
            .build();
    }
}

5. Testing Adapter Pattern

5.1 Unit Testing

@ExtendWith(MockitoExtension.class)
class PaymentSystemAdapterTest {
    @Mock
    private LegacyPaymentSystem legacySystem;

    @InjectMocks
    private PaymentSystemAdapter adapter;

    @Test
    void whenProcessPayment_thenCallLegacySystem() {
        // Given
        double amount = 100.50;

        // When
        adapter.processPayment(amount);

        // Then
        verify(legacySystem).makePayment("100.50", "USD");
    }

    @Test
    void whenProcessPayment_withZeroAmount_thenCallLegacySystem() {
        // Given
        double amount = 0.00;

        // When
        adapter.processPayment(amount);

        // Then
        verify(legacySystem).makePayment("0.00", "USD");
    }
}

5.2 Integration Testing

@SpringBootTest
class DatabaseAdapterIntegrationTest {
    @Autowired
    private DatabaseAdapter databaseAdapter;

    @Test
    void whenConnectAndExecuteQuery_thenSuccess() {
        // Given
        String query = "SELECT * FROM users";

        // When/Then
        assertDoesNotThrow(() -> {
            databaseAdapter.connect();
            databaseAdapter.executeQuery(query);
            databaseAdapter.disconnect();
        });
    }
}

6. Lợi Ích và Nhược Điểm

6.1 Lợi Ích

  1. Tăng tính tái sử dụng của code
  2. Tăng tính linh hoạt của code
  3. Tách biệt interface và implementation
  4. Dễ dàng thay đổi libraries bên thứ ba
  5. Đơn giản hóa testing

6.2 Nhược Điểm

  1. Tăng độ phức tạp của code
  2. Khó debug khi có nhiều adapters
  3. Có thể ảnh hưởng đến performance
  4. Cần thêm nhiều code boilerplate

7. Best Practices

7.1 Single Responsibility

// Bad: Adapter doing too much
public class PaymentAdapter implements PaymentProcessor {
    private LegacySystem legacy;
    private Logger logger;
    private MetricsCollector metrics;

    @Override
    public void processPayment(double amount) {
        // Logging
        logger.info("Processing payment: " + amount);

        // Metrics
        metrics.recordPaymentAttempt();

        // Validation
        if (amount <= 0) {
            throw new IllegalArgumentException(
                "Invalid amount");
        }

        // Currency conversion
        double convertedAmount = amount * 1.2;

        // Process payment
        legacy.makePayment(String.valueOf(convertedAmount));

        // More logging and metrics
        logger.info("Payment processed");
        metrics.recordPaymentSuccess();
    }
}

// Good: Separate responsibilities
public class PaymentAdapter implements PaymentProcessor {
    private final LegacySystem legacy;
    private final CurrencyConverter converter;

    @Override
    public void processPayment(double amount) {
        validateAmount(amount);
        double convertedAmount = converter.convert(amount);
        legacy.makePayment(String.valueOf(convertedAmount));
    }

    private void validateAmount(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException(
                "Invalid amount");
        }
    }
}

@Aspect
@Component
public class PaymentLoggingAspect {
    private final Logger logger;

    @Around("execution(* PaymentProcessor.processPayment(..))")
    public Object logPayment(ProceedingJoinPoint joinPoint) 
            throws Throwable {
        logger.info("Processing payment: " + 
            joinPoint.getArgs()[0]);

        Object result = joinPoint.proceed();

        logger.info("Payment processed");
        return result;
    }
}

7.2 Interface Segregation

// Bad: Fat interface
public interface DatabaseOperations {
    void connect();
    void disconnect();
    void beginTransaction();
    void commitTransaction();
    void rollbackTransaction();
    void executeQuery(String query);
    void executeBatch(List<String> queries);
    ResultSet getResults();
    void closeResults();
}

// Good: Segregated interfaces
public interface DatabaseConnection {
    void connect();
    void disconnect();
}

public interface TransactionManager {
    void beginTransaction();
    void commitTransaction();
    void rollbackTransaction();
}

public interface QueryExecutor {
    void executeQuery(String query);
    void executeBatch(List<String> queries);
}

public interface ResultSetHandler {
    ResultSet getResults();
    void closeResults();
}

public class DatabaseAdapter implements 
        DatabaseConnection, 
        QueryExecutor {
    private final Database database;

    @Override
    public void connect() {
        database.openConnection();
    }

    @Override
    public void disconnect() {
        database.closeConnection();
    }

    @Override
    public void executeQuery(String query) {
        database.execute(query);
    }

    @Override
    public void executeBatch(List<String> queries) {
        database.executeBatch(queries);
    }
}

8. Common Issues và Solutions

8.1 Performance Optimization

public class CachingDatabaseAdapter implements DatabaseAdapter {
    private final DatabaseAdapter delegate;
    private final Cache<String, Object> cache;

    public CachingDatabaseAdapter(
            DatabaseAdapter delegate,
            Cache<String, Object> cache) {
        this.delegate = delegate;
        this.cache = cache;
    }

    @Override
    public void executeQuery(String query) {
        Object cachedResult = cache.get(query);
        if (cachedResult != null) {
            return;
        }

        delegate.executeQuery(query);
        cache.put(query, result);
    }
}

@Configuration
public class CacheConfig {
    @Bean
    public Cache<String, Object> queryCache() {
        return Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(Duration.ofMinutes(5))
            .build();
    }
}

8.2 Error Handling

public class AdapterException extends RuntimeException {
    public AdapterException(String message) {
        super(message);
    }

    public AdapterException(String message, Throwable cause) {
        super(message, cause);
    }
}

public class ResilientDatabaseAdapter implements DatabaseAdapter {
    private final DatabaseAdapter delegate;
    private final int maxRetries;
    private final Duration retryDelay;

    @Override
    public void executeQuery(String query) {
        int attempts = 0;
        while (attempts < maxRetries) {
            try {
                delegate.executeQuery(query);
                return;
            } catch (Exception e) {
                attempts++;
                if (attempts == maxRetries) {
                    throw new AdapterException(
                        "Failed to execute query after " + 
                        maxRetries + " attempts", e);
                }
                try {
                    Thread.sleep(retryDelay.toMillis());
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new AdapterException(
                        "Retry interrupted", ie);
                }
            }
        }
    }
}

9. References

9.1 Books

  • Design Patterns: Elements of Reusable Object-Oriented Software
  • Head First Design Patterns
  • Clean Architecture by Robert C. Martin
  • Patterns of Enterprise Application Architecture

9.2 Online Resources