google-site-verification: google61fe8ba583a51912.html
Adapter Pattern for Compatibility in Java: Bridging Incompatible Interfaces

Article

The Adapter Pattern is a structural design pattern that allows objects with incompatible interfaces to collaborate. It acts as a bridge between two incompatible interfaces, converting the interface of a class into another interface that clients expect. This pattern is particularly useful when integrating legacy code, third-party libraries, or systems with different interfaces.


Understanding the Adapter Pattern

When to Use the Adapter Pattern:

  • Legacy Integration: Integrating old systems with new interfaces
  • Third-party Libraries: Making incompatible libraries work together
  • Interface Standardization: Creating consistent interfaces across different implementations
  • Testing: Creating test adapters for external dependencies

Adapter Pattern Components:

  1. Target: The interface that clients expect
  2. Adaptee: The existing interface that needs adapting
  3. Adapter: The class that implements the Target interface and wraps the Adaptee

Adapter Pattern Implementations

1. Object Adapter (Composition-based)

Scenario: Integrating a legacy payment system with a modern payment interface

// Target Interface - Modern Payment System public interface ModernPayment { void processPayment(String customerId, double amount); boolean validatePayment(String customerId); String getPaymentStatus(String transactionId); } // Adaptee - Legacy Payment System (incompatible interface) public class LegacyPaymentSystem { public void makePayment(int customerCode, BigDecimal paymentAmount) { System.out.println("Processing legacy payment: Customer " + customerCode + ", Amount: " + paymentAmount); } public boolean checkCustomer(int customerCode) { System.out.println("Checking legacy customer: " + customerCode); return customerCode > 0; } public String getTransactionStatus(int transactionNumber) { return "COMPLETED"; // Simplified } // Legacy method that we don't want to expose public void legacyInternalMethod() { System.out.println("Internal legacy method"); } } // Adapter - Bridges ModernPayment and LegacyPaymentSystem public class LegacyPaymentAdapter implements ModernPayment { private final LegacyPaymentSystem legacyPaymentSystem; public LegacyPaymentAdapter(LegacyPaymentSystem legacyPaymentSystem) { this.legacyPaymentSystem = legacyPaymentSystem; } @Override public void processPayment(String customerId, double amount) { // Convert String customerId to int customerCode int customerCode = Integer.parseInt(customerId); // Convert double amount to BigDecimal BigDecimal paymentAmount = BigDecimal.valueOf(amount); legacyPaymentSystem.makePayment(customerCode, paymentAmount); } @Override public boolean validatePayment(String customerId) { int customerCode = Integer.parseInt(customerId); return legacyPaymentSystem.checkCustomer(customerCode); } @Override public String getPaymentStatus(String transactionId) { int transactionNumber = Integer.parseInt(transactionId); return legacyPaymentSystem.getTransactionStatus(transactionNumber); } // Additional adapter methods can provide enhanced functionality public void processPaymentWithRetry(String customerId, double amount, int retries) { for (int i = 0; i < retries; i++) { try { processPayment(customerId, amount); return; } catch (Exception e) { System.out.println("Payment failed, retry " + (i + 1)); if (i == retries - 1) throw e; } } } }

2. Class Adapter (Inheritance-based)

Scenario: Adapting different logging systems

// Target Interface public interface ModernLogger { void debug(String message); void info(String message); void error(String message, Throwable throwable); void warn(String message); } // Adaptee - Legacy Logger public class LegacyLogger { public void logDebug(String message) { System.out.println("[DEBUG] " + message); } public void logInfo(String message) { System.out.println("[INFO] " + message); } public void logError(String message) { System.out.println("[ERROR] " + message); } public void logWarning(String message) { System.out.println("[WARN] " + message); } } // Class Adapter (using inheritance) public class LegacyLoggerAdapter extends LegacyLogger implements ModernLogger { @Override public void debug(String message) { logDebug(message); } @Override public void info(String message) { logInfo(message); } @Override public void error(String message, Throwable throwable) { logError(message + (throwable != null ? ": " + throwable.getMessage() : "")); } @Override public void warn(String message) { logWarning(message); } // Enhanced functionality public void debug(String message, Object... args) { logDebug(String.format(message, args)); } }

