آشنایی با Clean Architecture : آشنایی با CQRS (قسمت هفتم)

قسمت اول : مقدماتقسمت دوم : مبانی معماریقسمت سوم : نگاهی به معماری سنتی سه لایهقسمت چهارم : اجزای Clean Architectureقسمت پنجم : پیاده سازی بر اساس سرویس هاقسمت ششم : پیاده سازی بر اساس UseCase هاقسمت هفتم : آشنایی با CQRS (شما در حال خواندن این مقاله هستید)در این قسمت علاوه بر آشنایی با CQRS کتابخانه جالبی را خواهیم دید که به کمک آن نه تنها CQRS بلکه پیاده سازی بر اساس UseCase نیز برای شما ساده خواهد شد. قبل از هر چیز با پترن Mediator آشنا شویم.فرض کنید چهار سرویس داریم که با هم در ارتباط هستند.این پترن میگوید به جای اینکه این سرویس ها به این شکل به طور مستقیم به هم مرتبط باشند یک آبجکت میانی داشته باشیم به نام Mediator که سرویس ها فقط با آن در ارتباط هستند و فقط Mediator میتواند با سرویس ها به طور مستقیم در ارتباط باشد .این مدل ارتباطی به کمک کتابخانه ی MediatR قابل پیاده سازی است. اساس کار Request/Response است و هر ریکوئست یک هندلر دارد که یک ریسپانس را ایجاد میکند.(عبارت ریکوئست و ریسپانس در این کتابخانه به لحاظ مفهومی استفاده شده است.)ریکوئست ها توسط Mediator هندل میشوند و خبر ندارند با چه هندلری هندل میشوند. با این روش دی کوپل کردن کنترلر نسبت به لایه اپلیکیشن به سادگی و وضوح انجام شده است. در واقع کنترلر اینجکشن های متفاوت از هندلرهای مختلف ندارد و به کمک این روش پیاده سازی CQRS نیز ساده خواهد بود.اساس CQRS این است که کامندها و کوئری ها کاملا مسئولیت جدا دارند و پیاده سازی آنها نیز باید دارای این تفکیک باشد. MediatR و یا استفاده از UseCase ها ، مقدمه ای برای پیاده سازی CQRS هستند.کنترلری را فرض کنید که endpoint های مختلف با متدهای مختلف دارد. میتوان این متدها را تفکیک کرد. آنچه اساسش فقط کوئریست و منجر به تغییر دیتابیس نمیشود با آنچه به اصطلاح command است فرق می‌کند. مثلا یک متد GET معمولا وظیفه کوئری گرفتن دارد و متدهای POST,PUT یا DELETE کامند محسوب میشوند چون تغییری روی دیتابیس اعمال میکنند.در Mediator میتوانیم این تفکیک شدن را بر عهده همان آبجکت Mediator بگذاریم که هندلر مختص هر نوع ریکوئست را به آن نسبت دهد و استفاده کند.ویژگی مثبت این روش این است که بخش کوئری و کامند کاملا نسبت به هم ایزوله هستند و میتوانیم از دیتابیس های مختلف برای این دو مسیر استفاده کنیم و نوشتن unit test راحت تر و بدون ساید افکت خواهد بود.سه نوع CQRS داریم:اولین نوع را Single-database میگوییم.از یک دیتابیس ریلیشنال یا غیر ریلیشنال استفاده میشود.کامند ها از دامین برای تغییر state استفاده میکنند و نتیجه را از طریق لایه persistence در دیتابیس ذخیره میکنند که در دات نت معمولا از یک ORM مثل Nhibernate یا EF استفاده میشود. کوئری ها به طور مستقیم توسط یک لایه سبک data access به دیتابیس وصل شده و به کمک مکانیزم هایی مثل linq یا اسکریپت های SQL یا حتا strored procedure دیتا را از دیتابیس دریافت میکنند.این نوع CQRS که از یک دیتابیس استفاده میکند ساده ترین نوع است.دومین نوع Two-database است که یکی برای نوشتن و دیگری یبرای خواندن است. دیتابیسی که برای نوشتن است برای همینکار بهینه شده. مثلا از یک دیتابیس non-relational استفاده می‌شود.در سمت خواندن هم دیتابیس برای خواندن باید بهینه شود. تغییرات روی دیتابیس نوشتنی، به دیتابیس خواندنی باید منتقل شود تا خواندن ما درست باشد.پس از یک پترن پایدارسازی دیتابیس باید استفاده شود. یعنی دو دیتابیس باید همیشه سینک باشند. شاید این روش پیچیده تر باشد اما پرفورمنس بالاتری دارد.سومین نوع CQRS حالت event-sourcing است. که دراین حالت ما state فعلی entityها را در یک دیتابیس نرمالایز شده ذخیره نمی‌کنیم. ما فقط تغییرات entity ها در طول زمان را ذخیره میکنیم. تاریخچه ای از تغییرات را خواهیم داشت که به آن event store میگوییم. میتوانیم با مکانیزمی current state هر entity را در اختیار داشته باشیم.این روش به ما کمک بزرگی میکند تا وضعیت یک object را در گذشته به راحتی پیدا کنیم و از آن میتوان به عنوان یک Logger نیز استفاده نمود چون جزء به جزء تغییراتِ state سیستم، در آن ثبت شده است. از آنجاییکه دیتا بصورت سریالایز شده ذخیره میشود، بارگزاری آن نیز با سرعت بالایی انجام خواهد شد.این حالت پیچیده ترین نوع CQRS است، ولی بهینه ترین است.فواید این روش چیست؟- در معماری domain-centeric استفاده از CQRS باعث میشود کد های ما سازماندهی درستی داشته باشند و بخش خواندن و نوشتن جدا شود.- بهینه سازی سمت کوئری یا کامند به طور جداگانه با توجه به نوع CQRS که انتخاب کردیم قابل انجام است.- هر چقدر سیستم پیچیده تر باشد، این مزایا با ارزش‌تر خواهند بود.معایب چیست؟- بوجود آمدن این جداسازی بین کوئری و کامند باعث پیچیدگی نرم افزار میشود.- اگر از حالت دو دیتابیسی استفاده کنیم پیجیدگی درسمت دیتابیس بیشتر میشود.- اگر از حالت سوم استفاده کنیم هزینه و نگهداری بیشتری میطلبد.در ادامه ساده ترین حالت CQRS را بررسی میکنیم.ما معماری خود را به دو مسیر Queries و Commands تقسیم میکنیم. به این دو مسیر Query Stack و Command Stack میگوییم.این جداسازی در لایه اپلیکیشن با همان Core صورت میگیرد.توجه کنید که آنچه در کوئری باید بهینه سازی شود برای خواندن دیتا است و آنچه در بخش کامندز باید بهینه سازی شود برای نوشتن دیتاست. پس زیست متدهای این دو مسیر کاملا با هم متفاوت است.کامندها رفتاری را در دامین مدل اجرا میکنند که از مسیر لایه persistence میگذرد در نهایت به نوشتن دیتا در دیتابیس منجر میشود.کوئری ها دیتاهای مناسب را از دیتابیس دریافت میکنند و با فرمتی مناسب در اختیار لایه پرزنتر قرار میدهند. در ادامه یک اکشن Post و یک اکشن Get را با این روش پیاده سازی میکنیم.پکیج های MediatR و MediaR.Extensions.Microsoft.DependencyInjection را دانلود میکنیم.خط زیر را اضافه میکنیم که به طور خودکار هندلر های ما اسکن و رجیستر شود.میخواهیم یک endpoint برای گرفتن لیست تمام پست ها بنویسیم. طبیعتا مسیر Query را باید طی کنیم. https://gist.github.com/b0dbec79955d2d9b0fd5adc1f0a95256 چیزی که نوشتیم نمونه ساختن از یک کلاس است که میتواند پراپرتی های آن پارامترهای ورودی اکشن باشد که فعلا برای سادگی، بی‌خیال شده ایم. در لایه Core سه فولدر به نام Queries و Commands و Handlers درست میکنیم.در فولدر Queries کلاس فوق را میسازیم که از IRequest مربوط به MediatR ارث بری کند و تایپ خروجی هم مشخص میکنیم. https://gist.github.com/38f9f127358c0b2cc992fdc7b88c5b3b حالا اینترفیس MediatR را به کنترلر اینجکت میکنیم.آبجکت این اینترفیس متدهای send و publish دارد. ما از متد Send استفاده میکنیم که بر اساس روش Request/Response است که در ابتدای نوشته توضیح داده شد.و در نهایت خواهیم داشت : https://gist.github.com/aad98158cd1721091626c8e000e45bea همه endpoint های ما چیزی شبیه همین خواهند بود.حالا هندلر مربوط به این کوئری را مینویسیم.در فولدر handlers فایل GetAllPostHandler را میسازیم.به شکل زیر از IRequesHandler پیاده سازی می‌کنیم و ورودی/خروجی این کوئری را به شکل آرگومان به اینترفیس میدهیم. https://gist.github.com/910592cde065fefe928f74583206b52e حالا چیزهایی که این هندلر نیاز دارد را اینجکت میکنیم.نکته مثبت این روش و روش UseCase ها که در مقاله قبل به آن پرداختیم این است که هر هندلر فقط اینجکت های مربوط به خودش را در خود دارد. https://gist.github.com/7e8ebcfde9f7a601388527f0c12b4aa2 میتوانستیم از AutoMapper استفاده کنیم که تعداد خطوط کمتر شود.تفاوتی که این روش با روش UseCase ها دارد این است که کنترلر ما به شکل صریح هیچ ارتباط با هندلر ها ندارد.حالا میخواهیم یک آیتم به جدول post اضافه کنیم. به روش قبل متد اکشنی با نام add2 درست میکنیم. طبیعتا باید مسیر Command را طی کنیم. https://gist.github.com/f6ff3bc04f07ce49d930e832f8cfa695 برای سادگی از ولیدیشن ورودی متد صرف نظر میکنیم و همان مدلی که قرار است به mediatR بفرستیم را از ورودی میگیریم. (بهتر است تایپ ورودی اکشن به شکل ویومدل باشد و با آرگومان ورودی هندلر تفاوت داشته باشد).پیاده سازی این بخش در یک هندلر جدا به شکل زیر است که مسئول ثبت اطلاعات یا همان command است. https://gist.github.com/7e46946a543bdb626333a010f14b6030 دقت کنید که در این روش مثل حالت UseCase ظرفِ پرزنتر نداریم و میتوانیم برای ساختاردهی به خروجی های apiها، از متدهایی در BaseController استفاده کنیم؛ این روش را در مقاله پیاده سازی به کمک سرویس ها بررسی کردیم و به جای Return OK یا Return BadRequest از متد کاستوم خودمان استفاده کردیم که در اینجا نیز کاربرد دارد.با پیاده سازی دو متد Get و Post حالت کامند و کوئری را بررسی کردیم. داستان CQRS همیشه به این سادگی نیست و هدف از این مقاله و مقاله های قبلی فقط قدم گذاشتن در دنیای Clean Architecture بوده است.برای دیدن پروژه مربوط به این مقاله از طریق Github مخزن مرتبط را ببینید.