You’ve built a perfectly good system, but now you need to integrate a new, third-party component. The problem? The new component speaks a completely different language—its methods, parameters, and return types are totally incompatible with your existing code. Do you rewrite your entire system? No! You use the Adapter Pattern.
The Adapter is a structural design pattern that acts as a translator, allowing objects with incompatible interfaces to work together seamlessly.
Think of a universal travel adapter. Your laptop charger has a two-prong plug (your existing system), but the country you’re visiting has three-prong wall outlets (the new service). The adapter sits in the middle, making the connection possible without you having to change your charger or the wall outlet.
The Problem in Code: Data Transformation
Imagine you have a data provider that gives you data in XML format, but your application’s analytics library only understands JSON. The interfaces are incompatible.
An adapter can be created to sit between them, converting the XML data into JSON on the fly.
graph TD;
A[Data Provider <br/>(Returns XML)] --> B{XML-to-JSON Adapter};
B --> C[Analytics Library <br/>(Expects JSON)];
subgraph Your Application
B
C
end
This adapter makes the Analytics Library believe it’s communicating with a native JSON source, completely unaware of the XML-to-JSON translation happening behind the scenes.
A Real-World Example: Payment Gateways
Let’s build a more concrete example. We have a CheckoutService in our e-commerce application. Initially, it was built to only support PayPal for payments. The entire system is built around PayPal’s API, which takes an amount and currency, and returns a simple boolean (True for success, False for failure).
Now, the business wants to add Stripe as a new payment option. Here’s the catch: Stripe’s API is different.
- It requires an additional
descriptionfield in the request. - Its success response isn’t a boolean; it’s a dictionary like
{'status': 'approved'}.
Our mobile app and other third-party clients are already using our system. They all expect a boolean response. We can’t force them all to update their code just to accommodate Stripe’s unique response format. This is where the Adapter pattern shines.
Here is the class structure we want to achieve.
classDiagram
class Client {
+checkout(processor)
}
class PaymentProcessor {
<<interface>>
+pay(amount, currency) bool
}
class PayPalProcessor {
+pay(amount, currency) bool
}
class StripeService {
<<adaptee>>
-process_payment(charge_details) dict
}
class StripeAdapter {
-stripe_service: StripeService
+pay(amount, currency) bool
}
Client --> PaymentProcessor
PaymentProcessor <|-- PayPalProcessor
PaymentProcessor <|-- StripeAdapter
StripeAdapter o-- StripeService
- Target (
PaymentProcessor): The interface our client code depends on. - Adaptee (
StripeService): The incompatible class we need to integrate. - Adapter (
StripeAdapter): The class that bridges the gap between the Target and the Adaptee. - Client: The code that uses the payment system.
Project Structure
To keep our code organized, we’ll structure our project as follows. The adapters for new services will live in their own dedicated directory.
payment_project/
├── main.py
├── checkout.py
├── processors/
│ ├── __init__.py
│ ├── base.py
│ └── paypal_processor.py
└── adapters/
├── __init__.py
└── stripe_adapter.py
Step 1: The Initial System (PayPal-Only)
First, let’s define our Target interface using Python’s abc module. This is the contract all payment processors must follow.
processors/base.py
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
"""
The Target Interface that our client code uses.
"""
@abstractmethod
def pay(self, amount: float, currency: str = "USD") -> bool:
pass
Next, our original PayPalProcessor implements this interface.
processors/paypal_processor.py
from .base import PaymentProcessor
import time
class PayPalProcessor(PaymentProcessor):
"""
A Concrete Implementation for the PayPal service.
"""
def pay(self, amount: float, currency: str = "USD") -> bool:
print(f"Processing ${amount} payment via PayPal...")
# Simulate API call to PayPal
time.sleep(1)
print("PayPal payment successful.")
return True
Finally, our CheckoutService uses the PaymentProcessor abstraction, not a concrete implementation. This is a key principle (Dependency Inversion) that makes our system flexible.
checkout.py
from processors.base import PaymentProcessor
class CheckoutService:
def __init__(self, processor: PaymentProcessor):
self._processor = processor
def execute_payment(self, amount: float, currency: str = "USD"):
print("--- Checkout Service ---")
if self._processor.pay(amount, currency):
print("Payment Succeeded.")
else:
print("Payment Failed.")
print("------------------------")
Step 2: The Incompatible Service (Stripe)
Now, let’s define the “incompatible” Stripe service. Notice its process_payment method has a different signature and return type. We’ll simulate it in a file that our adapter will use.
adapters/stripe_service.py (Simulated Third-Party Library)
import time
class StripeService:
"""
The Adaptee: An incompatible third-party service.
It has a different method name, parameters, and return type.
"""
def process_payment(self, charge_details: dict):
print(f"Processing payment for {charge_details['amount']} via Stripe...")
# Simulate API call to Stripe
time.sleep(1)
if charge_details.get("amount", 0) > 0:
return {"status": "approved", "transaction_id": "xyz-123"}
else:
return {"status": "declined", "reason": "invalid_amount"}
[!NOTE] The
StripeServiceis our Adaptee. This is the class with the incompatible interface that we want to adapt to our system. We cannot (or should not) modify its source code.
Step 3: Building the Stripe Adapter
Here comes the magic. We create StripeAdapter, which inherits from our PaymentProcessor (the Target) but internally wraps and uses the StripeService (the Adaptee).
adapters/stripe_adapter.py
from processors.base import PaymentProcessor
from .stripe_service import StripeService # The incompatible service
class StripeAdapter(PaymentProcessor):
"""
The Adapter makes the StripeService's interface compatible with
the PaymentProcessor's interface.
"""
def __init__(self):
self._stripe_service = StripeService()
def pay(self, amount: float, currency: str = "USD") -> bool:
# 1. Translate the request from our system's format to Stripe's format
print("--> Stripe Adapter: Translating request...")
stripe_request = {
"amount": amount,
"currency": currency,
"description": "Checkout from Adapter Demo"
}
# 2. Call the adaptee's method
response = self._stripe_service.process_payment(stripe_request)
# 3. Translate the response from Stripe's format to our system's format (bool)
print("--> Stripe Adapter: Translating response...")
if response.get("status") == "approved":
return True
return False
The adapter performs a two-way translation:
- Request Translation: It converts the simple
(amount, currency)call into the dictionary thatStripeServiceexpects. - Response Translation: It converts the
{'status': 'approved'}dictionary from Stripe into the simpleTrue/Falseboolean that ourCheckoutServiceexpects.
Step 4: Updating the Client Code
Thanks to our adapter, the changes required in the main application logic are minimal. We just need to add logic to select the correct processor. The CheckoutService itself remains completely unchanged!
main.py
from checkout import CheckoutService
from processors.paypal_processor import PayPalProcessor
+ from adapters.stripe_adapter import StripeAdapter
def main():
gateway = input("Choose payment gateway (paypal or stripe): ").lower()
amount = float(input("Enter amount to pay: "))
processor = None
if gateway == "paypal":
processor = PayPalProcessor()
+ elif gateway == "stripe":
+ processor = StripeAdapter()
else:
print("Invalid gateway selected.")
return
if processor:
checkout_service = CheckoutService(processor)
checkout_service.execute_payment(amount)
if __name__ == "__main__":
main()
Now, when we run the application, we can seamlessly switch between PayPal and Stripe, and our CheckoutService is none the wiser. It continues to work with the PaymentProcessor interface, just as before.
[!TIP] Code Against Abstractions, Not Concretions. The power of this pattern comes from the
CheckoutServicedepending on thePaymentProcessorabstract class, not the concretePayPalProcessor. This allows us to swap in any new processor (like ourStripeAdapter) that adheres to the same interface without modifying the service.
Deep Dive: Object vs. Class Adapters
When to Use the Adapter Pattern
- When you need to use an existing class, but its interface is not compatible with the rest of your code.
- When you want to create a reusable class that cooperates with unrelated or unforeseen classes that don’t necessarily have compatible interfaces.
- When you need to integrate a third-party library or legacy component without changing its source code.
Pros and Cons
| Pros | Cons |
|---|---|
| Single Responsibility Principle: You separate the integration logic from your core business logic. | Increased Complexity: You introduce a new layer of objects that have to be managed. |
| Open/Closed Principle: You can introduce new adapters without modifying the existing client code. | Potential for Over-engineering: For very simple translations, a direct function call might be easier. |
| Improved Reusability: Adapters can be reused to integrate the same service in different parts of an application. | “Adapter Hell”: In complex systems, you can end up with chains of adapters adapting other adapters. |
Final Summary
The Adapter pattern is an essential tool for building flexible and maintainable systems. It allows you to integrate new and legacy components gracefully, acting as the glue that holds incompatible parts together.
mindmap
root((Adapter Pattern))
Definition
:A structural pattern that allows incompatible interfaces to work together.
Analogy
:A universal travel adapter.
Participants
Target
:The interface the client expects.
Adaptee
:The incompatible service/class.
Adapter
:The wrapper that translates between Target and Adaptee.
Client
:The code that uses the Target interface.
Benefits
Single Responsibility
Open/Closed Principle
Integrates Legacy/3rd-Party Code
Implementation (Python)
Use Composition (Object Adapter)
Inherit from an Abstract Base Class (Target)
Wrap an instance of the Adaptee
🧠 Quiz: Test Your Knowledge!
- In our example, which class is the "Adaptee"?
- Why is it better for `CheckoutService` to depend on `PaymentProcessor` instead of `PayPalProcessor` directly?
- What is the primary responsibility of the `StripeAdapter`?
View Answers
- The `StripeService` class is the Adaptee. It's the class with the incompatible interface we want to use.
- By depending on the `PaymentProcessor` abstraction (an ABC), the `CheckoutService` is decoupled from any specific implementation. This allows us to introduce new payment methods (like Stripe via its adapter) without changing the `CheckoutService`'s code, adhering to the Open/Closed Principle.
- The `StripeAdapter`'s primary responsibility is to perform a two-way translation: it translates method calls from the client's expected interface (`PaymentProcessor`) into calls the `StripeService` can understand, and then translates the `StripeService`'s response back into the format the client expects.