مقالات › معماری نرم‌افزار

۸ الگوی طراحی که هر برنامه‌نویس C# باید بشناسد (با مثال واقعی)

الگوهای طراحی‌ای را یاد بگیرید که واقعاً در پروژه‌های دات‌نت استفاده می‌کنید: Strategy، Factory، Decorator، Adapter، Observer، Builder، Options و Mediator؛ با C# مدرن و DI.

الگوهای طراحی راه‌حل‌های نام‌گذاری‌شده برای مسائل تکراری طراحی هستند. لازم نیست هر ۲۳ الگوی Gang of Four را حفظ کنید؛ چند الگو مدام در کدهای واقعی دات‌نت ظاهر می‌شوند و اغلب داخل خود فریم‌ورک پنهان‌اند. شناختن آن‌ها کمک می‌کند طراحی خوب را تشخیص دهید، با تیم راحت‌تر حرف بزنید («بیایید برای کش از Decorator استفاده کنیم») و راه‌حل‌های ناشیانه را از نو اختراع نکنید.

۱. Strategy — تعویض الگوریتم در زمان اجرا

مسئله: چند روش قابل جایگزینی برای انجام یک کار (هزینه‌ی ارسال، درگاه پرداخت، قانون تخفیف) که در زمان اجرا انتخاب می‌شوند.

public interface IPaymentGateway
{
    string Name { get; }
    Task<PaymentResult> ChargeAsync(decimal amount, string orderId, CancellationToken ct);
}

public class ZarinpalGateway : IPaymentGateway { public string Name => "zarinpal"; /* ... */ }
public class StripeGateway   : IPaymentGateway { public string Name => "stripe";   /* ... */ }

public class PaymentService(IEnumerable<IPaymentGateway> gateways)
{
    public Task<PaymentResult> PayAsync(string gateway, decimal amount, string orderId, CancellationToken ct) =>
        gateways.Single(g => g.Name == gateway).ChargeAsync(amount, orderId, ct);
}

با DI، اضافه کردن یک درگاه یعنی یک کلاس جدید و یک ثبت؛ اصل باز/بسته در عمل. در ‎.NET 8 به بعد Keyed Services هم جایگزین داخلی برای انتخاب استراتژی با نام است.

۲. Factory — متمرکز کردن ساخت اشیا

مسئله: ساخت یک شیء منطقی دارد (پیکربندی، انتخاب زیرنوع) که نباید همه‌جا تکرار شود.

public interface INotificationFactory
{
    INotifier Create(NotificationChannel channel);
}

public class NotificationFactory(IServiceProvider services) : INotificationFactory
{
    public INotifier Create(NotificationChannel channel) => channel switch
    {
        NotificationChannel.Email    => services.GetRequiredService<EmailNotifier>(),
        NotificationChannel.Sms      => services.GetRequiredService<SmsNotifier>(),
        NotificationChannel.Telegram => services.GetRequiredService<TelegramNotifier>(),
        _ => throw new ArgumentOutOfRangeException(nameof(channel))
    };
}

فریم‌ورک پر از Factory است: IHttpClientFactory، ILoggerFactory و IDbContextFactory<T>.

۳. Decorator — اضافه کردن رفتار بدون تغییر کلاس

مسئله: اضافه کردن رفتار سراسری (کش، لاگ، تلاش مجدد، متریک) به یک سرویس موجود، بدون تغییر آن.

public interface IArticleService
{
    Task<ArticleDto?> GetAsync(string slug, CancellationToken ct);
}

public class CachedArticleService(IArticleService inner, IMemoryCache cache) : IArticleService
{
    public Task<ArticleDto?> GetAsync(string slug, CancellationToken ct) =>
        cache.GetOrCreateAsync($"article:{slug}", entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
            return inner.GetAsync(slug, ct);
        });
}

// ثبت: Decorator پیاده‌سازی واقعی را در بر می‌گیرد
builder.Services.AddScoped<ArticleService>();
builder.Services.AddScoped<IArticleService>(sp =>
    new CachedArticleService(sp.GetRequiredService<ArticleService>(), sp.GetRequiredService<IMemoryCache>()));

