در این مقاله قصد داریم تا الگوی CQRS که مخفف Command and Query Responsibility Segregation یا همان "جداسازی مسوولیت های خواندن و نوشتن" می باشد را معرفی کنیم. این الگو امروزه به یکی از پرکاربردترین الگو ها در زمینه نرم افزار تبدیل شده است و اولین بار توسط آقای Greg Young مطرح شد.
فلسفه الگوی CQRS این است که به طور کلی اعمال اصلی که در هسته نرم افزار وجود دارند، یا باعث تغییر در داده های موجود می شوند (Command)، و یا باعث تغییر در هیچ نوع داده ای نمی شوند و صرفا مقادیر داده ها را می خوانند (Query). تاکید این الگو بر این امر است که اعمال باید از هم دیگر جدا باشند و به صورت تفکیک شده از هم کار کنند.
ابتدا با ذکر یک مثال به تعریف دقیق تر Command و Query می پردازیم:
فرض کنید یک مشتری قصد دارد تا فرآیند خرید یک کتاب را انجام بدهد. وی ابتدا باید وارد سایت شود. سپس لیست کتاب ها را ببیند. وقتی کاربر بر روی گزینه "مشاهده لیست کتاب ها" کلیک می کند، در پشت ماجرا یک کد اجرا می شود که لیست کتاب ها را از پایگاه داده خوانده و به کاربر باز می گرداند. به این نوع دستورات که تغییری در سیستم ایجاد نمی کند و مقداری را بر میگرداند Query گفته می شود.
هنگامی که وی قصد ثبت سفارش دارد، یک کتاب را به سبد خریدش اضافه می کند. با این کار یک رکورد در پایگاه داده اضافه می شود. پس پایگاه داده ی این سایت مشمول تغییراتی شده. به این نوع دستورات که تغییر در سیستم ایجاد می کند و مقداری را بر نمی گردانند Command گفته می شود.
از یک نگاه دیگر، 4 نوع دستور در پایگاه داده وجود دارد که به CRUD معرف است: Create (ایجاد یک رکورد)، Read (خواندن یک رکورد)، Update (به روز رسانی یک رکورد) و Delete (حذف یک رکورد). از بین این دستورات، Create و Update و Delete که باعث ایجاد یک تغییر در داده ها می شوند، در دسته Command قرار می گیرند. دستور Read هم در دسته Query قرار می گیرد:
شکل فوق دسته بندی دستورات پایگاه داده در الگوی CQRS را نشان می دهد.
الگوی CQRS بر این نکته تاکید دارد که به هیچ عنوان دو دسته عمل Command ها و Query ها نباید با هم دیگر قاطی شوند! این اتفاق در این مثالی که گفتیم رخ نمی دهد، اما ممکن است در بعضی شرایط ممکن است یک عمل هم شامل خواندن و هم شامل نوشتن باشد که در ادامه این مقاله خواهیم دید. الگوی CQRS تاکید دارد که این تابع باید به دو تا تابع خرد شود. همچنین مدل هایی که برای خواندن داده ها استفاده می شوند، باید با مدل هایی که برای نوشتن داده ها استفاده می شوند تفاوت داشته باشند.
شکل زیر کلیات این الگو را نشان می دهد:
همانطور که مشاهده می شود، در سمت چپ تصویر UI یا همان User Interface قرار دارد. User Interface به معنی رابط کاربری است، به عنوان مثال یک صفحه از وبسایت یا یک بخش از اپلیکیشن. آن چه مهم است این است که پشت رابط کاربری، یک کاربر نشسته و با سیستم کار می کند. اگر این کاربر صرفا بخواهد این صفحه را ببیند و یا صفحه ای از سایت را رفرش کند، دیدن او تغییری در پایگاه داده ی این سایت یا اپلیکیشن ایجاد نمی کند. این یعنی Query. اطلاعات بازگشتی نیز به کمک Query Model (قسمت صورتی رنگ عکس) از پایگاه داده خوانده شده و به وی در صفحه ی آن وبسیات نمایش داده می شود. اما همانطور که پیش تر گفته شد، ممکن است کاربر با سایت تعامل برقرار کند و مثلا بخواهد سفارشی در سایت بگذارد، در این صورت به کمک Command Model این تغییر در پایگاه داده (یا همان DB که با رنگ زرد رنگ در سمت راست تصویر مشخص شده است) ثبت می شود.
نکته دیگری که در عکس فوق وجود دارد این است که مدل هایی که مدل هایی که برای خواندن داده ها استفاده می شوند، با مدل هایی که برای نوشتن یا تغییر داده ها استفاده می شوند تفاوت دارند. اما قبل از به کارگیری الگوی CQRS این مدل ها با یکدیگر تفاوت نداشتند و یک مدل داشتیم.
در این شکل از یک پایگاه داده استفاده شده است، اما در نسخه های پیشرفته تر الگوی CQRS، ما دو عدد پایگاه داده داریم. یک پایگاه داده فقط مختص نوشتن و تغییرات داده است و یک پایگاه داده فقط برای خواندن داده ها است. یعنی ما نباید از پایگاه داده ای که برای نوشتن داده ها استفاده می شود داده ای بخوانیم، و همین طور نباید در پایگاه داده ای که برای خواندن داده ها استفاده می شود چیزی بنویسیم. جزییات این روش در تصویر زیر مشخص شده است:
همانطور که در شکل بالا مشخص است، یک Write DB داریم و یک Read DB. این دو پایگاه داده باید با هم دیگر sync باشند، یعنی داده های موجود در آنان باید دقیقا یکسان باشد. این در حالی است که ما فقط داده ها را در Write SB می نویسیم و نه Read DB، پس چگونه می شود که داده ها باید در هر دو پایگاه داده وجود داشته باشند؟ جواب این است که این امر به کمک Replication و Message Broker اتفاق می افتد که پیاده سازی آن آسان هم نیست. همانطور که مشخص است اگر کاربر قصد نوشتن داشته باشد، از سمت چپ شکل در Write DB داده اش را می نویسد. سپس Read DB هم از این نوشتن با خبر می شود و خود را به روز می کند. حال اگر کاربر قصد خواندن داده ای را داشته باشد از سمت راست تصویر داده را از Read DB میخواند و دیگر کاری با Write DB ندارد.
به این نکته توجه داشته باشید که جنی پایگاه داده های Write DB و Read DB نباید لزوما یکسان باشد و این خوبی این الگو است.
همچنین در شکل فوق یک Event Data Store وجود دارد که در الگوی Event Sourcing از آن استفاده می شود.
در ادامه مثالی از قطعه کد را می بینم که در یکی از آن ها از الگوی CQRS استفاده نشده است ولی در قطعه کد دیگر که ویرایش شده همان نسخه اول است، از این الگو استفاده شده است و CQRS بر آن اعمال شده است.
در قطعه کد اوله زیر از الگوی CQRS استفاده نشده است:
همانطور که مشاهده می شود، تمام توابع، چه توابعی که عمل خواندن انجام می هند و چه توابعی که عمل نوشتن انجام می دهند، در یک interface (واسط) قرار گرفته اند. (این یک عیب نیست، اما اگر از CQRS استفاده شود مزایای به همراه خواهد داشت که در ادامه به آن اشاره می کنیم)
در ادامه قطعه کد جدیدی را مشاهده می کنیم که همان قطعه کد قبلی است با این تفاوت که الگوی CQRS در آن اعمال شده است:
بعد از اینکه الگوی CQRS اعمال شد، یک interface به دو تا interface تبدیل شد (PolicyCommandService و PolicyQueryService).
در PolicyCommandService فقط توابعی که تغییر در پایگاه داده به وجود می آورند (Command) ها وجود دارند و اگر دقت کنید متوجه می شوید که هیچ کدام از آن ها مقداری را بر نمیگردانند. (چون void هستند)
در PolicyQueryService نیز فقط توابعی که از پایگاه داده اطلاعاتی را می خوانند و هیچ واکنش از خود بروز نمی دهند وجود دارند. تمام این توابع لیستی از مقادیر (یا یک مقدار خاص یک شی) را بر میگردانند.
یعنی مسوولیت توابع موجود در یک کلاس، باید یا نوشتن باشد، یاخواندن!
مثال دیگری در شکل های زیر آورده شده است که در آن یک قطعه کد وجود دارد که قصد دارد تا مقدار یک متغیر به نام x را 1 عدد زیاد کرده و سپس مقدار جدید آن را برگرداند:
همانطور که مشاهده می شود، در این تابع هم تغییر روی متغیر x داده شده و هم مقدار آن خوانده شده. الگوی CQRS این را قبول ندارد! پس از اعمال الگوی CQRS به روی قطعه کد فوق، به جای یک تابع، دو تابع خواهیم داشت.
یکی از از این توابع وظیفه اش این است که مقدار x را بخواند و دیگری وظیفه اش این است که مقدار x را زیاد کند:
بنابراین مسوولیت دو تابع تفکیک شده و یکی عملیات خواندن (Query) را دارد و دیگری مسوولیت اضافه کردن (Command) را دارد. در این قطعه کد اعمال Update و Read با هم دیگر درون یک تابع بودند، اما وقتی الگوی CQRS اعمال شد، این تابع به 2 عدد تابع خرد شد. تابع ()value تغییری در داده x ایجاد نمی کند و صرفا مقدار داده x را بر میگرداند. تابع ()increment مقداری برنمی گرداند اما تغییر در داده x ایجاد می کند (مقدار آن را یکی زیاد می کند) و این دقیقا مطابق همان تعریفی است که در ابتدای این مقاله داشتیم.
این الگو نیز مانند سایر الگو ها یک سری کاربرد و همچنین یک سری مزایا و معایب دارد. در ادامه به هر کدام از آن ها می پردازیم.
شاید این سوال برای شما نیز پیش آمده باشد که کاربرد این الگو چیست و اساسا چرا باید از این الگو استفاده شود؟ از جمله دلایلی که بسیاری از برنامه نویسان از این الگو استفاده می کنند می توان به موارد زیر اشاره کرد:
از آن طرف ماجرا، این الگو معایبی دارد که به آن اشاره می کنیم:
نکته مهم: استفاده از این الگو برای کسب و کار های که قواعد خیلی ساده ای دارند پیشنهاد نمی شود!
در راستای معایب الگوی CQRS، مارتین فاولر در نوشته ای می گوید:
مارتین فاولر: با وجود مزایای زیادی که CQRS دارد، باید هنگام استفاده از آن خیلی مراقب بود. به روز کردن داده ها در بسیاری از سیستم های اطلاعاتی با همان روش خواندن داده ها انجام می گیرد. افزودن CQRS به چنین سیستمی می تواند پیچیدگی قابل توجهی را به سیستم اضافه کند. من مواردی را دیدهام که در آن استفاده از الگوی CQRS ریسک غیرقابل توجیهی را به پروژه اضافه کرده است، حتی اگر آن پروژه در دست یک تیم قوی بوده باشد. بنابراین، در حالی که CQRS الگوی خوبی است، مراقب باشید که استفاده از آن دشوار است.
در واقع در برخی سیستم ها، در دستور Update ابتدا یک خواندن مقدار اولیه از آن متغیر داریم که این ممکن است کار را سخت کند، چون گفتیم که دستور Update باید از دستور Read جدا باشد، اما این ممکن نیست چرا که Read بخشی از Update است! این موضوع ممکن است نیازمندی قفل گزاری داده را به وجود بیارد که همین امر به پیچیدگی کد و سیستم اضافه خواهد کرد.
الگوی CQRS یک تکنولوژی نیست، بلکه صرفا یک فرهنگ و مفهوم است که استفاده از آن مزایا و معایبی به همراه دارد. استفاده از آن در هر جایی پیشنهاد نمی شود و اشخاصی که با تجربه تر هستند (مانند مدیران پروژه) باید تصمیم بگیرند که از این الگو استفاده بشود یا خیر. علاوه بر این موضوع، این الگو می تواند مکملی برای سایر الگو ها مانند Event Sourcing باشد. الگوی CQRS و Event Sourcing معمولا به همراه همدیگر استفاده می شوند و مزایای دیگری را برای اهداف دیگری به وجود می آورند. این الگو مختص زبان برنامه نویسی خاصی نیست و در تمام زبان های برنامه نویسی می توان از آن استفاده کرد. همچنین استفاده از این الگو در معماری میکروسرویس به همراه الگوی Event Sourcing بسیار رایج است.
[1] https://microservices.io/patterns/data/cqrs.html
[2] https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs
[3] https://www.redhat.com/architect/pros-and-cons-cqrs
[4] https://martinfowler.com/bliki/CQRS.html
[5] https://ravendb.net/articles/cqrs-and-event-sourcing-made-easy-with-ravendb
[6] https://dzone.com/articles/cqrs-and-event-sourcing-intro-for-developers
[7] https://en.wikipedia.org/wiki/Command%E2%80%93query_separation
[8] https://www.baeldung.com/cqrs-event-sourcing-java
[9] https://vladikk.com/2017/03/20/tackling-complexity-in-cqrs/
[10] https://www.redhat.com/architect/illustrated-cqrs
#معماری_نرم_افزار_بهشتی
این مطلب، بخشی از تمرینهای درس معماری نرمافزار در دانشگاه شهیدبهشتی است.