1. The Problem
Problem: Incompatible Interfaces Need to Work Together
Software often needs to integrate external systems, legacy components, or third-party libraries whose APIs do not match your system’s expectations.
Example: Your app expects an object with send(data) but a library exposes publish(payload).
You end up writing procedural glue everywhere:
if service == "legacy":
legacy.publish(data)
else:
modern.send(data)
This leads to:
- Tight coupling to implementation details
- Scattered integration logic
- Repeated conversion code
- Fragile maintenance
- Violations of Single Responsibility and Open/Closed Principles
You need a way to make incompatible interfaces compatible without modifying them.
2. The Adapter Pattern: Make One Interface Look Like Another
The Adapter Pattern allows objects with incompatible interfaces to collaborate.
Core idea:
- Take an existing object
- Wrap it in an adapter
- Present the interface your code expects
- Delegate calls by translating requests
A system using Adapter has:
- Target Interface — the interface your system expects
- Adaptee — the existing/foreign component
- Adapter — a wrapper translating Target calls into Adaptee calls
You get:
- Zero changes to client code
- Seamless compatibility
- Cleaner integrations
- Encapsulated conversion logic
Use Adapter when:
- You integrate legacy or 3rd-party code
- You must keep client code stable
- APIs mismatch but semantics align
- You migrate systems incrementally
- You want to avoid rewriting existing components
3. Implementation: Adapter in Python
Assume your application expects a Notifier with send(message).
Target interface:
from abc import ABC, abstractmethod
class Notifier(ABC):
@abstractmethod
def send(self, message: str):
pass
Your modern service already matches it:
class EmailNotifier(Notifier):
def send(self, message: str):
return f"Email sent: {message}"
But a legacy library exposes a completely different API:
class LegacyAlertSystem:
def publish_alert(self, payload: str):
return f"Legacy alert published: {payload}"
Your system expects send(), but the legacy system uses publish_alert().
Adapter wraps the legacy system and adapts the API:
class LegacyAlertAdapter(Notifier):
def __init__(self, legacy_system: LegacyAlertSystem):
self.legacy = legacy_system
def send(self, message: str):
return self.legacy.publish_alert(message)
Client code works without knowing anything changed:
def notify_user(notifier: Notifier, message: str):
print(notifier.send(message))
notify_user(EmailNotifier(), "Welcome!")
notify_user(LegacyAlertAdapter(LegacyAlertSystem()), "System Down!")
Output conceptually behaves the same from the client’s perspective.
- No branching
- No client changes
- No rewriting legacy code
- Clean separation of compatibility logic
4. When to Use Adapter (and When Not To)
You likely need Adapter when you notice:
- “Old system exposes API A, my code needs API B.”
- “I cannot modify the existing component.”
- “I want my business logic to depend on stable interfaces.”
- “Integration code is duplicated everywhere.”
- “Migration must be incremental.”
Common pitfalls:
- Using Adapter to hide poor design choices instead of fixing them
- Creating deep chains of adapters → complexity and performance issues
- Stuffing business logic into the adapter instead of only mapping interfaces
- Mixing multiple responsibilities into the adapter