Overview
The Adapter Pattern serves as a bridge between incompatible interfaces. This structural design pattern enables objects with incompatible interfaces to collaborate by wrapping its own interface around an existing class. Think of it as a electrical transformer that converts voltage from power grids to what your device requires—in software terms, it converts one interface into another that clients expect.
Also known as the Wrapper pattern, the Adapter Pattern is particularly valuable when you want to integrate legacy components into modern systems without rewriting existing code. Instead of modifying the original class, you create an adapter that translates calls between the new interface and the old implementation.
Pattern Structure
The adapter pattern involves three key participants:
- Target: Defines the domain-specific enterface that clients use
- Adaptee: The existing interface that needs adapting
- Adapter: Wraps the adaptee and implements the Target interface
There are two implementation approaches: class adapters using inheritance, and object adapters using composition. The object adapter approach is generally preferred as it allows a single adapter to work with multiple adaptees.
Implementation Example
Below is a practical demonstration showing how to adapt a legacy payment processor to work with a modern payment gateway interface:
package com.example.patterns.adapter;
// Legacy payment processor with existing incompatible interface
public class LegacyPaymentProcessor {
public void processTransaction(String transactionData, double amount) {
System.out.println("Processing legacy transaction: " + transactionData);
System.out.println("Amount: " + amount);
}
public String generateReceipt(String transactionId) {
return "LEGACY-" + transactionId;
}
}
// Modern payment gateway interface that our system expects
public interface PaymentGateway {
void executePayment(PaymentRequest request);
PaymentResponse confirmTransaction(String reference);
}
// Adapter implementation
public class PaymentGatewayAdapter implements PaymentGateway {
private final LegacyPaymentProcessor legacyProcessor;
public PaymentGatewayAdapter() {
this.legacyProcessor = new LegacyPaymentProcessor();
}
@Override
public void executePayment(PaymentRequest request) {
String transactionData = buildTransactionData(request);
legacyProcessor.processTransaction(transactionData, request.getAmount());
}
@Override
public PaymentResponse confirmTransaction(String reference) {
String receipt = legacyProcessor.generateReceipt(reference);
return new PaymentResponse(reference, receipt, PaymentStatus.COMPLETED);
}
private String buildTransactionData(PaymentRequest request) {
return request.getCustomerId() + ":" + request.getCurrency();
}
}
This implementation demonstrates the object adapter approach, where the adapter holds a reference to the legacy processor and translates method calls between the modern interface and the existing implementation.
Spring Framework Application: HandlerAdapter
Spring MVC extensively uses the Adapter Pattern through its HandlerAdapter interface. This allows Spring to support diverse controller types through a unified handling mechanism.
The core HandlerAdapter contract requires implementations to process requests through three primary methods: supports() to indicate which handler types can be handled, handle() to process the actual request, and getLastModified() for caching support.
Spring provides several built-in Adapter implementations:
package org.springframework.web.servlet;
public class SimpleControllerHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return Controller.class.isInstance(handler);
}
@Override
public ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
return ((Controller) handler).handleRequest(request, response);
}
@Override
public long getLastModified(HttpServletRequest request, Object handler) {
return ((SimpleControllerHandlerAdapter) handler).getLastModified(request);
}
}
Additional adapters exist for different handler types: HttpRequestHandlerAdapter hendles beans implementing HttpRequestHandler, and HandlerFunctionAdapter inetgrates functional web handlers in reactive programming scenarios.
Configuration Example
When customizing Spring MVC configuration, you can explicitly register handler adapters:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureHandlerAdapters(HandlerAdapterRegistry registry) {
registry.addAdapterFunction(new CustomHandlerAdapter());
}
}
@Component
public class CustomHandlerAdapter implements HandlerFunctionAdapter {
@Override
public boolean supports(Function<?> handler) {
return CustomEndpoint.class.isInstance(handler);
}
@Override
public Mono<ServerResponse> handle(ServerRequest request, Object handler) {
return ((CustomEndpoint) handler).process(request);
}
}
Benefits and Considerations
The Adapter Pattern provides significant advantages: it promotes loose coupling between components, facilitates testing through interface mocking, and enables graceful evolution of systems. However, overusing adapters can lead to unnecessary layers of abstraction. Best practice suggests using adapters when integrating external systems or maintaining backward compatibility with legacy code.