Event sourcing
می توان گفت که Event Sourcing یک راه حل جایگزین برای داده های ماندگار است. برخلاف state-oriented persistence که تنها آخرین نسخه از حالت موجودیت را نگه می دارد، Event Sourcing هر جهش حالت را به عنوان یک رکورد جداگانه تحت عنوان رویداد ذخیره می کند.
State-oriented persistence
تمام سیستم های real-life داده ها را ذخیره می کنند. شمار بسیاری از انواع مختلف ذخیره سازی داده وجود دارد، اما معمولاً توسعه دهندگان از پایگاه های داده برای ایمن نگه داشتن داده ها استفاده می کنند. پایگاه های داده کامپیوتری درست پس از ورود کامپیوترها به دنیای کسب و کار ظهور کردند. پایگاه داده های سلسله مراتبی به سرعت توسط پایگاه های داده رابطه ای که از دهه ی 1970 به بعد رشد کردند یعنی زمانی که اولین زبان های پرس و جو نویسی به نام های QUEL و SEQUEL ظاهر شدند، از بین رفتند. از دهه 1980، زمانی که SQL به زبان پرس و جوی استاندارد برای پایگاه های داده رابطه ای و RDBMS های مختلف مانند Oracle تبدیل شد، IBM DB/2 و Microsoft SQL Server رفته رفته بر دنیای پایداری داده ها مسلط شدند. با این حال، با ظهور برنامه نویسی شی گرا، که در اواخر دهه 1990 به جریان اصلی تبدیل شد، توسعه دهندگان شروع به مبارزه برای ماندگاری اشیاء در پایگاه داده های رابطه ای کردند. این مبارزه ای "عدم تطابق امپدانس شی-رابطه ای" نام گرفت. خانوادههای جایگزین پایگاههای داده، از پایگاههای داده قدیمیتر اشیاء تا پایگاههای داده اسناد مدرنتر، در طول دهه ها برای رسیدگی به مسائل عدم تطابق ظهور پیدا کردند.
اگرچه اهمیتی نداردکه برنامه از چه نوع پایگاه داده ای استفاده می کند، با این حال تنها چیزی که در پایگاه داده ذخیره می شود وضعیت فعلی سیستم است. اساساً، همه DMBS ها از چهار عملیات اساسی برای ماندگاری پشتیبانی می کنند - ایجاد، خواندن، به روز رسانی و حذف (CRUD).
اشیاء سیستم معمولاً به عنوان رکوردهای پایگاه داده – در ردیف هایی در جداول یا اسناد باقی می مانند. به این معنی که هنگامی که شی تغییر می کند، و تغییر نیاز به ماندگاری دارد، رکورد پایگاه داده با یک رکورد جدید که حاوی مقادیر به روز شده است، جایگزین می شود.
تاریخچه رکورد
نگهداری تاریخچه
اغلب تنها حفظ وضعیت فعلی یک موجودیت کافی نیست. هنگامی که چنین نیازی وجود دارد، توسعه دهندگان اغلب انتخاب می کنند که تاریخچه به روز رسانی خاصی را در یک جدول یا مجموعه اسناد جداگانه ذخیره کنند. با این حال، چنین تاریخچه ای فقط به یک مورد کاربردی خاص می پردازد و باید از قبل ساخته شود.
حتی زمانی که چنین راهحلی پیادهسازی و اجرا میشود، هیچ تضمینی وجود ندارد که فردا یا سال آینده، کسب وکار نیازی به نگهداشتن سابقه تغییرات وضعیت های دیگر نداشته باشد. باز هم، تمام تاریخچه قبل از تغییری که چنین رکوردی را حفظ می کرد، از بین خواهد رفت. مسئله دیگری که فقط در مورد حفظ وضعیت فعلی موجود است این است که تمام تغییراتی که در پایگاه داده رخ می دهد، طبیعتاً ضمنی هستند.
Eric Evans در کتاب Domain-Driven Design معتقد است که تاریخچه تغییرات موجودیت ها می تواند اجازه دسترسی به حالت های قبلی را بدهد، اما معنای آن تغییرات را نادیده می گیرد، به طوری که هرگونه دستکاری اطلاعات رویه ای است و اغلب از لایه دامنه خارج می شود.
هر تغییری در یک موجودیت ماندگار شده فقط یک به روز رسانی دیگر است که از همه به روز رسانی های دیگر قابل تشخیص نیست. برای مثال، وقتی به موجودیت Order و ویژگی Status آن نگاه می کنیم، اگر به Paid یا Shipped به روز شود، می توانیم (به طور ضمنی) بفهمیم که چه اتفاقی افتاده است. با این حال، در صورتی که مقدار ویژگی Total تغییر کند، چگونه می توانیم تشخیص دهیم چه چیزی باعث این تغییر شده است؟
جهش های صریح به عنوان رویداد
هنگام استفاده از رویدادهای دامنه، که به نوبه خود از اصطلاحات دامنه (Ubiquitous Language) برای توصیف تغییرات حالت استفاده می کنند، توضیح صریحی از تغییر داریم.
مورد پرداخت ممکن است برای افشای تفاوت کمی ساده باشد، بنابراین، بیایید از مثال دیگری استفاده کنیم. تصور کنید که مبلغ کل سفارش به دلیل دیگری تغییر می کند. اگر فقط حالت موجودیت را حفظ کنیم، این تغییر به صورت زیر انجام می شود:
در اینجا یک بهروزرسانی ضمنی برای وضعیت موجود اعمال شده است، و تنها راه برای اینکه بفهمیم چه اتفاقی افتاده است، نگاه کردن به نوع مسیر حسابرسی یا حتی گزارش برنامه است. وقتی تغییرات حالت را به عنوان رویدادهای دامنه مدل می کنیم، می توانیم آن تغییرات را تصریح کنیم و از زبان دامنه برای توصیف تغییر استفاده کنیم. در زیر مشاهده می کنید که اگر فقط به وضعیت نهایی نگاه کنیم، دو عملیات کاملاً متفاوت ممکن است اتفاق بیفتد که دقیقاً به یک نتیجه منجر شود.
هنگامی که تخفیف برای سفارش اعمال می شود، مبلغ کل تغییر می کند.
مجدداً، کل مبلغ ممکن است به دلایل دیگری مانند حذف یک مورد سفارش نیز تغییر کند.
رویداد دامنه در کد (Domain event in code)
یک رویداد دامنه زمانی که در کد پیاده سازی می شود چگونه به نظر می رسد؟ در واقع مثل یک کیسه از دارایی های ساده است. باید اطمینان حاصل کنیم که میتوانیم آن را جهت ماندگاری سریالسازی کنیم، همچنین میتوانیم هنگامی که زمان خواندن رویداد از پایگاه داده میرسد، آن را غیر سریالسازی کنیم.
لازم به ذکر است که Eric Evans در کتاب Domain-Driven Design معتقد است که رویداد دامنه بخش کاملی از مدل دامنه و نمایش چیزی که در دامنه اتفاق افتاده می باشد.
موجودیت ها به عنوان جریان رویداد
بنابراین، Event Sourcing مکانیسم ماندگاری است که در آن هر انتقال حالت برای یک موجودیت معین به عنوان یک رویداد دامنه نشان داده میشود که به پایگاه داده رویداد (مخزن رویداد) تداوم مییابد. هنگامی که حالت موجودیت جهش می یابد، یک رویداد جدید تولید و ذخیره می شود. هنگامی که نیاز به بازیابی حالت موجودیت داریم، همه ی رویدادها را برای آن موجودیت می خوانیم و هر رویداد را برای تغییر وضعیت اعمال می کنیم، زمانی که همه رویدادهای موجود خوانده و اعمال شدند، به وضعیت نهایی و صحیح موجودیت می رسیم.
جریان رسیدگی به فرمان (Command handling flow)
از آنجایی که قبلاً از مثال سفارش در این بخش استفاده کردهایم، میتوانیم موجودی را بسازیم که یک سفارش را نشان میدهد:
در این مثال، ما از الگوی Domain Model استفاده میکنیم، بنابراین موجودیت دارای حالت و رفتار است. انتقال حالت زمانی اتفاق میافتد که متدهای موجودیت را فراخوانی میکنیم. در نتیجه جریان اجرای هر عملیاتی به شکل زیر است:
اگر برنامه ما از معماری پورت ها و آداپتورها استفاده می کند، یک سرویس کاربردی خواهیم داشت که دستور زیر را انجام می دهد:
همچنین یک آداپتور لبه مانند HTTP خواهیم داشت که دستورات دنیای بیرون را میپذیرد، اما بحث آن خارج از محدوده ی این مقاله است.
موجودیت مبتنی بر رویداد
اکنون، بیایید بررسی کنیم که چه چیزی باید تغییر کند تا برنامه ما از Event Sourcing استفاده کند:
رویداد به عنوان کد
در ابتدا، ما به یک رویداد نیاز داریم که انتقال حالت را نشان دهد:
این رویداد اکنون رفتار دامنه خاصی را توصیف می کند و حاوی اطلاعات کافی برای تغییر وضعیت موجودیت Order است.
تولید رویدادها
سپس، باید کد موجودیت را تغییر دهیم، بنابراین یک رویداد ایجاد می شود:
در این مرحله، متد AddItem مستقیماً وضعیت موجودیت را تغییر نمی دهد، بلکه یک رویداد تولید می کند پس از متد Apply استفاده می کند که هنوز در کلاس Order وجود ندارد، بنابراین باید آن را پیاده سازی کنیم.
جمع آوری تغییرات
هر رویداد جدید یک تغییر است. کلاس Order باید تمام تغییراتی را که در جریان اجرای دستور اتفاق میافتد پیگیری کند، بنابراین ما میتوانیم آن تغییرات را در کنترلکننده فرمان ادامه دهیم. چنین رفتاری عمومی است و منحصر به کلاس Order نیست، بنابراین میتوانیم یک کلاس انتزاعی برای جداسازی کار فنی در آن بسازیم:
اکنون می توانیم کلاس Order را به منظور ارث بردن از کلاس Entity تغییر دهیم و کد کامپایل خواهد شد. با این حال، توجه داشته باشید که وقتی یک رویداد جدید تولید می کنیم، وضعیت موجودیت تغییر نمی کند. اگر هنگام رسیدگی به یک فرمان فقط یک رویداد تولید کنیم، ممکن است مشکلی نباشد، اما همیشه اینطور نیست. به عنوان مثال، هنگام اضافه کردن یک آیتم جدید، میتوانیم دو رویداد تولید کنیم: ItemAdded و TotalUpdated تا تغییرات حالت واضح تر و اتمی تر شود. در برخی موارد، کدی که رویدادهای متعاقب را تولید می کند، اما همچنان در همان تراکنش است، نیاز به دانستن وضعیت موجودیت جدید دارد که با رویداد قبلی تغییر کرده است. بنابراین، زمانی که هر رویداد را اعمال می کنیم، باید حالت در فرآیند را جهش دهیم.
استفاده از رویدادها برای جهش وضعیت
بیایید ابتدا روشی را پیاده سازی کنیم که با استفاده از رویدادها حالت موجودیت را تغییر می دهد. معمولاً این روش را When می نامند. میتوانیم یک متد انتزاعی را به کلاس پایه اضافه کنیم و اطمینان حاصل کنیم که وقتی رویدادهای جدید را به لیست تغییرات اضافه میکنیم فراخوانی میشود:
اکنون می توانیم متد When را در کلاس Order پیاده سازی کنیم:
اساساً برای بازیابی حالت موجودیت از رویدادها، باید فولد سمت چپ را روی همه رویدادهای جریان موجودیت اعمال کنیم.
با تمام این تغییرات، API عمومی کلاس Order تغییری نکرده است. همچنان متد AddItem را نشان می دهد و فقط اجرای داخلی متفاوت است.
استفاده از رویدادها برای ماندگاری
حال، بیایید به جریان اصلی رسیدگی فرمان بازگردیم و ببینیم از زمان استفاده از رویدادها تا الان، چگونه تغییر کرده اند.
در واقع، جریان کلی یکسان است، کد سرویس برنامه نیز یکسان است. تنها تغییر نحوه ی عملکرد آداپتور EntityStore است. برای state-oriented persistence، شبیه به قرار دادن حالت موجودیت در یک سند، سریال سازی و ذخیره در یک پایگاه داده سند، و سپس خواندن آن است. وقتی یک موجودیت event-sourced داریم چگونه به نظر می رسد؟ ما از همان پورت استفاده می کنیم، بنابراین API تغییر نمی کند. باید یک آداپتور را پیاده سازی کنیم که بتواند از یک پایگاه داده رویدادگرا مانند Event Store استفاده کند.
از نظر اشیاء ماندگار، مورد کاربردی اصلی برای پایگاه داده رویداد این است که بتواند رویدادها را برای یک شی یا موجودیت واحد با استفاده از شناسه موجودیت ذخیره و بارگذاری کند. تفاوت اصلی این است که وقتی از یک پایگاه داده رابطهای یا سندی استفاده کرده و شناسه موجودیت برای دریافت دادهها را به کار می گیرید، رکورد واحدی را دریافت میکنید که مستقیماً وضعیت موجودیت فعلی را نشان میدهد. در مقابل، هنگامی که یک موجودیت را از پایگاه داده رویداد بازیابی می کنید، چندین رکورد برای یک شناسه دریافت می کنید و هر رکورد در مجموعه یک رویداد است.
بنابراین، کل مجموعه رویدادهایی که یک موجودیت واحد را نشان می دهند، دارای یک شناسه منحصر به فرد هستند. از آنجایی که ما یک رکورد واحد نداریم که به آن شناسه اختصاص داده شده باشد، آن مجموعه رویداد را یک جریان می نامیم. جریان مجموعه ای مرتب از رویدادها برای یک شی واحد در سیستم است. شناسه شی همراه با نوع شی اغلب به عنوان نام جریان به کار می رود.
وقتی همه رویدادها را از یک جریان واحد میخوانیم، میتوانیم وضعیت فعلی را با فراخوانی متد When برای همه رویدادها به ترتیب بازسازی کنیم. با یک پایگاه داده انتزاعی رویداد گرا، آداپتور EntityStore شبیه کد زیر است:
نکته مهمی که در اینجا باید ذکر شود این است که کدی که بر اساس اطلاعات ذخیره شده در رویدادها حالت موجودیت را تغییر می دهد، نباید منطق یا محاسبات پیشرفته ای داشته باشد. ترجیحاً رویداد از قبل حاوی تمام اطلاعات مورد نیاز باشد. بنابراین متد When میتواند مستقیماً از آن برای تغییر ویژگیهای حالت موجودیت استفاده کند. با پیروی از این الگو، مطمئن خواهید شد که انتقال های حالت ماندگار هستند و وضعیت موجودیت فعلی همیشه قابل پیش بینی خواهد بود.
منبع مورد استفاده:
https://www.eventstore.com/blog/what-is-event-sourcing
این مطلب بخشی از تمرین درس معماری نرم افزار در دانشگاه شهید بهشتی می باشد.