Real-World Examples

1. File Format Adapter

Scenario: Adapting different file format readers to a unified interface

// Target Interface public interface FileReader { List<String> readLines(); String readAll(); Map<String, Object> getMetadata(); boolean supports(String fileExtension); } // Adaptee 1 - CSV Reader public class CsvReader { private final String filePath; public CsvReader(String filePath) { this.filePath = filePath; } public List<String[]> readCsvData() throws IOException { List<String[]> data = new ArrayList<>(); try (BufferedReader br = new BufferedReader(new FileReader(filePath))) { String line; while ((line = br.readLine()) != null) { data.add(line.split(",")); } } return data; } public boolean isCsvFile() { return filePath.toLowerCase().endsWith(".csv"); } } // Adaptee 2 - JSON Reader public class JsonReader { private final String filePath; public JsonReader(String filePath) { this.filePath = filePath; } public JsonNode parseJson() throws IOException { ObjectMapper mapper = new ObjectMapper(); return mapper.readTree(new File(filePath)); } public boolean isJsonFile() { return filePath.toLowerCase().endsWith(".json"); } } // CSV Adapter public class CsvFileAdapter implements FileReader { private final CsvReader csvReader; public CsvFileAdapter(String filePath) { this.csvReader = new CsvReader(filePath); } @Override public List<String> readLines() { try { List<String[]> csvData = csvReader.readCsvData(); return csvData.stream() .map(row -> String.join(" | ", row)) .collect(Collectors.toList()); } catch (IOException e) { throw new RuntimeException("Failed to read CSV file", e); } } @Override public String readAll() { List<String> lines = readLines(); return String.join("\n", lines); } @Override public Map<String, Object> getMetadata() { Map<String, Object> metadata = new HashMap<>(); metadata.put("format", "CSV"); metadata.put("supports", "Tabular data"); return metadata; } @Override public boolean supports(String fileExtension) { return "csv".equalsIgnoreCase(fileExtension); } } // JSON Adapter public class JsonFileAdapter implements FileReader { private final JsonReader jsonReader; public JsonFileAdapter(String filePath) { this.jsonReader = new JsonReader(filePath); } @Override public List<String> readLines() { try { JsonNode root = jsonReader.parseJson(); List<String> lines = new ArrayList<>(); root.fieldNames().forEachRemaining(fieldName -> { lines.add(fieldName + ": " + root.get(fieldName).asText()); }); return lines; } catch (IOException e) { throw new RuntimeException("Failed to read JSON file", e); } } @Override public String readAll() { try { return jsonReader.parseJson().toPrettyString(); } catch (IOException e) { throw new RuntimeException("Failed to read JSON file", e); } } @Override public Map<String, Object> getMetadata() { Map<String, Object> metadata = new HashMap<>(); metadata.put("format", "JSON"); metadata.put("supports", "Structured data"); return metadata; } @Override public boolean supports(String fileExtension) { return "json".equalsIgnoreCase(fileExtension); } }

2. Database Adapter

Scenario: Adapting different database clients to a unified repository interface

