طراحی مبتنی بر دامنه (DDD) اغلب بهخاطر شفافیت استراتژیکش ستایش میشود، اما اگر پیادهسازی تاکتیکی بیدقت باشد، ممکن است ناکارآمد شود. یکی از حیاتیترین و در عین حال پرخطاترین اجزا، رویداد دامنه است. با پیادهسازی صحیح، رویدادهای دامنه به زمینههای محدود (Bounded Contexts) مختلف اجازه میدهند بدون اتصال تنگ (Tight Coupling) به تغییرات واکنش نشان دهند. اما اگر بهدرستی انجام نشود، وابستگیهای پنهان و سیستمهای شکننده ایجاد میکند.
در این پست، سه ستون اصلی طراحی مؤثر رویدادهای دامنه را بررسی میکنیم: قراردادهای نامگذاری، ساختار بار پیام (Payload) و راهکارهای جداسازی.
هنر نامگذاری رویدادها
نامها، API شما هستند. در DDD، نام رویدادها باید بهوضوح چه اتفاقی افتاده و چه زمانی رخ داده را منتقل کنند. یک اشتباه رایج، استفاده از افعال امری یا اسمهای مبهم است.
بهترین روشها:
- از زمان گذشته استفاده کنید: رویدادها چیزهایی را توصیف میکنند که از قبل رخ دادهاند. OrderPlaced (سفارش ثبت شد)، نه PlaceOrder (سفارش ثبت کن).
- دقیق باشید: PaymentFailed (پرداخت ناموفق بود) بهتر از PaymentError (خطای پرداخت) است. دقت، تفسیر برای مشترکان را کاهش میدهد.
- ریشه آگریگت را شامل شوید: اگر زمینه واضح نباشد، نام آگریگت را به عنوان پیشوند یا بخشی از نام قرار دهید. InvoiceGenerated (فاکتور تولید شد) در مقابل InvoiceApproved (فاکتور تأیید شد).
از نامهای عمومی مانند EntityUpdated (کیته بهروزرسانی شد) پرهیز کنید. این کار مصرفکنندگان را مجبور میکند تا برای درک اهمیت تغییر، بار پیام را بررسی کنند که نقض اصل کپسولهسازی (Encapsulation) است.
ساختار بار پیام: دادههای حداقلی قابل استفاده
یک ضدالگوی رایج، ریختن کل وضعیت کیته (Entity) در بار پیام رویداد است. این کار دو مشکل ایجاد میکند:
- نشت اطلاعات: مشترکان دادههایی را دریافت میکنند که به آنها نیاز ندارند و جزئیات داخلی آگریگت را فاش میکند.
- شکنندگی: اگر ساختار کیته را تغییر دهید، تمام مصرفکنندگان را خراب میکنید، حتی کسانی که فقط به یک فیلد علاقهمند بودند.
قانون "حداقل بار پیام قابل استفاده"
فقط دادههای ضروری برای اقدام مشترک را شامل شوید. اگر مشترک به داده بیشتری نیاز دارد، باید از مخزن (Repository) خود پرسوجو کند یا یک فراخوانی API جداگانه انجام دهد.
// ❌ بد: همه چیز را میریزد
public class OrderCreatedEvent {
public Order Order { get; set; } // شامل ۵۰ فیلد، اکثر آنها نامرتبط
}
// ✅ خوب: فقط آنچه لازم است
public class OrderCreatedEvent {
public Guid OrderId { get; }
public Guid CustomerId { get; }
public DateTime CreatedAtUtc { get; }
public decimal TotalAmount { get; }
public Currency Code { get; }
}
توجه کنید که OrderCreatedEvent فقط فیلدهای ضروری برای واکنش سیستمهای پاییندست (مثلاً موجودی، صورتحساب) را شامل میشود. سرویس حمل و نقل ممکن است فقط به OrderId و CustomerId نیاز داشته باشد تا بعداً آدرس را جستجو کند. به TotalAmount نیاز ندارد.
جداسازی: هدف اصلی
هدف اصلی رویدادهای دامنه اتصال شل (Loose Coupling) است. اگر ناشر (Publisher) بداند مشترکان کیستند، شما جداسازی انجام ندادهاید. فقط یک لایه غیرمستقیم اضافه کردهاید.
راهکارهای جداسازی واقعی:
- انتشار از طریق انتزاع: ریشه آگریگت نباید مستقیماً سرویسها را فراخوانی کند. به جای آن، رویدادها را آشکار میکند و یک میانجی (Mediator) یا باس رویداد (Event Bus) آنها را منتشر میکند.
public class Order { private readonly List_events = new List (); public void Place() { if (Status != Status.Pending) throw new DomainException("Order not pending"); Status = Status.Placed; _events.Add(new OrderPlacedEvent(OrderId, CustomerId, TotalAmount, DateTime.UtcNow)); } public List GetUncommittedEvents() => _events; } - جداسازی رویدادهای یکپارچهسازی از رویدادهای دامنه:
- رویدادهای دامنه: در همان زمینه محدود یا برای یکپارچگی داخلی استفاده میشوند. ممکن است همگام یا ناهمگام باشند اما به منطق دامنه بهشدت متصل هستند.
- رویدادهای یکپارچهسازی: برای ارتباط بین زمینههای محدود استفاده میشوند. اینها باید پیامهای یکپارچگی نهایی (Eventual Consistency) باشند. باید پایدار، نسخهبندی شده و از طریق یک بکر (Broker) مانند Kafka یا RabbitMQ منتقل شوند.
رویدادهای دامنه داخلی خود را مستقیماً به یک بکر پیام منتشر نکنید. ابتدا آنها را به رویدادهای یکپارچهسازی نگاشت (Map) کنید. این کار به شما اجازه میدهد دامنه داخلی خود را بازطراحی کنید بدون اینکه قراردادها را با سیستمهای خارجی خراب کنید.
مدیریت نسخهبندی و سازگاری معکوس
رویدادها پیامهای طولانیعمر هستند. پس از انتشار، ممکن است توسط سیستمهایی مصرف شوند که کنترل آنها را در دست ندارید. اسکیماهای رویداد را مانند APIهای عمومی در نظر بگیرید.
- هرگز فیلدها را حذف نکنید: به جای آن فیلدهای جدید اضافه کنید. مصرفکنندگان میتوانند فیلدهای ناشناخته را نادیده بگیرند.
- رویدادها را نسخهبندی کنید: اگر تغییر خرابکننده (Breaking Change) لازم است، یک نسخه جدید از رویداد ایجاد کنید (مثلاً
OrderPlacedV2) و هر دو را برای یک دوره گذار بهصورت موازی اجرا کنید. - از ثبتکنندههای اسکیما (Schema Registries) استفاده کنید: ابزارهایی مانند Confluent Schema Registry قبل از استقرار، بررسیهای سازگاری را اعمال میکنند.
نتیجهگیری
رویدادهای دامنه ابزاری قدرتمند برای ساخت سیستمهای مقیاسپذیر و جداشده در DDD هستند. با این حال، قدرت آنها با مسئولیت همراه است. با پایبندی به قراردادهای نامگذاری واضح، نگه داشتن بار پیامها حداقلی و متمرکز، و جداسازی سختگیرانه رویدادهای دامنه از رویدادهای یکپارچهسازی، میتوانید سیستمهایی بسازید که در برابر تغییر مقاوم و آسان برای تکامل هستند.
به یاد داشته باشید: اگر مشترکان شما نیاز دارند بدانند چگونه رویداد تولید شده، شما جزئیات پیادهسازی را نشت دادهاید. اگر فقط نیاز دارند بدانند چه اتفاقی افتاده و آنها باید چه کنند، یک رویداد خوب طراحی کردهاید.
کدنویسی شاد!