سرویس اصلی روی منطق بیزینس متمرکز می‌ماند و کش به‌صورت یک لایه روی آن قرار می‌گیرد. کتابخانه‌هایی مثل Scrutor متد کمکی Decorate<T>() را برای این کار اضافه می‌کنند. خود میان‌افزارهای ASP.NET Core هم زنجیره‌ای از Decoratorها دور درخواست هستند.

۴. Adapter — سازگار کردن اینترفیس‌های ناهمخوان

مسئله: به API یک شرکت دیگر وابسته‌اید که شکلش با انتزاع شما نمی‌خواند.

// انتزاع شما
public interface ISmsSender
{
    Task SendAsync(string phone, string message, CancellationToken ct);
}

// Adapter روی SDK سرویس‌دهنده
public class KavenegarSmsAdapter(KavenegarApi api, IOptions<SmsOptions> options) : ISmsSender
{
    public Task SendAsync(string phone, string message, CancellationToken ct) =>
        api.SendAsync(options.Value.SenderLine, phone, message);
}

اگر سرویس‌دهنده را عوض کنید، یک Adapter جدید می‌نویسید و باقی برنامه متوجه نمی‌شود. Adapterها ستون فقرات لایه‌ی Infrastructure در Clean Architecture هستند.

۵. Observer — اطلاع دادن یک تغییر به شنونده‌های متعدد

مسئله: وقتی اتفاقی می‌افتد، چند بخش مستقل باید واکنش نشان دهند (ارسال ایمیل، به‌روزرسانی آمار، ابطال کش) بدون اینکه مبدأ از آن‌ها خبر داشته باشد.

public record OrderPlaced(int OrderId, string CustomerEmail) : IDomainEvent;

public interface IDomainEventHandler<in TEvent> where TEvent : IDomainEvent
{
    Task HandleAsync(TEvent domainEvent, CancellationToken ct);
}

public class SendReceiptHandler(IEmailSender email) : IDomainEventHandler<OrderPlaced>
{
    public Task HandleAsync(OrderPlaced e, CancellationToken ct) =>
        email.SendAsync(e.CustomerEmail, "Your receipt", ct);
}

public class UpdateSalesStatsHandler(IStatsService stats) : IDomainEventHandler<OrderPlaced>
{
    public Task HandleAsync(OrderPlaced e, CancellationToken ct) => stats.IncrementAsync(ct);
}

Eventهای C#، IObservable<T>، Domain Eventها و Message Brokerها همگی شکل‌هایی از Observer هستند.

۶. Builder — ساخت گام‌به‌گام اشیای پیچیده

مسئله: یک شیء بخش‌های اختیاری زیادی دارد و سازنده‌ای با ده پارامتر خوانا نیست.

var report = new ReportBuilder()
    .WithTitle("Monthly sales")
    .ForPeriod(new DateOnly(2026, 4, 1), new DateOnly(2026, 4, 30))
    .IncludeChart(ChartType.Line)
    .GroupBy("Category")
    .Build();

مدام از Builder استفاده می‌کنید: WebApplication.CreateBuilder، StringBuilder، ModelBuilder در EF Core و UriBuilder. در تست‌ها هم Test Data Builder آماده‌سازی را خوانا نگه می‌دارد.

۷. Options — پیکربندی Strongly-Typed

یک الگوی مخصوص دات‌نت که جای رشته‌های جادویی را می‌گیرد:

public class StorageOptions
{
    public const string Section = "Storage";
    [Required] public string Root { get; set; } = "";
    public int MaxFileSizeMb { get; set; } = 5;
}

builder.Services.AddOptions<StorageOptions>()
    .BindConfiguration(StorageOptions.Section)
    .ValidateDataAnnotations()
    .ValidateOnStart();

public class UploadService(IOptions<StorageOptions> options) { /* options.Value.Root */ }