// Target Interface public interface UserRepository { void save(User user); User findById(String id); List<User> findAll(); void delete(String id); } // Adaptee 1 - MongoDB Client public class MongoUserClient { private final MongoCollection<Document> collection; public MongoUserClient(MongoDatabase database) { this.collection = database.getCollection("users"); } public void insertUser(Document userDoc) { collection.insertOne(userDoc); } public Document findUserById(String objectId) { return collection.find(new Document("_id", new ObjectId(objectId))).first(); } public List<Document> findAllUsers() { return collection.find().into(new ArrayList<>()); } public void removeUser(String objectId) { collection.deleteOne(new Document("_id", new ObjectId(objectId))); } } // Adaptee 2 - SQL Database Client public class SqlUserClient { private final Connection connection; public SqlUserClient(Connection connection) { this.connection = connection; } public void createUser(String sql, Object... params) throws SQLException { try (PreparedStatement stmt = connection.prepareStatement(sql)) { for (int i = 0; i < params.length; i++) { stmt.setObject(i + 1, params[i]); } stmt.executeUpdate(); } } public ResultSet queryUser(String sql, Object... params) throws SQLException { PreparedStatement stmt = connection.prepareStatement(sql); for (int i = 0; i < params.length; i++) { stmt.setObject(i + 1, params[i]); } return stmt.executeQuery(); } public void deleteUser(String sql, Object... params) throws SQLException { try (PreparedStatement stmt = connection.prepareStatement(sql)) { for (int i = 0; i < params.length; i++) { stmt.setObject(i + 1, params[i]); } stmt.executeUpdate(); } } } // MongoDB Adapter public class MongoUserRepository implements UserRepository { private final MongoUserClient mongoClient; private final ObjectMapper objectMapper; public MongoUserRepository(MongoUserClient mongoClient) { this.mongoClient = mongoClient; this.objectMapper = new ObjectMapper(); } @Override public void save(User user) { try { String json = objectMapper.writeValueAsString(user); Document doc = Document.parse(json); mongoClient.insertUser(doc); } catch (Exception e) { throw new RuntimeException("Failed to save user to MongoDB", e); } } @Override public User findById(String id) { try { Document doc = mongoClient.findUserById(id); if (doc != null) { return objectMapper.readValue(doc.toJson(), User.class); } return null; } catch (Exception e) { throw new RuntimeException("Failed to find user in MongoDB", e); } } @Override public List<User> findAll() { try { return mongoClient.findAllUsers().stream() .map(doc -> { try { return objectMapper.readValue(doc.toJson(), User.class); } catch (Exception e) { throw new RuntimeException(e); } }) .collect(Collectors.toList()); } catch (Exception e) { throw new RuntimeException("Failed to find all users in MongoDB", e); } } @Override public void delete(String id) { mongoClient.removeUser(id); } } // SQL Adapter public class SqlUserRepository implements UserRepository { private final SqlUserClient sqlClient; public SqlUserRepository(SqlUserClient sqlClient) { this.sqlClient = sqlClient; } @Override public void save(User user) { try { String sql = "INSERT INTO users (id, name, email) VALUES (?, ?, ?)"; sqlClient.createUser(sql, user.getId(), user.getName(), user.getEmail()); } catch (SQLException e) { throw new RuntimeException("Failed to save user to SQL database", e); } } @Override public User findById(String id) { try { String sql = "SELECT * FROM users WHERE id = ?"; ResultSet rs = sqlClient.queryUser(sql, id); if (rs.next()) { return new User( rs.getString("id"), rs.getString("name"), rs.getString("email") ); } return null; } catch (SQLException e) { throw new RuntimeException("Failed to find user in SQL database", e); } } @Override public List<User> findAll() { try { String sql = "SELECT * FROM users"; ResultSet rs = sqlClient.queryUser(sql); List<User> users = new ArrayList<>(); while (rs.next()) { users.add(new User( rs.getString("id"), rs.getString("name"), rs.getString("email") )); } return users; } catch (SQLException e) { throw new RuntimeException("Failed to find all users in SQL database", e); } } @Override public void delete(String id) { try { String sql = "DELETE FROM users WHERE id = ?"; sqlClient.deleteUser(sql, id); } catch (SQLException e) { throw new RuntimeException("Failed to delete user from SQL database", e); } } }

Advanced Adapter Patterns

1. Two-Way Adapter

Scenario: When you need bidirectional compatibility between two systems

// Interface A public interface NewSystem { void newProcess(String data); String newAnalysis(); } // Interface B public interface OldSystem { void legacyProcess(int code, String information); String legacyReport(); } // Two-Way Adapter public class BidirectionalAdapter implements NewSystem, OldSystem { private final NewSystem newSystem; private final OldSystem oldSystem; public BidirectionalAdapter(NewSystem newSystem, OldSystem oldSystem) { this.newSystem = newSystem; this.oldSystem = oldSystem; } // NewSystem implementation using OldSystem @Override public void newProcess(String data) { // Convert new format to old format int code = data.hashCode(); oldSystem.legacyProcess(code, data); } @Override public String newAnalysis() { String legacyReport = oldSystem.legacyReport(); // Convert old format to new format return "Analysis: " + legacyReport; } // OldSystem implementation using NewSystem @Override public void legacyProcess(int code, String information) { // Convert old format to new format String combinedData = code + ":" + information; newSystem.newProcess(combinedData); } @Override public String legacyReport() { String newAnalysis = newSystem.newAnalysis(); // Convert new format to old format return "Legacy: " + newAnalysis; } }

