🏗 استفاده در Entityها و Value Objectها (با یک نکته مهم)
کمکم استفاده از Primary Constructorها را در Entityها و Value Objectها نیز آغاز کردم؛ مخصوصاً در جاهایی که میخواهید وجود برخی پارامترها را هنگام ساخت شیء اجباری کنید.
public class Order(Guid customerId, Money total)
{
public Guid Id { get; } = Guid.NewGuid();
public Guid CustomerId { get; } = customerId;
public Money Total { get; } = total;
public OrderStatus Status { get; private set; } = OrderStatus.Pending;
public DateTime CreatedAt { get; } = DateTime.UtcNow;
public void Confirm()
{
if (Status != OrderStatus.Pending)
{
throw new InvalidOperationException(
$"Cannot confirm order in {Status} status.");
}
Status = OrderStatus.Confirmed;
}
}
در این طراحی، هیچ راهی برای ایجاد یک Order بدون
customerId یا total وجود ندارد.ءPrimary Constructor این محدودیت را مستقیماً در سطح تعریف Type نمایش میدهد.
اما یک تفاوت مهم با مثال سرویسها وجود دارد:
در اینجا پارامترهای Primary Constructor را به Propertyها اختصاص دادهایم:
public Guid CustomerId { get; } = customerId;این نکته اهمیت زیادی دارد و مستقیماً به بزرگترین چالش Primary Constructorها منتهی میشود.
⚠️ مشکلی که نزدیک بود باعث شود هرگز از آن استفاده نکنم
دلیل اصلی مقاومت اولیه من همین موضوع بود.
پارامترهای Primary Constructor در واقع فیلدهای readonly نیستند.
زمانی که مستقیماً از پارامترهای Primary Constructor در بدنه کلاس استفاده میکنید، کامپایلر آنها را بهعنوان یک متغیر قابل تغییر (mutable) Capture میکند.
هیچ فیلد readonly مخفیای در پشت صحنه ایجاد نمیشود.
به همین دلیل میتوانید بهاشتباه مقدار آنها را تغییر دهید:
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);
}
public void SomeOtherMethod()
{
// This compiles. No warning. No error.
orderRepository = null!;
logger = null!;
}
}
این کد بدون هیچ Warning یا Error کامپایل میشود.
در صورتی که اگر از Constructor سنتی و فیلدهای
private readonly استفاده میکردید، کامپایلر بلافاصله جلوی این کار را میگرفت.اما در Primary Constructorها سکوت میکند.
🔒 اگر به Immutability نیاز دارید
در صورتی که تضمین Immutable بودن برای شما اهمیت دارد، میتوانید پارامترها را به فیلدهای readonly اختصاص دهید:
public class OrderService(
IOrderRepository orderRepository,
ILogger<OrderService> logger)
{
private readonly IOrderRepository _orderRepository = orderRepository;
private readonly ILogger<OrderService> _logger = logger;
public async Task<Order?> GetOrderAsync(Guid id)
{
_logger.LogInformation("Fetching order {OrderId}", id);
return await _orderRepository.GetByIdAsync(id);
}
}
اما در این حالت بخش زیادی از مزیت Primary Constructor از بین میرود.
دوباره به تعریف فیلدها و Assignmentها بازمیگردید؛ فقط با سینتکسی متفاوت.
در عمل، تاکنون هرگز با این مشکل در یک کلاس سرویس مبتنی بر DI مواجه نشدهام.
احتمال اینکه در میانه اجرای یک متد، بهاشتباه Logger یا Repository را Reassign کنید بسیار کم است.
اما در Entityها و Value Objectها که Immutability اهمیت بیشتری دارد، این موضوع میتواند دردسرساز شود.
به همین دلیل هنوز در این بخش با احتیاط بیشتری عمل میکنم.
❌ چه زمانی هنوز از Constructorهای سنتی استفاده میکنم؟
با وجود تمام مزایا، هنوز همه چیز را به Primary Constructor تبدیل نکردهام.
برخی سناریوها همچنان برای Constructorهای کلاسیک مناسبتر هستند.
1️⃣ اعتبارسنجی پیچیده
اگر لازم باشد قبل از مقداردهی، پارامترها اعتبارسنجی شوند، به بدنه Constructor نیاز خواهید داشت.
public class EmailAddress
{
private readonly string _value;
public EmailAddress(string value)
{
if (string.IsNullOrWhiteSpace(value) || !value.Contains('@'))
{
throw new ArgumentException(
"Invalid email address.", nameof(value));
}
_value = value;
}
}