You’ve just integrated a third-party payment library into your e-commerce application. Everything looks good until you realize the library expects PaymentRequest objects, but your codebase uses Order objects. Do you refactor your entire system, or is there a smarter way? The Adapter pattern solves this exact problem—allowing incompatible interfaces to work together seamlessly.
Problem
Imagine you’re building an e-commerce system that processes orders. Your existing code uses an Order class:
public class Order
{
public string OrderId { get; set; }
public decimal Amount { get; set; }
public string CustomerEmail { get; set; }
public DateTime OrderDate { get; set; }
}
public class OrderProcessor
{
public void ProcessOrder(Order order)
{
Console.WriteLine($"Processing order {order.OrderId} for {order.Amount:C}");
}
}
Now you need to integrate a third-party payment service that expects a completely different interface:
// Third-party library - you can't modify this
public class PaymentService
{
public void ProcessPayment(PaymentRequest request)
{
Console.WriteLine($"Charging {request.TotalAmount} to {request.Email}");
}
}
public class PaymentRequest
{
public string TransactionId { get; set; }
public decimal TotalAmount { get; set; }
public string Email { get; set; }
}
Without the Adapter pattern, you’d need to manually convert between Order and PaymentRequest everywhere, creating duplicate code and tight coupling.
Solution
The Adapter pattern allows objects with incompatible interfaces to collaborate by creating a wrapper class that translates calls from one interface to another. It acts as a bridge between your existing code and new or third-party components.
Definition: The Adapter pattern converts the interface of a class into another interface that clients expect. It lets classes work together that couldn’t otherwise because of incompatible interfaces.
The pattern has two main variants:
- Object Adapter: Uses composition to wrap the adaptee
- Class Adapter: Uses inheritance (less common in C# due to single inheritance)
Pseudocode
Client (OrderProcessor)
↓ uses
Target Interface (IPaymentProcessor)
↓ implemented by
Adapter (OrderToPaymentAdapter)
↓ wraps
Adaptee (PaymentService)
Examples
Here’s a complete, runnable C# implementation:
using System;
// Target interface that your code expects
public interface IPaymentProcessor
{
void ProcessPayment(Order order);
}
// Adapter that bridges Order and PaymentService
public class OrderToPaymentAdapter : IPaymentProcessor
{
private readonly PaymentService _paymentService;
public OrderToPaymentAdapter(PaymentService paymentService)
{
_paymentService = paymentService;
}
public void ProcessPayment(Order order)
{
// Convert Order to PaymentRequest
var paymentRequest = new PaymentRequest
{
TransactionId = order.OrderId,
TotalAmount = order.Amount,
Email = order.CustomerEmail
};
// Delegate to the third-party service
_paymentService.ProcessPayment(paymentRequest);
}
}
// Your existing Order class
public class Order
{
public string OrderId { get; set; }
public decimal Amount { get; set; }
public string CustomerEmail { get; set; }
public DateTime OrderDate { get; set; }
}
// Third-party service (can't modify)
public class PaymentService
{
public void ProcessPayment(PaymentRequest request)
{
Console.WriteLine($"Charging {request.TotalAmount:C} to {request.Email}");
Console.WriteLine($"Transaction ID: {request.TransactionId}");
}
}
public class PaymentRequest
{
public string TransactionId { get; set; }
public decimal TotalAmount { get; set; }
public string Email { get; set; }
}
// Updated OrderProcessor that works with the adapter
public class OrderProcessor
{
private readonly IPaymentProcessor _paymentProcessor;
public OrderProcessor(IPaymentProcessor paymentProcessor)
{
_paymentProcessor = paymentProcessor;
}
public void ProcessOrder(Order order)
{
Console.WriteLine($"Processing order {order.OrderId}");
_paymentProcessor.ProcessPayment(order);
}
}
// Usage example
class Program
{
static void Main()
{
// Create the third-party service
var paymentService = new PaymentService();
// Create adapter
var adapter = new OrderToPaymentAdapter(paymentService);
// Use adapter with your existing code
var processor = new OrderProcessor(adapter);
var order = new Order
{
OrderId = "ORD-12345",
Amount = 99.99m,
CustomerEmail = "customer@example.com",
OrderDate = DateTime.Now
};
processor.ProcessOrder(order);
}
}
Output:
Processing order ORD-12345
Charging $99.99 to customer@example.com
Transaction ID: ORD-12345
Real-World Example: Legacy Database Integration
// Legacy database interface
public class LegacyDatabase
{
public void SaveData(string tableName, Dictionary<string, string> data)
{
Console.WriteLine($"Saving to {tableName}: {string.Join(", ", data)}");
}
}
// Modern interface your code uses
public interface IRepository<T>
{
void Save(T entity);
}
// Adapter for legacy database
public class LegacyDatabaseAdapter<T> : IRepository<T>
{
private readonly LegacyDatabase _legacyDb;
private readonly string _tableName;
private readonly Func<T, Dictionary<string, string>> _mapper;
public LegacyDatabaseAdapter(
LegacyDatabase legacyDb,
string tableName,
Func<T, Dictionary<string, string>> mapper)
{
_legacyDb = legacyDb;
_tableName = tableName;
_mapper = mapper;
}
public void Save(T entity)
{
var data = _mapper(entity);
_legacyDb.SaveData(_tableName, data);
}
}
// Usage
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
var legacyDb = new LegacyDatabase();
var adapter = new LegacyDatabaseAdapter<Customer>(
legacyDb,
"Customers",
c => new Dictionary<string, string>
{
{ "Id", c.Id.ToString() },
{ "Name", c.Name },
{ "Email", c.Email }
}
);
var customer = new Customer { Id = 1, Name = "John", Email = "john@example.com" };
adapter.Save(customer);
Applications
Enterprise Use Cases:
- Third-Party API Integration: When integrating payment gateways, shipping providers, or external services that use different data structures
- Legacy System Integration: Connecting modern applications with legacy systems that have incompatible interfaces
- Database Abstraction: Adapting different database drivers to a common repository interface
- UI Component Libraries: Adapting third-party UI components to match your application’s component interface
- Message Queue Integration: Adapting different message queue systems (RabbitMQ, Azure Service Bus, AWS SQS) to a common messaging interface
- File Format Conversion: Adapting different file format readers/writers to a common interface
Benefits:
- Enables code reuse without modifying existing classes
- Separates interface conversion logic from business logic
- Makes your code work with classes from incompatible interfaces
- Follows the Single Responsibility Principle by separating adaptation logic
When to Use:
- You need to use a class that has an incompatible interface
- You want to reuse existing subclasses that don’t match the required interface
- You’re integrating third-party libraries or legacy systems
The Adapter pattern is essential for building maintainable systems that integrate with external dependencies while keeping your core codebase clean and consistent.
