🎯 Design Pattern: Adapter
@CSharpGeeksگاهی در یک سیستم، دو بخش داریم که قابلیتهای مورد نیاز یکدیگر را دارند، اما Interface آنها با هم سازگار نیست.
مثلاً Application ما انتظار دارد با این قرارداد کار کند:
public interface IPaymentGateway
{
Task<bool> PayAsync(decimal amount);
}
اما یک Third-party SDK چنین APIای دارد:
public class StripeClient
{
public Task<bool> ChargeAsync(decimal amount)
{
// Call Stripe API
}
}
از نظر قابلیت، هر دو تقریباً یک کار انجام میدهند:
پرداخت مبلغ
اما Client ما دنبال PayAsync است و سرویس خارجی ChargeAsync دارد.
اگر StripeClient را مستقیم وارد Application کنیم، Application به API مربوط به Stripe وابسته میشود. اینجاست که Adapter Pattern وارد میشود.
🔌 ءAdapter دقیقاً چیست؟
ءAdapter یک Structural Design Pattern است که اجازه میدهد Objectهایی با Interfaceهای ناسازگار با یکدیگر همکاری کنند.
به زبان ساده:
ءAdapter، Interface یک Object موجود را به Interfaceای تبدیل میکند که Client انتظار دارد.
این همان تعریف کلاسیک Adapter در GoF است. یعنی:
Client │ ▼ Target Interface │ ▼ Adapter │ ▼ Adaptee
🧩 مثال واقعی در C#
ءInterface مورد انتظار سیستم:
public interface IPaymentGateway
{
Task<bool> PayAsync(decimal amount);
}
سرویس خارجی:
public class StripeClient
{
public Task<bool> ChargeAsync(decimal amount)
{
// Stripe implementation
return Task.FromResult(true);
}
}
حالا Adapter:
public sealed class StripePaymentAdapter : IPaymentGateway
{
private readonly StripeClient _stripe;
public StripePaymentAdapter(StripeClient stripe)
{
_stripe = stripe;
}
public Task<bool> PayAsync(decimal amount)
{
return _stripe.ChargeAsync(amount);
}
}
حالا Application فقط IPaymentGateway را میشناسد:
public class CheckoutService
{
private readonly IPaymentGateway _paymentGateway;
public CheckoutService(IPaymentGateway paymentGateway)
{
_paymentGateway = paymentGateway;
}
public async Task CheckoutAsync(decimal amount)
{
var success =
await _paymentGateway.PayAsync(amount);
// Application logic...
}
}
و در Composition Root:
services.AddScoped<IPaymentGateway, StripePaymentAdapter>();
در نتیجه:
CheckoutService
│
▼
IPaymentGateway
│
▼
StripePaymentAdapter
│
▼
StripeClient
│
▼
Stripe API
ءCheckoutService اصلاً لازم نیست بداند Stripe چه APIای دارد.
🧠 چهار نقش اصلی Adapter
در مدل کلاسیک GoF چهار نقش اصلی داریم:
1️⃣ Target
ءInterfaceای که Client انتظار دارد.
public interface IPaymentGateway
{
Task<bool> PayAsync(decimal amount);
}
2️⃣ Client
کدی که با Target کار میکند.
public class CheckoutService
{
private readonly IPaymentGateway _gateway;
}
3️⃣ Adaptee
کلاس موجودی که قابلیت مورد نیاز را دارد، اما Interface آن با Target سازگار نیست.
public class StripeClient
{
public Task<bool> ChargeAsync(decimal amount)
{
// ...
}
}
4️⃣ Adapter
کلاسی که Target را پیادهسازی میکند و درخواستها را به Adaptee ترجمه میکند.
public class StripePaymentAdapter : IPaymentGateway
{
private readonly StripeClient _stripe;
public Task<bool> PayAsync(decimal amount)
{
return _stripe.ChargeAsync(amount);
}
}
🔄 ءAdapter فقط اسم متد را تغییر نمیدهد
این نکته مهم است. Adapter میتواند علاوه بر Interface، Data Format یا نحوه فراخوانی را هم تبدیل کند.
مثلاً Application ما:
public record PaymentRequest(
decimal Amount,
string Currency);
اما سرویس خارجی انتظار دارد:
public record StripeChargeRequest(
long AmountInCents,
string CurrencyCode);
ءAdapter میتواند این دو مدل را ترجمه کند:
public sealed class StripePaymentAdapter : IPaymentGateway
{
private readonly StripeClient _stripe;
public StripePaymentAdapter(StripeClient stripe)
{
_stripe = stripe;
}
public async Task<bool> PayAsync(
decimal amount)
{
var amountInCents =
(long)(amount * 100);
return await _stripe.ChargeAsync(
amountInCents);
}
}
پس Adapter میتواند Interface Translation و Data Translation انجام دهد.
اما این با قرار دادن Business Logic اصلی سیستم داخل Adapter فرق دارد.
🏗️ ءAdapter در Clean Architecture
اینجا Adapter واقعاً جالب میشود. فرض کنید Application ما به این abstraction نیاز دارد:
public interface IFileStorage
{
Task<string> UploadAsync(
Stream file,
string fileName);
}
ءApplication فقط این Interface را میشناسد. حالا برای Azure:
Application
│
▼
IFileStorage
▲
│
AzureBlobStorageAdapter
│
▼
Azure SDK
اگر فردا S3 اضافه کنیم:
┌── AzureBlobStorageAdapter
│
IFileStorage ◄──────┤
│
└── S3StorageAdapter
ءApplication مجبور نیست برای تغییر Provider تغییر کند.
این ایده با معماری Ports & Adapters / Hexagonal Architecture ارتباط نزدیکی دارد؛ اما دقت کنیم:
ءAdapter Pattern و Hexagonal Architecture یکی نیستند.
ءAdapter یک Design Pattern است. Ports & Adapters یک Architectural Style است.
در یک معماری Hexagonal، Adapter میتواند implementation یک Port باشد.
🔥 یک مثال مهمتر: Notification
فرض کنید سیستم ما این قرارداد را دارد:
public interface INotificationSender
{
Task SendAsync(
string recipient,
string message);
}
حالا سه Provider داریم:
Twilio SendSmsAsync(...) SendGrid SendEmailAsync(...) Firebase SendNotificationAsync(...)
میتوانیم برای هر کدام Adapter داشته باشیم:
┌── TwilioSmsAdapter
│
INotificationSender ├── SendGridEmailAdapter
│
└── FirebasePushAdapter
ءApplication فقط:
await sender.SendAsync(
recipient,
message);
را میشناسد. این یکی از کاربردهای بسیار رایج Adapter در سیستمهای واقعی است:
External Provider → Internal Contract
⚔️ Adapter vs Decorator
این دو Pattern را خیلیها اشتباه میگیرند.
Adapter
هدف:
تغییر Interface
Client │ ▼ Target │ ▼ Adapter │ ▼ Existing Object
مثلاً:
PayAsync() ↓ ChargeAsync()
Decorator
هدف:
اضافه کردن رفتار، بدون تغییر قرارداد اصلی
مثلاً:
IPaymentGateway
│
▼
LoggingDecorator
│
▼
RetryDecorator
│
▼
StripePaymentAdapter
ءInterface میتواند همان IPaymentGateway باقی بماند. Decorator برای اضافه کردن behavior است؛ Adapter برای سازگار کردن Interfaceها.
🆚 Adapter vs Facade
این یکی هم مهم است.
Adapter:
یک Interface موجود را با Interface مورد انتظار Client سازگار میکند.
Expected Interface
↓
Adapter
↓
Existing Object
Facade:
یک Interface سادهتر برای کار با یک Subsystem پیچیده ارائه میدهد.
Client ↓ Facade ↓ ┌───────────────┐ │ Service A │ │ Service B │ │ Service C │ │ Service D │ └───────────────┘
پس:
Adapter = Compatibility
Facade = Simplification
ءMicrosoft نیز دقیقاً همین تفاوت را بین Adapter و Facade مطرح میکند.
🧬 Object Adapter vs Class Adapter
ءAdapter را میتوان به دو شکل کلاسیک پیادهسازی کرد.
Object Adapter
با Composition:
public class StripePaymentAdapter
: IPaymentGateway
{
private readonly StripeClient _stripe;
public StripePaymentAdapter(
StripeClient stripe)
{
_stripe = stripe;
}
}
یعنی:
Adapter │ └── has-a → StripeClient
این مدل در C# بسیار طبیعی است.
Class Adapter
با Inheritance / Multiple Inheritance پیادهسازی میشود.
در مدل کلاسیک، Adapter از Interfaceهای مورد نیاز و کلاس موجود ارثبری میکند.
اما C# از Multiple Inheritance برای classها پشتیبانی نمیکند.
بنابراین شکل کلاسیک Class Adapter در C# قابل پیادهسازی نیست و معمولاً از Object Adapter + Composition استفاده میکنیم.
🎯 چه زمانی Adapter بسازیم؟
وقتی چنین شرایطی داریم:
من یک API/Contract دارم
یک implementation موجود دارم
این دو با هم سازگار نیستند
نمیخواهم یا نمیتوانم implementation
موجود را تغییر دهم
مثلاً:
🔹 Third-party SDK
🔹 Legacy Code
🔹 External API
🔹 Payment Provider
🔹 SMS Provider
🔹 Email Provider
🔹 Cloud Storage
🔹 Message Broker
🔹 Database Driver
🔹 API Version Migration
🔹 Integration بین دو Module
این دقیقاً یکی از کاربردهای اصلی Adapter است: قرار دادن یک لایه ترجمه بین Client و یک سرویس موجود با Interface ناسازگار.
🚫 چه زمانی Adapter نسازیم؟
هر Wrapperای الزاماً Adapter نیست. اگر فقط این کار را میکنیم:
public class UserService
{
private readonly UserRepository _repository;
public UserService(UserRepository repository)
{
_repository = repository;
}
public Task<User> GetAsync(Guid id)
{
return _repository.GetAsync(id);
}
}
صرفاً به خاطر اینکه یک کلاس را داخل کلاس دیگری قرار دادهایم، نباید سریع بگوییم:
«این Adapter Pattern است.»
سؤال اصلی این است:
آیا داریم یک Interface ناسازگار را به Interface مورد انتظار Client تبدیل میکنیم؟
اگر نه، احتمالاً Pattern دیگری در کار است یا اصلاً Pattern مشخصی لازم نیست.
⚠️ هزینه Adapter چیست؟
ءAdapter رایگان نیست. با اضافه کردن آن معمولاً داریم:
Interface
+
Adapter
+
Tests
+
Registration
+
Mapping
را به سیستم اضافه میکنیم. بنابراین Complexity بیشتر میشود.
اگر کنترل کامل هر دو طرف را داریم و تغییر مستقیم یک کلاس سادهتر و کمریسکتر است، ممکن است Adapter ارزش اضافه کردن نداشته باشد.
یکی از trade-offهای شناختهشده Adapter همین است:
انعطاف بیشتر در برابر Complexity بیشتر.
💡 ءAdapter چه چیزی را از سیستم محافظت میکند؟
یکی از مهمترین ارزشهای Adapter این است که تغییرات بیرونی را در یک مرز محدود میکند.
بد:
Application │ ├── Stripe SDK ├── AWS SDK ├── Twilio SDK ├── SendGrid SDK └── Legacy API
هر تغییر External Provider میتواند بخشهای زیادی از سیستم را تحت تأثیر قرار دهد.
بهتر:
Application
│
Internal Interfaces
│
┌─────────────┼─────────────┐
▼ ▼ ▼
Adapter Adapter Adapter
│ │ │
Stripe AWS Twilio
حالا تغییرات Integration بیشتر در مرزهای سیستم محصور میشوند.
🧠 نکته مهندسی مهم
ءAdapter فقط یک Design Pattern برای «تغییر اسم متدها» نیست.
تفکر پشت آن مهمتر است:
سیستم داخلی خودت را مجبور نکن با زبان و Interface سیستم خارجی صحبت کند.
بهجای اینکه Core سیستم را با External Dependency تطبیق بدهی:
Our System
↓
External API
یک مرز ایجاد کن:
Our System
↓
Our Contract
↓
Adapter
↓
External API
این یعنی External Dependency را به مدل و قرارداد داخلی سیستم ترجمه کنیم.
🎯 خلاصه نهایی
Adapter یعنی:
Make incompatible interfaces work together.
یا سادهتر:
اگر دو Object کار مورد نیاز هم را دارند، اما Interfaceشان با هم جور نیست، Adapter میتواند بین آنها ترجمه کند.
Client
│
▼
Target Interface
│
▼
Adapter
│
▼
Adaptee
و مهمترین نکته: Adapter برای حل مشکل Compatibility است، نه برای اضافه کردن Behavior، نه برای ساده کردن یک Subsystem و نه برای جمع کردن Business Logic.
پس دفعه بعد که دیدی:
Interface A
≠
Interface B
قبل از اینکه یکی را مجبور به تغییر کنی، یک سؤال بپرس:
🎯 آیا اینجا یک Adapter لازم دارم؟
📚 منابع معتبر برای مطالعه بیشتر
• Design Patterns: Elements of Reusable Object-Oriented Software — Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (GoF)
• Microsoft — Design Patterns: Adapter and Façade
• Refactoring.Guru — Adapter Pattern