عنوان:

‫استفاده از Keyed Services در NET 8. برای مدیریت پیشرفته تزریق وابستگی‌ها


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۱/۱۲ ۰۹:۳۲
آدرس: www.dntips.ir
مقدمه
تزریق وابستگی (Dependency Injection یا DI) یکی از ستون‌های اصلی توسعه نرم‌افزارهای مدرن در NET. است که به ایجاد کدهایی تمیز، قابل تست و قابل نگهداری کمک می‌کند. در بسیاری از برنامه‌های واقعی، توسعه‌دهندگان با سناریوهایی مواجه می‌شوند که نیاز به استفاده از چندین پیاده‌سازی مختلف برای یک رابط (interface) واحد دارند؛ مثلاً ارسال اعلان‌ها از طریق ایمیل، پیامک یا نوتیفیکیشن‌ها. پیش از NET 8.، مدیریت این سناریوها اغلب نیازمند راه‌حل‌هایی مانند استفاده از الگوهای کارخانه (factory pattern)، فیلتر کردن دستی پیاده‌سازی‌ها یا حتی استفاده از سرویس‌لوکیتور (service locator) بود که همگی پیچیدگی‌هایی به کد اضافه می‌کردند. با معرفی Keyed Services در NET 8.، مایکروسافت راه‌حلی داخلی و ساده ارائه کرده است که امکان ثبت و بازیابی سرویس‌ها را با استفاده از کلیدهای مشخص فراهم می‌کند. این مقاله به بررسی مفهوم Keyed Services، نحوه استفاده از آن در سناریوهای واقعی و بهترین شیوه‌های به‌کارگیری آن در برنامه‌های NET. می‌پردازد.

Keyed Services چیست؟
Keyed Services مکانیزمی در تزریق وابستگی NET 8. است که به توسعه‌دهندگان اجازه می‌دهد چندین پیاده‌سازی از یک رابط را با استفاده از کلیدهای منحصربه‌فرد (مانند رشته‌ها یا مقادیر ثابت) ثبت و بازیابی کنند. این قابلیت جایگزینی برای روش‌های قدیمی‌تر مانند تزریق مجموعه‌ای از پیاده‌سازی‌ها (IEnumerable) یا استفاده از الگوهای پیچیده‌تر است. با Keyed Services، می‌توانید به‌طور مستقیم پیاده‌سازی موردنظر خود را با یک کلید مشخص درخواست کنید، بدون نیاز به شرط‌های اضافی یا منطق انتخاب در کد.

مزایای اصلی Keyed Services شامل موارد زیر است:
  • کاهش پیچیدگی: دیگر نیازی به فیلتر کردن دستی پیاده‌سازی‌ها یا استفاده از کارخانه‌های سفارشی نیست.
  • خوانایی بهتر: کد تمیزتر و قابل فهم‌تر می‌شود، زیرا وابستگی‌ها به‌صورت واضح با کلیدها مشخص می‌شوند.
  • انعطاف‌پذیری: امکان استفاده از کلیدهای مختلف (مانند رشته یا enum) برای سناریوهای پویا.

سناریوی واقعی: ارسال اعلان‌های چندکاناله
برای درک بهتر، فرض کنید برنامه‌ای دارید که باید اعلان‌ها را از طریق کانال‌های مختلف مانند ایمیل، پیامک و نوتیفیکیشن، ارسال کند. همه این کانال‌ها از یک رابط مشترک INotificationService پیروی می‌کنند. در ادامه، نحوه پیاده‌سازی این سناریو با Keyed Services بررسی می‌شود.
ابتدا رابط و پیاده‌سازی‌های آن را تعریف می‌کنیم:
public interface INotificationService
{
    Task SendAsync(string message);
}

public class EmailNotificationService : INotificationService
{
    public Task SendAsync(string message)
    {
        Console.WriteLine($"Sending email: {message}");
        return Task.CompletedTask;
    }
}

public class SmsNotificationService : INotificationService
{
    public Task SendAsync(string message)
    {
        Console.WriteLine($"Sending SMS: {message}");
        return Task.CompletedTask;
    }
}

public class PushNotificationService : INotificationService
{
    public Task SendAsync(string message)
    {
        Console.WriteLine($"Sending push notification: {message}");
        return Task.CompletedTask;
    }
}

ثبت Keyed Services
برای ثبت این سرویس‌ها، از متدهای AddKeyedSingleton، AddKeyedScoped یا AddKeyedTransient استفاده می‌کنیم. به‌عنوان مثال، در فایل Program.cs:
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<INotificationService, EmailNotificationService>("email");
builder.Services.AddKeyedSingleton<INotificationService, SmsNotificationService>("sms");
builder.Services.AddKeyedSingleton<INotificationService, PushNotificationService>("push");

builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
در اینجا، هر پیاده‌سازی با یک کلید رشته‌ای ("email"، "sms" یا "push") ثبت شده است. نوع کلید می‌تواند هر شیء‌ای باشد که متد Equals را به‌درستی پیاده‌سازی کند، اما استفاده از رشته یا enum رایج‌تر است.

استفاده از Keyed Services
برای استفاده از یک سرویس خاص، از ویژگی FromKeyedServices استفاده می‌کنیم. به‌عنوان مثال، در یک کنترلر:
[ApiController]
[Route("api/[controller]")]
public class NotificationController : ControllerBase
{
    private readonly INotificationService _emailService;

    public NotificationController(
        [FromKeyedServices("email")] INotificationService emailService)
    {
        _emailService = emailService;
    }

    [HttpPost("send-email")]
    public async Task<IActionResult> SendEmail(string message)
    {
        await _emailService.SendAsync(message);
        return Ok("Email sent!");
    }
}
در این کد، فقط پیاده‌سازی مربوط به کلید "email" تزریق می‌شود، بدون نیاز به منطق اضافی برای انتخاب سرویس.

استفاده از کلیدهای enum برای ایمنی بیشتر
استفاده از رشته‌ها به‌عنوان کلید ساده است، اما ممکن است به خطاهایی مانند اشتباه تایپی منجر شود. برای افزایش ایمنی نوع (type safety)، می‌توان از enum استفاده کرد. به‌عنوان مثال:
public enum NotificationType
{
    Email,
    Sms,
    Push
}

builder.Services.AddKeyedSingleton<INotificationService, EmailNotificationService>(NotificationType.Email);
builder.Services.AddKeyedSingleton<INotificationService, SmsNotificationService>(NotificationType.Sms);
builder.Services.AddKeyedSingleton<INotificationService, PushNotificationService>(NotificationType.Push);
سپس در کنترلر:
public NotificationController(
    [FromKeyedServices(NotificationType.Email)] INotificationService emailService)
{
    _emailService = emailService;
}
این روش خطر خطاهای زمان اجرا را کاهش می‌دهد و کد را قابل نگهداری‌تر می‌کند.

انتخاب پویا در زمان اجرا
گاهی اوقات، نیاز است که سرویس مناسب در زمان اجرا و بر اساس ورودی کاربر یا تنظیمات انتخاب شود. در این موارد، می‌توان از IServiceProvider استفاده کرد:
public class DynamicNotificationHandler
{
    private readonly IServiceProvider _provider;

    public DynamicNotificationHandler(IServiceProvider provider)
    {
        _provider = provider;
    }

    public async Task SendAsync(string message, string channel)
    {
        var service = _provider.GetKeyedService<INotificationService>(channel);
        if (service != null)
        {
            await service.SendAsync(message);
        }
        else
        {
            throw new InvalidOperationException($"No service registered for channel: {channel}");
        }
    }
}
این روش انعطاف‌پذیری بالایی برای سناریوهای پویا فراهم می‌کند، اما باید با احتیاط استفاده شود تا از الگوی سرویس‌لوکیتور دوری شود.

بهترین شیوه‌ها و نکات
  • از enum به‌جای رشته استفاده کنید: برای کاهش خطاها و افزایش خوانایی.
  • محدودیت‌های زمانی حیات (lifetime) را در نظر بگیرید: اطمینان حاصل کنید که زمان حیات سرویس‌ها (singleton، scoped یا transient) با نیازهای برنامه همخوانی دارد.
  • از استفاده بیش از حد اجتناب کنید: Keyed Services برای سناریوهای خاص طراحی شده است. در مواردی که یک پیاده‌سازی کافی است، از ثبت ساده استفاده کنید.
  • تست‌پذیری را حفظ کنید: اطمینان حاصل کنید که کلیدها به‌گونه‌ای انتخاب شوند که تست واحد (unit testing) ساده باقی بماند.

نتیجه‌گیری
Keyed Services در NET 8. راه‌حلی قدرتمند و تمیز را برای مدیریت چندین پیاده‌سازی یک رابط در تزریق وابستگی‌ها، ارائه می‌دهد. این قابلیت با حذف نیاز به الگوهای پیچیده مانند کارخانه یا فیلتر کردن دستی، به توسعه‌دهندگان NET. امکان می‌دهد تا کدهایی خواناتر و قابل نگهداری‌تر بنویسند. با استفاده از کلیدهای مشخص (چه رشته‌ای و چه enum)، می‌توان به‌راحتی سرویس مناسب را در زمان طراحی یا اجرا انتخاب کرد. با این حال، استفاده صحیح و متعادل از این ویژگی برای جلوگیری از پیچیدگی غیرضروری ضروری است.

مراجع


مشاهده مطلب اصلی

نظرات

  • یاسین اسدنژاد در ۱۴۰۴/۰۱/۱۴ ۱۲:۰۱
    عالی بود. جایگزین خوبی برای پترن فکتوری متد هست. دیگه نمیخواد متد سازنده داشته باشیم و کلی switch داخلش بنویسیم.