CQRS به زبان ساده

اگر در دنیای توسعۀ نرم‌افزارها فعالیت کرده باشید، حتماً گوشتان با عبارت «الگوهای طراحی» آشناست. الگوهای طراحی شامل روش‌هایی تست شده و بدست آمده از طریق تجربه هستند که به کمک آن‌ها می‌توان مسائل طراحی نرم‌افزارها را برطرف نمود. الگوهای طراحی را می‌توان با یک مثال به‌صورت ساده‌تر بیان کرد.مثلاً فرض کنید خیاطی می‌خواهد یک پیراهن بدوزد و ابعاد و اندازه‌ها را در اختیار دارد. اما برای دوختن یقه یا سرآستین و حتی قسمت‌های دیگر، پارچه باید با شرایط خاصی بُریده شود تا هم دورریز آن به حداقل برسد و هم دوخت لباس زیبا باشد. خیاط‌ها این کار را به کمک شابلون‌ها یا الگوهای کاغذی انجام می‌دهند که طی سال‌ها تجربه بدست آمده‌اند.البته در دنیای نرم‌افزارها، موضوع کمی پیچیده‌تر است. الگوهای طراحی یک قطعه کُد و یا حتی یک ماژول و الگوریتم نیستند که بتوان آن را کُپی و پِیست کرد؛ بلکه آن‌ها یک دید کلی و راه‌حل سطح بالا برای حل مسائلی خاص، با شرایط ویژه هستند. توسعه‌دهنده باید ابتدا مسئله و شرایط آن را شناسایی نموده و سپس با الهام گرفتن از این الگوها، نسبت به حل مشکل اقدام نماید.یکی از مسائلی که ذهن توسعه‌دهندگان را به خود مشغول می‌کند، نحوۀ انتقال داده‌ها و کانال‌های آن است. امروزه میلیون‌ها نرم‌افزار وجود دارند که در کانال‌های مختلف و به روش‌های گوناگونی تبادل اطلاعات می‌کنند. یکی از این روش‌ها، CQRS است که رفتار و عملکرد سیستم را به دو قسمت دستور (Command) و کوئِری (Query) تقسیم می‌کند. در ادامه به معرفی این روش خواهیم پرداخت.CQRS چیست؟قبل از اینکه بدانیم CQRS چیست، ابتدا باید با CQS آشنا شویم. CQS در واقع یک الگوی طراحی و مخفف عبارت Command Query Separation است که معادل فارسی آن برابر است با جداسازی دستور و کوئری. این الگوی طراحی، اولین بار توسط آقای بِرتِراند مایِر در کتاب ساخت نرم‌افزار شئ‌گرا معرفی شد که مطالعۀ آن خالی از لطف نیست. او این الگو را در جریان ساخت زبان برنامه‌نویسی ایفِل کشف نمود و در ابتدای صحبت‌هایش می‌گوید که «سوال پرسیدن نباید باعث ایجاد تغییر در پاسخ شود». ایدۀ اصلی پشت CQS آن است که در یک سیستم نرم‌افزاری، دو دسته رخداد وجود دارد: 1) یک دستور که وظیفۀ خاصی را اجرا می‌کند و 2) یک کوئری که اطلاعاتی را بازمی‌گرداند؛ و هیچ تابعی نباید این دو عمل را به‌صورت همزمان انجام دهد و به اصطلاح، این دو نوع رخداد از یکدیگر جدا باشند.با علم به این موضوع، عبارت CQRS (مخفف Command Query Responsibility Segregation) چنین رفتار سیستمی را به حالت کلی‌تر و در سطح معماری منتقل می‌کند. در واقع CQRS یک الگوی معماری نرم‌افزار است که در آن بر جداسازی مسئولیت اجرای دستورات و کوئری‌ها تاکید می‌شود. در حقیقت وقتی از الگوی CQRS استفاده می‌کنیم، منطق نرم‌افزار به‌صورت عمودی به دو قسمت تقسیم می‌شود: اولی مدیریت دستورات و دومی دستیابی به داده‌ها. در حقیقت CQRS را می‌توان یک توسعه از CQS دانست. آقای گِرِگ یانگ که یکی از صاحبنظران در این زمینه است و عبارت CQRS اولین بار توسط ایشان ابداع شده، در این مورد می‌گوید:CQRS همان تعاریف CQS که توسط مایر بیان شده را استفاده کرده و تمامی نماهای آن را حفظ می‌کند. تنها تفاوت این دو مفهوم، آن است که CQRS اشیاء را به دو قسمت تقسیم می‌کند: یکی شامل دستورات و دیگری شامل کوئری‌ها.حالا که با کلیات CQRS آشنا شدید، نوبت به جزئیات می‌رسد. ابتدا باید دانست که دستور و کوئری چه هستند؟دستور در واقع یک امر مستقیم برای اجرای یک وظیفۀ خاص است. بنابراین هر دستور باعث ایجاد تغییری در داده‌ها می‌شود که کاربر آن را درخواست کرده است. آقای مایر دستور را چنین تعریف می‌کند:یک دستور، عملیاتی را انجام می‌دهد اما هیچ چیزی باز نمی‌گرداند.تمامی دستوراتی که در نرم‌افزار وجود دارند، مدل ثبت داده‌ها را تشکیل می‌دهند که این مدل شباهت بسیار زیادی با فرایندهای کسب‌وکار آن سازمان دارد. مدل ثبت دارای دو بُعد است: بُعد فیزیکی شامل اینکه داده‌ها در کجا ذخیره می‌شوند و بُعد منطقی شامل دامنۀ کسب‌وکار که به‌صورت کُد درآمده است.از طرف دیگر کوئری یک درخواست برای جستجوی داده‌هاست. آقای مایر کوئری را چنین تعریف می‌کند:کوئری، کاری را انجام نمی‌دهد (تغییر حالت ایجاد نمی‌کند) ولی نتیجه بازمی‌گرداند.هر کوئری یک درخواست کاربر را بیان می‌کند که می‌خواهد قسمتی از داده‌های ثبت شدۀ قبلی را مشاهده کند. تمامی درخواست‌های از پیش تعریف شده در سیستم، مدل خواندن داده‌ها را تشکیل می‌دهند که این مدل معمولاً از روی مدل ثبت داده‌ها تشکیل می‌شود. البته یکسان بودن مدل‌های ثبت و خواندن الزامی نیست. مدل خواندن داده‌ها همیشه ثابت نیست و همواره می‌توان به‌راحتی و با توجه به نیازها آن را تغییر داد.شرکت مایکروسافت یک مستند در رابطه با CQRS منتشر کرده که مثال‌های زیادی در آن بیان شده‌اند و در اینجا به یکی از آن‌ها اشاره خواهد شد. شکل زیر نشان‌دهنده دو مدل ثبت و خواندن مربوط به یک مدل کلی فرضی است. مثلاً اگر سیستم رزرو پرواز هواپیما را درنظر بگیریم، یک دستور فرضی می‌تواند «رزرو کردن 2 بلیط برای هاوایی» باشد و از طرف دیگر یک کوئری فرضی می‌تواند «رزروهای دو هفتۀ بعدی من را نشانم بده» باشد.وقتی بحث چنین الگویی مطرح می‌شود، ذهن برنامه‌نویسان و توسعه‌دهندگان خودبه‌خود به سمت پایگاه داده‌ها می‌رود؛ اما باید بدانید که الگوی CQRS به هیچ‌وجه دربارۀ ذخیره‌سازی داده‌ها صحبت نمی‌کند. منبع داده می‌تواند یک فایل اکسل، فایل متنی و یا حتی پایگاه داده MySql باشد. مهم آن است که CQRS یک طرز فکر جدید را معرفی می‌کند و در آن مدل ثبت و بازیابی داده‌ها از یکدیگر مستقل هستند. چنین تفکری باعث ایجاد برش عمودی در طراحی سیستم می‌شود. بسیاری از توسعه‌دهندگان فکر می‌کنند که باید دو پایگاه داده برای ثبت و خواندن داده‌ها وجود داشته باشد؛ اما چرا؟ پاسخ آن ساده است؛ CQRS این توانایی را بوجود می‌آورد که مدل خواندن را برای مطابقت با نیازهای مشتری اصلاح کنیم و داشتن یک مدل خواندن اختصاصی برای هر نما، یک امر معمولی است. چنین مدل‌های خواندنی با کوئری‌های SQL کمی متفاوت هستند و طیف متفاوتی از ستون‌ها را از جدول انتخاب می‌کنند. اما آن‌ها همچنین می‌توانند نماها یا جداول جداگانه باشند. ممکن است توسعه‌دهنده این کار را برای بهینه‌سازی عملکرد انجام دهد که در اینصورت، یکی از راه‌حل‌های بالقوه استفاده از جداول یا پایگاه داده‌های مختلف و همگام‌سازی آنها پس از ثبت است. با این‌حال، این یک قانون کلی نیست. توسعه‌دهنده باید استراتژی خود را متناسب با نیاز خودش انتخاب کند.آقای جیمی بوگارد در یکی از پست‌های وبلاگ خود به نام معماری بُرش عمودی، معماری تمیز (یا پوست پیازی، لایه‌ای – که یک روش شناخته شده و معروف است) را با معماری بُرش عمودی مقایسه می‌کند. شکل زیر نشان‌دهندۀ این دو معماری است.در معماری لایه‌ای، زمانی‌که توسعه‌دهنده بخواهد روی هستۀ اصلی یک لایه تغییر ایجاد کند، ممکن است به لایه‌های بعدی و حتی توابع وابسته آسیب بزند. چنین امری ریسک ایجاد تغییرات را بالا برده و در نتیجه رغبت وی برای ایجاد تغییرات بزرگ را از بین می‌برد. اما در معماری بُرش عمودی، مدل و API‌ها همگی به‌صورت عمودی بُرش می‌خورند و بنابراین هر گرداننده (Handler) در یک محیط مجزا قرار می‌گیرد. نتیجۀ چنین معماری، آن است که کوپلینگ (Coupling) میان لایه‌های مختلف کاهش می‌یابد.اما سوال دیگری در ذهن بوجود می‌آید که اگر توسعه‌دهنده بخواهد منبع ذخیره‌سازی داده‌های ثبت شدنی و خواندنی را از یکدیگر جدا کند چه اتفاقی می‌افتد؟ همانطور که در پاراگراف‌های بالاتر بیان شد، ایدۀ CQRS جداسازی منبع ثبت و منبع خواندن اطلاعات است؛ اما CQRS زمانی مفید واقع می‌شود که میان داده‌های ثبت شدنی و خواندنی هماهنگی وجود داشته باشد. اگر اطلاعاتی که ثبت می‌شوند با اطلاعاتی که به کاربران ارسال می‌شوند هماهنگ نباشد، یعنی برنامه وارد حالتی از خطا می‌شود که مطلوب نیست؛ درصورتیکه همۀ این تلاش‌ها برای بهبود عملکرد نرم‌افزار است.پاسخ این سوال که چگونه می‌توان میان منابع مختلف داده‌ها هماهنگی ایجاد کرد، قبلاً پاسخ داده شده است. در مورد CQRS باید به این مسئله بپردازیم که چگونه می‌توان این کار را بهتر انجام داد؟ روش حفظ تداوم داده مبتنی بر رویداد (Event-driven data persistence) تکنیکی است که در آن رویدادها مطابق با هدف یک برنامه خاص مطرح می‌شوند و داده‌های مرتبط با رویداد مورد نظر در اختیار افراد ذینفع قرار می‌گیرند. البته توضیح کامل این مسئله خارج از حوصلۀ مطلب فعلی است.در شکل زیر یک معماری داده را نشان داده شده است که CQRS را با استفاده از الگوی واسطه (Mediator) پیاده‌سازی می‌کند. اولین چیزی که باید به آن توجه کرد این است که همه درخواست‌ها، هم دستورات ثبت و هم کوئری برای خواندن داده‌ها، از وب سرور برنامه به مؤلفه‌ای به نام Mediator منتقل می‌شوند. هدف Mediator تشخیص درخواست‌های ثبت و خواندن و سپس مسیریابی درخواست بر اساس آن است. اگر درخواست یک دستور ثبت باشد، آن داده به مؤلفه‌ای به نام WriteDataManager ارسال می‌شود. اگر درخواست یک کوئری باشد، از مؤلفه‌ای به نامReadDataManager بازیابی می‌شود. در این مرحله، هر دو داده که توسط WriteDataManager و ReadDataManager نشان داده شده‌اند، سازگار هستند. بنابراین، هدف CQRS به گونه‌ای محقق شده است که کوپلینگ آن ضعیف بوده و در عین حال قابل اعتماد و تأیید بین منابع داده است.چه زمانی از CQRS استفاده کنیم؟همانند سایر الگوهای طراحی دیگر، CQRS هم مزایا و معایب مخصوص به خود را دارد. شاید بهتر باشد که برای پاسخ به این سوال، از صاحبنظران این زمینه استفاده کرد. آقای مارتین فاولِر در یکی از پست‌های بلاگ خود (که پیشنهاد می‌شود آن را مطالعه کنید) به این سوال چنین پاسخ می‌دهد که استفاده از CQRS یک چالش بزرگ است و فقط درصورتیکه مزایای آن بیشتر از معایبش باشد، باید از CQRS استفاده نمود. مهم است که بدانیم الگوی CQRS مثل چراغ جادو نیست و نمی‌تواند جوابی برای تمام مشکلات باشد. طبق نظر بسیاری از متخصصین، می‌توان از الگوی CQRS در شرایط زیر استفاده کرد:زمانی‌که نیازمندی‌های کسب‌وکار دائماً در حال تغییر هستند.مشخص نیست که کسب‌وکار دقیقاً به چه سمت و سویی می‌رود.پروژه دارای مشکلات مقیاس‌پذیری است.تیم پروژه با سایر تیم‌های خارجی ارتباط دارد.چندین سرویس با یکدیگر برای تغییر یک منبع رقابت دارند.نرم‌افزار فقط وظیفه ثبت را به‌عهده دارد و سیستم‌های خارجی نیاز به خواندن داده‌های نرم‌افزار دارند.به‌صورت کلی مزایای CQRS را به‌صورت زیر خلاصه نمود:استفاده یک از مدل برای ثبت و یک مدل دیگر برای خواندن، به توسعه‌دهنده اجازه می‌دهد تا تکنولوژی مناسب را برای هر عملیات لحاظ کند. مثلاً از SQL برای ذخیرۀ داده‌ها و NoSQL برای بازیابی داده‌ها استفاده کند.دفعات انجام عملیات خواندن اطلاعات بسیار بیشتر از ثبت آن‌هاست و بنابراین می‌توان با جایگذاری منابع داده در موقعیت‌های جغرافیایی مناسب، زمان تاخیر را کاهش داد.جداسازی عملیات ثبت و خواندن از یکدیگر باعث مقیاس‌پذیری بهتر استفاده از ظرفیت ذخیره‌سازی داده‌ها براساس استفادۀ واقعی می‌شود.همچنین معایب CQRS عبارتست از:پشتیبانی از CQRS نیازمند تخصص بالا در زمینۀ انواع پایگاه‌های داده است.استفاده از CQRS به معنای استفاده از پایگاه‌های دادۀ بیشتری است که در واقع برابر با هزینۀ بیشتر است.استفاده از تعداد پایگاه داده‌های متعدد برابرست با نقاط خرابی بیشتر. بنابراین سازمان‌ها باید یک تیم مجزا برای نظارت بر نقاط حساس تشکیل دهند و جدا از هماهنگی‌ها و تعاریف وظایف این تیم، هزینۀ آن هم مطرح است.با وجود این مزایا، باید در استفاده از CQRS بسیار محتاط بود زیرا بسیاری از سیستم‌های اطلاعاتی با مفهوم پایگاه اطلاعاتی که به همان روشی که خوانده می‌شود به‌روزرسانی می‌شود، به‌خوبی مطابقت دارند، افزودن CQRS به چنین سیستمی می‌تواند پیچیدگی قابل توجهی را اضافه کند. حتی اگر یک تیم توانا و پرتجربه هم در پروژه بکار گرفته شده باشند، ممکن است تاثیر روی کارایی و افزایش ریسک بسیار بالا باشد. استفاده کردن از CQRS همانند داشتن یک جعبه ابزار خوب است، اما باید مراقب بود که استفادۀ مناسب از آن دشوار است و در صورت سوء استفاده، براحتی قطعات مهم از بین خواهند رفت.سخن پایانیالگوی CQRS روشی برای جداسازی رفتار ثبت و رفتار خواندن داده‌ها از یکدیگر است. این روش مناسب سیستم‌هایی است که با تعداد درخواست‌های بسیار زیادی روی پایگاه‌های دادۀ خود مواجه هستند و برای حل مشکل آن حاضرند هزینه پرداخت نمایند. اثبات شده است که CQRS در شرایط مناسب، باعث افزایش کارایی سیستم‌ها می‌شود و آن‌ها را قادر می‌سازد تا مقیاس‌پذیری بسیار بهتری داشته باشند.البته باید گفت که هرچقدر CQRS قدرتمند است، استفاده از آن آسان نیست و نیاز به نیروی متخصص دارد. جداسازی مدل‌های مختلف از یکدیگر به این معنی خواهد بود که همواره یک ریسک برای ایجاد شدن ناهماهنگی میان داده‌ها وجود خواهد داشت. بنابراین شرایط سیستم، هزینه‌ها و نیروی انسانی همگی باید با استفاده از این الگو سنجیده شوند.این مطلب به‌عنوان پاسخ برای بخشی از تمرین‌های درس معماری نرم‌افزار در دانشگاه شهید بهشتی نوشته شده است که امیدوارم از آن استفاده برده باشید.مراجع[1] Bogard, J. (2018, April 19). Vertical Slice Architecture. Jimmy Bogard. https://jimmybogard.com/vertical-slice-architecture/ [2] CQRS facts and myths explained - Event-Driven.io. (2021). Lazywill. https://event-driven.io/en/cqrs_facts_and_myths_explained/ [3] Derosiaux, S. (2021, July 11). CQRS: What? Why? How? - Stéphane Derosiaux. Medium. https://sderosiaux.medium.com/cqrs-what-why-how-945543482313 [4] Fowler, M. (2011). bliki: CQRS. Martinfowler.Com. https://martinfowler.com/bliki/CQRS.html [5] Ltd, E. S. (2021). Beginner’s Guide to Event Sourcing | Event Store. Event Store. https://www.eventstore.com/event-sourcing [6] Meyer, B. (1997). Object-oriented Software Construction. Prentice Hall.[7] R. (2021a, October 15). An illustrated guide to CQRS data patterns. Enable Architect. https://www.redhat.com/architect/illustrated-cqrs [8] R. (2021b, October 15). The pros and cons of the CQRS architecture pattern. Enable Architect. https://www.redhat.com/architect/pros-and-cons-cqrs