🎯 ءInvariant چیست و چرا بهترین مکان برای اعمال آن Domain Model است؟
بخش زیادی از کدهای بهظاهر DDD که در پروژههای NET. میبینم، قوانین کسبوکار را بین Handlerها، Validatorها و Controllerها پخش کردهاند، اما خود Domain Model تقریباً هیچ نقشی در محافظت از این قوانین ندارد.نتیجه چیست؟
همان قانون در چندین نقطه مختلف تکرار میشود، هر نسخه به مرور کمی از نسخههای دیگر فاصله میگیرد و در نهایت معتبر بودن یک شیء به این بستگی پیدا میکند که از چه مسیری وارد سیستم شده است.
البته میتوان با این رویکرد سیستمهای کاملاً عملیاتی ساخت. همه ما پروژههایی را دیدهایم که با تعداد زیادی
if و Validation کار میکنند و سالها بدون مشکل در حال اجرا هستند.
اما راه بهتری هم وجود دارد؛ راهی که از یک مفهوم بسیار مهم شروع میشود:
Invariant💡 ءInvariant چیست؟
ءInvariant قانونی است که باید در تمام طول عمر یک شیء برقرار بماند.
نه فقط هنگام ذخیره شدن در پایگاه داده.
نه فقط زمانی که یک Validator اجرا میشود.
بلکه هر زمان که به آن شیء دسترسی پیدا میکنید، باید آن قانون برقرار باشد؛ فارغ از اینکه شیء چگونه ساخته شده یا از کجا بارگذاری شده است.
چند مثال:
✅ یک Course همیشه باید عنوان داشته باشد.
✅ مجموع Order همیشه باید برابر مجموع آیتمهای آن باشد.
✅ یک Subscription در هر لحظه فقط در یکی از وضعیتهای Trial، Active، PastDue یا Canceled قرار دارد.
✅ یک Course منتشرشده باید حداقل یک Lesson داشته باشد.
نکته مهم اینجاست که هیچکدام از این قوانین درباره HTTP، Validation یا Database صحبت نمیکنند.
اینها قوانین دامنه هستند و باید مستقل از زیرساخت همیشه برقرار باشند.
❌ مشکل رویکردهای Procedural
فرض کنید یک Course به شکل زیر تعریف شده باشد:
public class Course
{
public string Title { get; set; }
public CourseStatus Status { get; set; }
public DateTime? PublishedOn { get; set; }
public decimal Price { get; set; }
}
در این طراحی: Constructor وجود ندارد.
تمام Setterها عمومی هستند.
هر کسی میتواند هر مقداری را در هر زمانی تغییر دهد.
در نتیجه قوانین کسبوکار در بخشهای مختلف سیستم پخش میشوند:
ءCreateCourseValidator بررسی میکند Title خالی نباشد.
ءPublishCourseHandler بررسی میکند Course قبلاً منتشر نشده باشد.
ءChangePriceHandler بررسی میکند Course آرشیو نشده باشد.
ءEndpoint جدیدی اضافه میشود و توسعهدهنده یکی از Handlerهای قبلی را Copy میکند اما یکی از Validationها را فراموش میکند.
اینجاست که مدل بهتدریج وارد وضعیتهای نامعتبر میشود.
مشکل اصلی Anemic Model همین است.
نه اینکه رفتار ندارد.
بلکه هیچ تضمینی درباره وضعیت خودش ارائه نمیکند.
در نتیجه تمام مصرفکنندگان آن مجبورند دائماً قوانین را تکرار کنند.
✅ Always Valid Model
ایده اصلی بسیار ساده است:
ء
Domain Model نباید هیچگاه وضعیت نامعتبر را بپذیرد.اگر Reference یک Course را در اختیار دارید، باید بتوانید به آن اعتماد کنید.
نباید در لایههای بالاتر دائماً بنویسید:
if(course.Title is null)
نباید چندین Validator موازی داشته باشید.
نباید امیدوار باشید که Handler مربوطه Validationها را فراموش نکرده باشد.
برای رسیدن به این هدف معمولاً سه اصل مهم وجود دارد.
1️⃣ جلوگیری از ساخت Object نامعتبر
اگر Course بدون Title معنایی ندارد، پس نباید امکان ساخت آن وجود داشته باشد.
public class Course
{
private Course(CourseId id, string title, Money price)
{
Id = id;
Title = title;
Price = price;
Status = CourseStatus.Draft;
}
public static Result<Course> Create(string title, Money price)
{
if (string.IsNullOrWhiteSpace(title))
{
return CourseErrors.TitleRequired;
}
return new Course(CourseId.New(), title, price);
}
}
در اینجا: Constructor خصوصی است.
تمام ساخت Object از طریق Factory انجام میشود.Validation تنها در یک نقطه انجام میشود.
از این لحظه به بعد هر Course موجود در سیستم دارای Title معتبر است.
2️⃣ محافظت از تغییر وضعیتها
حتی اگر ساخت Object کنترل شود، همچنان تغییر وضعیت میتواند قوانین را نقض کند.
به همین دلیل Setterهای عمومی نباید وجود داشته باشند.
تمام تغییرات باید از طریق متدهایی انجام شوند که قوانین را میشناسند.