ValidateOnStart باعث می‌شود پیکربندی اشتباه همان لحظه‌ی شروع برنامه خطا بدهد، نه ساعت سه بامداد هنگام اولین آپلود.

۸. Mediator — جدا کردن فرستنده از Handler

مسئله: کنترلرهایی که به سرویس‌های زیادی وابسته‌اند و نیاز به رفتار سراسری دور هر Use Case.

public record GetArticleQuery(string Slug) : IRequest<ArticleDto?>;

public class GetArticleHandler(AppDbContext db) : IRequestHandler<GetArticleQuery, ArticleDto?>
{
    public Task<ArticleDto?> Handle(GetArticleQuery q, CancellationToken ct) =>
        db.Articles.Where(a => a.Slug == q.Slug)
            .Select(a => new ArticleDto(a.Title, a.Content))
            .FirstOrDefaultAsync(ct);
}

// کنترلر
[HttpGet("{slug}")]
public async Task<IActionResult> Get(string slug, ISender sender) =>
    await sender.Send(new GetArticleQuery(slug)) is { } dto ? Ok(dto) : NotFound();

هر Use Case یک Handler کوچک می‌شود و Pipeline Behaviorها اعتبارسنجی، لاگ یا تراکنش را دور همه‌ی آن‌ها اضافه می‌کنند. این معمولاً پایه‌ی CQRS (جدا کردن Command و Query) است. با دقت سبک‌سنگین کنید: در اپلیکیشن‌های کوچک، تزریق مستقیم سرویس ساده‌تر است.

الگوهایی که همین حالا استفاده می‌کنید

الگو در دات‌نت
Chain of Responsibility / Decorator Pipeline میان‌افزارها
Factory IHttpClientFactory، ILoggerFactory
Builder WebApplicationBuilder، ModelBuilder
Observer Eventها، IObservable<T>، IChangeToken
Strategy IComparer<T>، Authorization Handlerها
Unit of Work / Repository DbContext / DbSet<T>
Iterator IEnumerable<T>، yield return

کِی از الگو استفاده نکنیم

الگوها لایه‌ی واسط اضافه می‌کنند. قبل از به کار بردن یکی بپرسید: همین الان چه مسئله‌ی مشخصی را حل می‌کند؟ یک Strategy با فقط یک پیاده‌سازی، Factoryی که فقط new می‌کند یا Mediator در یک API پنج‌Endpointی، بیشتر فایل اضافه برای پیمایش می‌سازد. ساده شروع کنید و وقتی نیاز پیدا شد، به سمت الگو Refactor کنید.

جمع‌بندی

Strategy و Factory تنوع و ساخت را مدیریت می‌کنند، Decorator و Adapter رفتار و سیستم‌های خارجی را در بر می‌گیرند، Observer و Mediator اجزا را از هم جدا می‌کنند و Builder و Options ساخت و پیکربندی را تمیز می‌کنند. یاد بگیرید آن‌ها را در فریم‌ورک تشخیص دهید و وقتی مسئله‌ی واقعی آن‌ها را طلب کرد استفاده کنید تا طراحی‌های C# شما انعطاف‌پذیر باشند بدون اینکه بیش از حد مهندسی شوند.

پرسش‌های متداول

آیا الگوهای طراحی با C# مدرن هنوز مطرح‌اند؟

بله، هرچند قابلیت‌های زبان شکلشان را تغییر داده‌اند: Delegateها جای برخی کلاس‌های Strategy را گرفته‌اند، Recordها Value Objectها را ساده کرده‌اند و Containerهای DI خودشان Factory را پیاده می‌کنند.

تفاوت Decorator و Adapter چیست؟

Decorator همان اینترفیس را حفظ و رفتار اضافه می‌کند؛ Adapter یک اینترفیس را به اینترفیس دیگری تبدیل می‌کند.

آیا در هر پروژه‌ای از MediatR استفاده کنم؟

نه. در اپلیکیشن‌های بزرگ با Use Caseها و رفتارهای سراسری زیاد مفید است. در APIهای کوچک می‌تواند لایه‌ی واسط غیرضروری اضافه کند.