-
Adapter Pattern
-
The Adapter Pattern is one of the most practical and widely used structural design patterns in modern software development. It is designed to solve a very common problem — how to make two incompatible systems work together without modifying their existing code.
In real-world applications, developers frequently integrate third-party services, legacy systems, or external APIs. These systems often have different interfaces, which makes direct communication difficult.
Instead of rewriting stable and tested code, the Adapter Pattern provides a clean and scalable solution by acting as a bridge between incompatible interfaces.
Understanding the Problem (Real-World Thinking)
Before jumping into code, let’s understand the problem in a simple way.
Imagine you bought a laptop from another country. The plug of your charger does not fit into your local wall socket.
Now you have two options:
- Change the wall socket (not practical)
- Modify the charger (risky and unnecessary)
Instead, you use a power adapter.
The adapter does not change either side — it simply converts one interface into another so both can work together.
This is exactly what the Adapter Pattern does in software.
What is Adapter Pattern?
The Adapter Pattern is a structural design pattern that allows two incompatible interfaces to collaborate by introducing a middle layer called an adapter.
This adapter converts the interface of an existing class into another interface that the client expects.
In simple terms, it acts like a translator between two systems.
Key Components (Very Important to Understand)
To fully understand this pattern, you must clearly know these three components:
- Target (What your system expects): This is the interface your application is built around. All your internal code depends on this structure.
- Adaptee (Existing incompatible system): This is the external or legacy system that has a different interface. You cannot modify this code.
- Adapter (The bridge): This is the class that connects both systems. It translates requests from the Target into a format the Adaptee understands.
Why Do We Use Adapter Pattern in C#?
The Adapter Pattern in C# is used to integrate two incompatible systems without changing or modifying their existing implementation.
It plays a crucial role in real-world applications where systems evolve independently.
- Reuse existing or legacy code: You can reuse already tested and stable code without rewriting it, which saves development time and reduces risk.
- Integrate third-party APIs: External APIs rarely match your system’s interface. The adapter helps map them seamlessly.
- Avoid modifying stable systems: Instead of changing production-level code, you introduce a safe layer that handles compatibility.
- Improve flexibility and scalability: You can easily replace or extend components without breaking existing functionality.
- Follow SOLID principles: The pattern supports clean architecture by separating concerns and reducing tight coupling.
Real-World Example : Language Translator
Imagine you are traveling to a foreign country where people speak Japanese, and you only understand English.
Now communication becomes impossible:
- You speak → English
- They understand → Japanese
You bring in a translator.
The translator listens to your English, converts it into Japanese, and delivers it. Then converts the response back to English.
Here’s how it maps to Adapter Pattern:
- You → Target
- Local person → Adaptee
- Translator → Adapter
Neither side changes — the adapter handles everything.
Example 1 : Payment Gateway Integration (Step-by-Step)
Now let’s understand this with a real software example.
You are building an e-commerce application.
Your system expects all payments to follow a standard interface:
Step 1: Target (Your System Expectation)
public interface IPaymentProcessor { void Pay(decimal amount); }This interface is used everywhere in your application.
Step 2: Adaptee (Third-Party Service)
The third-party service provides a different method:
public class ThirdPartyPayment { public void MakePayment(double value) { Console.WriteLine($"Paid {value} using third-party service"); } }This is the Adaptee. You cannot modify it.
Step 3: Adapter (Bridge Between Both)
Now we create a class that connects both systems.
public class PaymentAdapter : IPaymentProcessor { private readonly ThirdPartyPayment _payment; public PaymentAdapter(ThirdPartyPayment payment) { _payment = payment; } public void Pay(decimal amount) { _payment.MakePayment((double)amount); } }This adapter converts your system call into a format the third-party understands.
Step 4: Usage
class Program { static void Main() { var thirdParty = new ThirdPartyPayment(); IPaymentProcessor adapter = new PaymentAdapter(thirdParty); adapter.Pay(1000); } }Your system works perfectly without any changes.
Example 2: Notification System Integration
Real-World Scenario:
Imagine you are building a notification system for your application.
Your system is designed to send notifications using a simple interface like:
Send(message)
All parts of your application — user registration, order confirmation, password reset — rely on this interface.
Now, your company integrates a third-party notification service that sends messages differently.
Instead of Send(), it uses:
PushNotification(string content)
Now you have a problem:
- Your system expects → Send(string message)
- Third-party service provides → PushNotification(string content)
Changing your entire system would be risky and time-consuming.
This is where the Adapter Pattern solves the problem.
Step 1: Target Interface (Your System Expectation)
This is the interface your application is already using.
public interface INotificationService { void Send(string message); }All your application components depend on this interface.
Step 2: Adaptee (Third-Party Service)
This is the external service that does not match your system.
public class ThirdPartyNotifier { public void PushNotification(string content) { Console.WriteLine("Notification sent: " + content); } }This class is the Adaptee. You cannot modify it.
Step 3: Adapter (The Bridge)
Now we create an adapter that connects your system with the third-party service.
public class NotificationAdapter : INotificationService { private readonly ThirdPartyNotifier _notifier; public NotificationAdapter(ThirdPartyNotifier notifier) { _notifier = notifier; } public void Send(string message) { // Convert and delegate _notifier.PushNotification(message); } }This adapter:
- Implements your system’s interface (Target)
- Uses the third-party service (Adaptee)
- Translates the method call
Step 4: Usage
class Program { static void Main() { var notifier = new ThirdPartyNotifier(); INotificationService adapter = new NotificationAdapter(notifier); adapter.Send("Welcome to our platform!"); } }Now your system works seamlessly with the external notification service without any changes.
What Did We Achieve?
- No changes in existing system: Your application continues using the same interface.
- Easy integration: Third-party service is connected without modifying its code.
- Clean architecture: The adapter handles all conversion logic.
- Future flexibility: You can replace the notification provider without affecting your system.
Common Use Cases
- Integrating third-party APIs
- Working with legacy systems
- Data format conversion (XML ↔ JSON)
- External SDK integration
Key Benefits of Adapter Pattern
- Code Reusability: You can reuse existing systems without modification, saving time and effort.
- Flexibility: New components can be added without breaking existing code.
- Backward Compatibility: Old systems can work with modern applications.
- Loose Coupling: Components remain independent and easy to maintain.
Common Mistakes While Using Adapter Pattern
While the Adapter Pattern is simple and powerful, developers often misuse it or overcomplicate their design. Understanding these common mistakes will help you apply the pattern correctly in real-world applications.
- Using Adapter unnecessarily: The Adapter Pattern should only be used when there is a real incompatibility between interfaces. Applying it where systems are already compatible adds unnecessary complexity and makes the code harder to maintain.
- Mixing business logic inside the adapter: The adapter’s responsibility is only to convert one interface into another. Adding business logic inside it violates clean architecture principles and makes the code difficult to manage.
- Not clearly identifying Target and Adaptee: Many developers confuse these roles, which leads to poor implementation. Always identify what your system expects (Target) and what you are adapting (Adaptee) before writing code.
- Overusing multiple adapters: Creating too many adapters for small differences can clutter your codebase. Instead, design a more generalized adapter if possible.
- Ignoring performance impact: Although minimal, adapters add an extra layer of abstraction. In performance-critical systems, excessive use can slightly impact execution.
By avoiding these mistakes, you can ensure that your implementation of the Adapter Pattern remains clean, efficient, and easy to maintain.
Summary
The Adapter Pattern is a powerful design pattern that helps connect incompatible systems without modifying existing code.
It acts as a bridge between different interfaces, making applications flexible, scalable, and easy to maintain.
By understanding the roles of Target, Adapter, and Adaptee, you can confidently apply this pattern in real-world scenarios.