هنگامی که از رویکرد Domain Driven Design در برنامه خود استفاده میکنیم، گاهی اوقات مجبور میشویم متدی را در چندین نمونه از Aggregate از یک نوع فراخوانی کنیم.
به عنوان مثال، در دامنه خود موجودیت مشتری داریم و وقتی کمپین بزرگ جمعه سیاه شروع می شود، باید تخفیف های آنها را دوباره محاسبه کنیم. بنابراین در مدل دامنه، Aggregate مشتری با متد RecalculateDiscount وجود دارد و در Application Layer، DiscountAppService را داریم که مسئولیت این use case را بر عهده دارد.
2 راه برای اجرای این سناریو و سناریوهای مشابه وجود دارد:
public class DiscountAppService { private readonly ICustomersRepository customersRepository; public DiscountAppService(ICustomersRepository customersRepository) { this.customersRepository = customersRepository; } public void RecalculateCustomersDiscounts() { var allCustomers = this.customersRepository.GetAll(); using (var transaction = new TransactionScope()) { foreach (customer in allCustomers) { customer.RecalculateDiscount(); // Save changes to DB } transaction.Complete(); } } }
این سادهترین راهحل است، ما تمام مشتریان را جمعآوری میکنیم و در هر نمونه از متد RecalculateDiscount استفاده میکنیم. ما پردازش خود را با ()TransactionScope احاطه کردیم تا پس از آن مطمئن شویم که هر مشتری تخفیف را مجدداً محاسبه کرده است یا هیچ کدام از آنها را محاسبه نکرده است. این یک transacinal consicty است - ACID را برای ما فراهم می کند و گاهی اوقات راه حل کافی است، اما در بسیاری از موارد (مخصوصاً در هنگام پردازش چند Aggregate با رویکرد DDD) این راه حل رویکرد بسیار بدی است.
اول از همه، مشتریان در حافظه بارگذاری می شوند و ما می توانیم مشکل عملکرد/performance داشته باشیم. البته میتوانیم پیادهسازی را کمی تغییر دهیم، فقط شناسههای مشتریان را دریافت کنیم و در حلقه foreach مشتریان را یکی یکی بارگیری کنیم. اما ما مشکل بدتری داریم - تراکنش ما تا پایان پردازش روی aggregate ما قفل می کند و سایر فرآیندها باید منتظر بمانند. ما نمی توانیم از قفل ها خلاص شویم. در این حالت برنامه کمتر پاسخگو میشود، میتوانیم timeouts و بنبستهایی/deadlocks داشته باشیم.
در این رویکرد ما از یک تراکنش بزرگ استفاده نمی کنیم. به جای این کار، ما هر مشتری را جداگانه پردازش می کنیم. سازگاری نهایی به این معنی است که در زمان مشخصی سیستم ما در حالت ناسازگار باشد، اما پس از زمان معین سازگار خواهد بود. در مثال ما زمانی وجود دارد که برخی از مشتریان تخفیفهای خود را مجدداً محاسبه میکنند و برخی دیگر را خیر. بیایید کد را ببینیم:
public class DiscountAppService { private readonly ICustomersRepository customersRepository; public DiscountAppService(ICustomersRepository customersRepository) { this.customersRepository = customersRepository; } public void RecalculateCustomersDiscounts() { var allCustomersIds = this.customersRepository.GetAllCustomerIds(); foreach (customerId in allCustomersIds) { Process(new RecalculateCustomerDiscountCommand(customerId)); } } private void Process(RecalculateCustomerDiscountCommand command) { // Execute processing asynchronously, for example: // Using new Task.Run() // Set background job in Hangfire/Quartz..etc // Send message to Queue/Bus } }
در این مورد در ابتدا ما فقط شناسههای مشتریان را دریافت کردیم و جمعبندی مشتریان را یک به یک به صورت ناهمزمان (و در صورت وجود به موازات/parallel) پردازش میکنیم. ما مشکل قفل شدن aggregate های خود را برای مدت طولانی برطرف کردیم. ساده ترین راه حل استفاده از Task.Run() است، اما با استفاده از این رویکرد کنترل پردازش را کاملا از دست می دهیم. راه حل بهتر این است که از برخی کتابخانه های شخص ثالث مانند Hangfire، Quartz.NET یا سیستم پیام رسانی/messaging استفاده کنید.
سازگاری نهایی موضوع بزرگی است که در محاسبات توزیعشده/distributed computing استفاده میشود و همراه با CQRS با آن مواجه میشویم. گاهی اوقات این روش انتخاب خوبی نیست - می تواند بر UI تأثیر بگذارد و کاربران ممکن است برای مدتی داده های قدیمی را ببینند. به همین دلیل مهم است که با کارشناسان دامنه صحبت کنید زیرا اغلب برای کاربر خوب است که منتظر به روز رسانی داده ها باشد اما گاهی اوقات غیر قابل قبول است.
در transactional consistency - کل پردازش در یک تراکنش اجرا می شود. این رویکرد "همه یا هیچ" است هیچ حد وسطی وجود ندارد و گاهی اوقات می تواند منجر به کاهش عملکرد/performance، مقیاس پذیری/scalability و در دسترس بودن/availability برنامه ما شود.
در transactional consistency یک کلاینت دستوری را بر روی یک سیستم اجرا می کند که در ادامه تمام عملیات مورد نیاز برای حفظ درستی دامنه را در یک تراکنش اجرا می کند. در پاسخی که کلاینت دریافت می کند یا تمام عملیات ها با موفقیت انجام شده اند یا همه شکست خورده اند، هیچ حد وسطی وجود ندارد.
اما در سیستمهای توزیع شده(distributed systems) ، یک کلاینت دستوری را بر روی یک سیستم اجرا میکند اما تنها بخشی از تمام عملیات مورد نیاز برای حفظ ثبات دامنه در داخل یک تراکنش اجرا میشود. بقیه عملیات در زمان دیگری اجرا می شود، پس از آن زمان سیستم سازگار خواهد بود.
در Eventual consistency - پردازش در یک تراکنش بزرگ تقسیم می شود و همزمان اجرا نمی شود. در مدتی برنامه در حالت ناسازگار خواهد بود. این منجر به مقیاس پذیری بهتر و در دسترس بودن برنامه می شود. از طرف دیگر می تواند مشکلاتی را در رابط کاربری گرافیکی (داده های قدیمی) ایجاد کند و به مکانیسم های پشتیبانی نیاز دارد که پردازش موازی، تلاش مجدد و گاهی اوقات مانیتورهای پردازش را نیز ممکن می کند.
در واقع، هر دو رویکرد می توانند درست باشند.انتخاب بین این دو واقعاً به نیازهای دامنه یا کسب و کار شما و آنچه کارشناسان دامنه به شما می گویند بستگی دارد. همچنین به میزان مقیاس پذیر بودن سرویس بستگی دارد. و این بستگی به میزان سرمایه گذاری شما برای انجام کد خود دارد، زیرا Eventual consistency به کد پیچیده تری نیاز دارد تا ناهماهنگی های احتمالی در کل Aggregate ها و نیاز به اجرای اقدامات جبرانی را شناسایی کند.
بیشتر بخوانید: Entities, Value Objects, Aggregates and Roots
بیشتر بخوانید : Implementing DDD - Clean Architecture
بیشتر بخوانید : لایه Domain در طراحی دامنه گرا DDD
بیشتر بخوانید : نقشه راه توسعه دهندگان Asp.NET Core