2. Pluggable Adapters

Scenario: Creating flexible adapters that can work with multiple adaptees

// Generic Target Interface public interface NotificationSender { void send(String recipient, String message); boolean supports(String channelType); } // Pluggable Adapter public class UniversalNotificationAdapter implements NotificationSender { private final Map<String, NotificationService> services; public UniversalNotificationAdapter() { this.services = new HashMap<>(); } public void registerService(String channelType, NotificationService service) { services.put(channelType, service); } @Override public void send(String recipient, String message) { // Determine channel type from recipient String channelType = determineChannelType(recipient); NotificationService service = services.get(channelType); if (service != null) { service.dispatch(recipient, message); } else { throw new IllegalArgumentException("Unsupported channel type: " + channelType); } } @Override public boolean supports(String channelType) { return services.containsKey(channelType); } private String determineChannelType(String recipient) { if (recipient.contains("@")) return "email"; if (recipient.matches("\\+?[\\d\\s-]+")) return "sms"; if (recipient.startsWith("@")) return "slack"; return "unknown"; } } // Adaptee Interface public interface NotificationService { void dispatch(String target, String content); } // Concrete Adaptees public class EmailService implements NotificationService { @Override public void dispatch(String target, String content) { System.out.println("Sending email to: " + target + " - " + content); } } public class SmsService implements NotificationService { @Override public void dispatch(String target, String content) { System.out.println("Sending SMS to: " + target + " - " + content); } } public class SlackService implements NotificationService { @Override public void dispatch(String target, String content) { System.out.println("Sending Slack message to: " + target + " - " + content); } }

Testing Adapters

Adapter Testing Strategy

// Test for Payment Adapter public class LegacyPaymentAdapterTest { @Test public void testProcessPayment() { // Given LegacyPaymentSystem legacySystem = mock(LegacyPaymentSystem.class); LegacyPaymentAdapter adapter = new LegacyPaymentAdapter(legacySystem); // When adapter.processPayment("123", 100.50); // Then verify(legacySystem).makePayment(123, BigDecimal.valueOf(100.50)); } @Test public void testValidatePayment() { // Given LegacyPaymentSystem legacySystem = mock(LegacyPaymentSystem.class); when(legacySystem.checkCustomer(456)).thenReturn(true); LegacyPaymentAdapter adapter = new LegacyPaymentAdapter(legacySystem); // When boolean isValid = adapter.validatePayment("456"); // Then assertTrue(isValid); verify(legacySystem).checkCustomer(456); } } // Mock for testing public class MockLegacyPaymentSystem extends LegacyPaymentSystem { private List<String> operations = new ArrayList<>(); @Override public void makePayment(int customerCode, BigDecimal paymentAmount) { operations.add("makePayment:" + customerCode + ":" + paymentAmount); } @Override public boolean checkCustomer(int customerCode) { operations.add("checkCustomer:" + customerCode); return customerCode > 0; } public List<String> getOperations() { return operations; } }

Best Practices

  1. Prefer Object Adapters: Use composition over inheritance for more flexibility
  2. Single Responsibility: Each adapter should handle one specific adaptation
  3. Error Handling: Provide meaningful error messages when adaptation fails
  4. Document Assumptions: Clearly document any assumptions made during adaptation
  5. Test Thoroughly: Adapters can hide compatibility issues
  6. Consider Performance: Be aware of any performance overhead from adaptation
  7. Use Factory Methods: Create adapters through factory methods for better control

Conclusion

The Adapter Pattern is a powerful tool for achieving compatibility between systems with incompatible interfaces. Whether you're integrating legacy systems, working with third-party libraries, or creating unified interfaces across different implementations, adapters provide a clean, maintainable solution. By understanding the different types of adapters (object vs. class) and applying best practices, you can effectively bridge interface gaps while keeping your codebase clean and extensible. The pattern's true power lies in its ability to enable communication between systems that weren't designed to work together, making it an essential tool in any Java developer's arsenal.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper