Adapter. This article provides an overview of the Adapter Design Pattern and demonstrates its practical use in Java.
1. Overview of the Adapter Pattern
1.1. Description
The Adapter Pattern is a well-established concept in software development and is implemented in many programming languages, including Java.
The Adapter Pattern enables the conversion of one object’s interface into another interface expected by the client. Essentially, this pattern adapts one object to another, allowing otherwise incompatible objects to collaborate.
Using an adapter allows you to reuse existing code without modifying it, as the adapter serves as a bridge between different interfaces, ensuring compatibility.
In contrast to the Decorator Pattern, which adds new functionality to an existing object without altering its interface, the Adapter Pattern focuses solely on converting one interface to another, leaving functionality unchanged.
1.2. Example of an Adapter in Java
Let’s implement a real-world adapter, such as a power adapter. Different countries often have varying electrical sockets and plug types. To make electrical devices compatible with different sockets, adapters are necessary.
1.2.1. The model for getting electricity
First, we define the electrical socket and plug connector interfaces for Germany and the UK:
public class GermanElectricalSocket {
public void plugIn(GermanPlugConnector plug) {
plug.giveElectricity();
}
}
public interface GermanPlugConnector {
public void giveElectricity();
}
public class UKElectricalSocket {
public void plugIn(UKPlugConnector plug) {
plug.provideElectricity();
}
}
public interface UKPlugConnector {
public void provideElectricity();
}
These classes define that only UKPlugConnectors
can be plugged into a UKElectricalSocket
, and only GermanPlugConnectors
can be plugged into a GermanElectricalSocket
.
1.2.2. Creating an adapter for plug connectors
Fortunately, a UKElectricalSocket
can still be used with a GermanPlugConnector
by utilizing an adapter. The adapter wraps a GermanPlugConnector
to make it compatible with a UKElectricalSocket
.
To do this, we create a new class that implements the UKPlugConnector
interface and wraps a GermanPlugConnector
:
public class GermanToUKPlugAdapter implements UKPlugConnector {
private GermanPlugConnector germanPlug;
public GermanToUKPlugAdapter(GermanPlugConnector germanPlug) {
this.germanPlug = germanPlug;
}
@Override
public void provideElectricity() {
germanPlug.giveElectricity();
}
}
1.2.3. Using the adapter
Now, the adapter can be used to plug a GermanPlugConnector
into a UKElectricalSocket
:
GermanPlugConnector germanPlug = //... create a GermanPlugConnector instance
UKElectricalSocket ukSocket = new UKElectricalSocket();
UKPlugConnector adapter = new GermanToUKPlugAdapter(germanPlug);
ukSocket.plugIn(adapter);
With this adapter in place, the GermanPlugConnector
can now be used with a UKElectricalSocket
.
1.2.4. Conclusion
The Adapter Pattern allows two incompatible interfaces to work together without modifying the existing code. In this example, we adapted a German plug to fit into a UK socket by using an adapter class. This demonstrates the flexibility of the Adapter Pattern in integrating legacy or third-party code with new systems.
1.3. Types of Adapters
Adapters can be classified into two main categories:
1.3.1. Object Adapter
In an Object Adapter, the adapter contains an instance of the class it is adapting. This is known as composition. The adapter forwards requests to the adaptee object (the existing class being adapted) while keeping the interfaces separate. This type of adapter is commonly used because of its flexibility—an adapter can adapt multiple adaptees.
public class GermanToUKPlugAdapter implements UKPlugConnector {
private GermanPlugConnector germanPlug;
public GermanToUKPlugAdapter(GermanPlugConnector germanPlug) {
this.germanPlug = germanPlug;
}
@Override
public void provideElectricity() {
germanPlug.giveElectricity();
}
}
In this example, the adapter (GermanToUKPlugAdapter
) contains a reference to a GermanPlugConnector
and forwards the call to its giveElectricity()
method, effectively adapting it to the UKPlugConnector
interface.
1.3.2. Class Adapter
In a Class Adapter, the adapter inherits from both the target interface and the adaptee. This requires multiple inheritance, which is not supported in Java directly. However, in languages that support multiple inheritance, a class adapter directly inherits and overrides the methods from both interfaces, adapting the behavior within a single class.
Since Java doesn’t support multiple inheritance, class adapters aren’t commonly used in Java. The Object Adapter approach is generally preferred.
1.4. Usage of the Adapter Pattern
The Adapter Pattern is particularly useful in the following scenarios:
-
Integration with Legacy Code: When you need to integrate new code with legacy systems that have incompatible interfaces.
-
Third-party Libraries: Adapters are useful when dealing with third-party libraries that do not offer the same interfaces as the rest of your system. An adapter allows you to wrap these external libraries and fit them into your codebase without changing the external library.
-
Modularization and Code Reusability: The Adapter Pattern encourages modularity by allowing the reuse of existing functionality without modifying its implementation. Instead of rewriting classes to fit into a new system, adapters help plug them in easily.
For example, imagine you have a payment processing system. Over time, you may need to integrate multiple payment gateways, each of which might provide different APIs. By using adapters, you can standardize these APIs in your system, allowing you to switch between different payment providers easily without needing to rewrite the core business logic.
1.4.1. Real-world Example: Payment Gateway Integration
public interface PaymentGateway {
void processPayment(double amount);
}
public class PaypalPaymentGateway {
public void sendPayment(double amount) {
System.out.println("Processing payment via PayPal: " + amount);
}
}
public class StripePaymentGateway {
public void makePayment(double amount) {
System.out.println("Processing payment via Stripe: " + amount);
}
}
To unify the interfaces of PayPal and Stripe, we use adapters:
public class PaypalAdapter implements PaymentGateway {
private final PaypalPaymentGateway paypal;
public PaypalAdapter(PaypalPaymentGateway paypal) {
this.paypal = paypal;
}
@Override
public void processPayment(double amount) {
paypal.sendPayment(amount);
}
}
public class StripeAdapter implements PaymentGateway {
private final StripePaymentGateway stripe;
public StripeAdapter(StripePaymentGateway stripe) {
this.stripe = stripe;
}
@Override
public void processPayment(double amount) {
stripe.makePayment(amount);
}
}
Now, you can easily switch between different payment providers by using their respective adapters without changing the core business logic:
public class PaymentService {
public void processOrder(PaymentGateway paymentGateway, double amount) {
paymentGateway.processPayment(amount);
}
public static void main(String[] args) {
PaymentService service = new PaymentService();
PaymentGateway paypalAdapter = new PaypalAdapter(new PaypalPaymentGateway());
service.processOrder(paypalAdapter, 100.0);
PaymentGateway stripeAdapter = new StripeAdapter(new StripePaymentGateway());
service.processOrder(stripeAdapter, 150.0);
}
}
1.5. Evaluation of the Adapter Pattern
The Adapter Pattern is one of the most useful and widely used structural design patterns. However, like all patterns, it has its benefits and potential drawbacks:
1.5.1. Pros
-
Increased Reusability: By using adapters, you can reuse existing classes that have incompatible interfaces, saving time and effort by not rewriting existing functionality.
-
Separation of Concerns: Adapters enable cleaner architecture, as they separate the adaptee’s functionality from the interface expected by the client.
-
Encapsulation of Legacy Code: If you have legacy code that you don’t want to modify, the adapter pattern allows you to work with that code without altering its source.
1.5.2. Cons
-
Overuse Can Lead to Code Complexity: Introducing too many adapters into your system can make the codebase harder to follow. Over-reliance on adapters can obscure the original system design, making it difficult to understand.
-
Performance Overhead: While generally minimal, there may be a slight performance hit due to the extra layer of abstraction introduced by the adapter.
-
Limited Flexibility for Class Adapters: In Java, since multiple inheritance is not supported, class adapters are not feasible, limiting the flexibility of adapter implementations. Object adapters are the most common approach.
1.6. Best Practices for Using the Adapter Pattern
-
Use Adapters When Necessary: Adapters are best suited for cases where existing systems cannot be easily modified or when integrating external systems that you do not control.
-
Limit the Number of Adapters: While adapters are powerful, using too many adapters can complicate the system, so it’s important to strike a balance.
-
Favor Composition Over Inheritance: In languages like Java, where multiple inheritance is not available, favor the Object Adapter approach, which uses composition rather than inheritance.
1.7. Alternatives to the Adapter Pattern
While the Adapter Pattern is effective in many situations, some alternatives may better suit specific cases:
-
Facade Pattern: If you’re looking to simplify an interface to a complex subsystem rather than converting one interface to another, consider using the Facade Pattern, which hides the complexity of the subsystem behind a simpler interface.
-
Decorator Pattern: If you want to add functionality to an object dynamically without changing its interface, consider using the Decorator Pattern, which allows for extending an object’s behavior without altering the interface.
2. Links and Literature
2.1. vogella Java example code
If you need more assistance we offer Online Training and Onsite training as well as consulting