# 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
- Tăng tính tái sử dụng của code
- Tăng tính linh hoạt của code
- Tách biệt interface và implementation
- Dễ dàng thay đổi libraries bên thứ ba
- Đơn giản hóa testing
6.2 Nhược Điểm
- Tăng độ phức tạp của code
- Khó debug khi có nhiều adapters
- Có thể ảnh hưởng đến performance
- 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