🚀 چرا برای Dependency Injection در #C به Primary Constructor مهاجرت کردم؟صادقانه بگویم، مدتها در برابر Primary Constructorها مقاومت میکردم.
زمانی که در C# 12 این قابلیت از Recordها به Classها و Structهای معمولی گسترش پیدا کرد، اولین واکنش من تردید بود.
استفاده از یک mutable capture ضمنی بهجای فیلدهای صریح
readonly، در نگاه اول شبیه این بود که امنیت و صراحت کد را فدای راحتی کنیم.اما بعد از استفاده از آن در چندین پروژه مختلف، نظرم کاملاً تغییر کرد.
مقدار Boilerplateی که در کلاسهای مبتنی بر Dependency Injection حذف میشود قابل توجه است و مشکلی که در ابتدا بابت آن نگران بودم، در عمل کاملاً قابل مدیریت است؛ البته به شرطی که از آن آگاه باشید.
در این مطلب توضیح میدهم چه چیزی باعث شد به Primary Constructorها مهاجرت کنم و مهمترین نکتهای که باید هنگام استفاده از آنها بدانید چیست.
💡 چه چیزی نظرم را تغییر داد؟
کلاسهای سرویس من قبلاً معمولاً به این شکل بودند:
public class OrderService
{
private readonly IOrderRepository _orderRepository;
private readonly ILogger<OrderService> _logger;
public OrderService(
IOrderRepository orderRepository,
ILogger<OrderService> logger)
{
_orderRepository = orderRepository;
_logger = logger;
}
public async Task<Order?> GetOrderAsync(Guid id)
{
_logger.LogInformation("Fetching order {OrderId}", id);
return await _orderRepository.GetByIdAsync(id);
}
}
و حالا همان کلاس به این شکل نوشته میشود:
public class OrderService(
IOrderRepository orderRepository,
ILogger<OrderService> logger)
{
public async Task<Order?> GetOrderAsync(Guid id)
{
logger.LogInformation("Fetching order {OrderId}", id);
return await orderRepository.GetByIdAsync(id);
}
}
تعریف فیلدها، بدنه Constructor و Assignmentها همگی حذف شدهاند.
پارامترهای Constructor بهصورت خودکار Capture میشوند و در تمام بدنه کلاس قابل استفاده هستند.
این دقیقاً رایجترین سناریوی استفاده از Primary Constructorها است:Dependency Injection در کلاسهای سرویس.
وابستگیها را تعریف میکنید و مستقیماً از آنها استفاده میکنید.
🎯 جایی که بیشترین استفاده را از آن دارم: کلاسهای سرویس مبتنی بر DI
بیشترین جایی که Primary Constructorها مرا متقاعد کردند، کلاسهای سرویس در ASP.NET Core بود.
بخش عمده زمان من صرف توسعه همین نوع کلاسها میشود و حذف Boilerplate در اینجا بهسرعت خودش را نشان میدهد.
یک نمونه واقعیتر از فرآیند Checkout:
public class CheckoutService(
IPaymentProcessor paymentProcessor,
IOrderRepository orderRepository,
ILogger<CheckoutService> logger,
IOptions<CheckoutOptions> options)
{
public async Task<CheckoutResult> ProcessAsync(
Cart cart,
CancellationToken ct = default)
{
var settings = options.Value;
if (cart.Total < settings.MinimumOrderAmount)
{
logger.LogWarning("Order below minimum: {Total}", cart.Total);
return CheckoutResult.BelowMinimum;
}
var order = Order.Create(cart);
await paymentProcessor.ChargeAsync(order, ct);
await orderRepository.SaveAsync(order, ct);
logger.LogInformation("Checkout complete for order {OrderId}", order.Id);
return CheckoutResult.Success;
}
}
چهار Dependency، بدون حتی یک خط Boilerplate.
کلاس از ابتدا تا انتها فقط منطق کسبوکار را نمایش میدهد و هیچ کد اضافهای بین آن دیده نمیشود.
این الگو بسیار خوب عمل میکند زیرا کلاسهای سرویس معمولاً نیازی به اعتبارسنجی یا تبدیل Dependencyها ندارند.Container آنها را ایجاد میکند و شما صرفاً از آنها استفاده میکنید.
به همین دلیل Primary Constructorها انتخابی بسیار مناسب برای این سناریو هستند.