منبع فارسی برای یادگیری معماری رویدادمحور و ابزارهای آن به زبان ساده و قابلفهم برای انسانها
مقدمهای بر معماری رویدادمحور (Event-Driven Architectures) معماری Event-Driven یک الگوی بنیادی برای طراحی ارتباط بین اجزای یک نرمافزار است که بر پایهی تبادل asynchronous پیامهایی به نام eventها بنا شده است. در این معماری، اجزای مختلف برنامه میتوانند به صورت پراکنده (distributed) و با اتصال ضعیف (loosely coupled) کنار هم فعالیت کنند بیآنکه مستقیماً به هم وابسته باشند. احتمالاً شناختهشدهترین نمونهی این سبک امروزه همان microservices architecture است: جایی که سیستم از چندین سرویس کوچک ساخته شده که از طریق event با هم ارتباط برقرار میکنند. جهان ما از رویدادها ساخته شده؛ آنها همهجا هستند. از لحظهای که صبح از خواب بیدار میشویم، رویدادی شکل گرفته است. خریدن یک کتاب هم یک رویداد است، چه در پایگاهدادهای ثبت شده باشد، چه نه. همین که اتفاق افتاده، میتواند منجر به چندین واکنش دیگر شود: از ارسال پیام تا صدور فاکتور یا بهروزرسانی وضعیت سفارش. همانطور که شرکتها در دههی گذشته به سراغ microservices رفتند تا چالشهایی مثل مقیاسپذیری وب (web-scale) را حل کنند، امروزه هم علاقه به EDA بیشتر شده تا ما را به سطح مقیاس جهانی (global-scale) برساند. هدف این فصل آن است که شما را با مفاهیم، اجزا و کاربردهای EDA آشنا کند؛ همان اجزایی که در طول این کتاب، از آنها برای ساختن یک برنامهی نمونه استفاده خواهیم کرد. همچنین نگاهی خواهیم داشت به مزایای استفاده از این معماری، دلایل انتخاب آن و چالشهایی که در مسیر پیادهسازی آن چه در پروژهای کاملاً جدید (greenfield)، چه هنگام افزودن به پروژهای موجود با آن روبهرو خواهید شد. اگر قصد دارید پروژهای را از ابتدا با دیدگاه event-driven آغاز کنید یا یک نرمافزار monolith را به بخشهای کوچکتر یا حتی microservices تقسیم کنید، این کتاب به شما الگوها و اطلاعات لازم را برای پیادهسازی EDA خواهد داد.
سه الگوی مختلف وجود دارد که هر کدام بهتنهایی یا در کنار یکدیگر، معماری Event-Driven را شکل میدهند. این سه الگو عبارتاند از:
در این کتاب، بهطور جداگانه به هر یک از این الگوها خواهیم پرداخت، موارد استفادهی آنها را بررسی میکنیم، و خواهیم دید در چه زمانی استفاده از آنها مفید است و چه زمانی شاید مناسب نباشد.
در این الگو، eventها صرفاً به منظور اطلاعرسانی منتشر میشوند تا به سایر اجزای سیستم اعلام کنند اتفاقی رخ داده است. این نوع event معمولاً کمترین میزان داده را در خود دارد گاهی فقط شامل یک شناسه (ID) یا زمان وقوع رویداد است.
اجزایی که این اعلانها را دریافت میکنند، آزاد هستند تا تصمیم بگیرند آیا واکنشی نشان دهند یا نه. ممکن است event برای اهداف audit محلی ذخیره شود یا سرویس دریافتکننده، با سرویس اصلی تماس بگیرد تا اطلاعات بیشتری دربارهی event بگیرد.
نمونهای از event notification در Go:
type PaymentReceived struct {
PaymentID string
}
در اینجا، فقط شناسهی پرداخت منتقل شده است. حالا فرض کنید ServiceA فقط همین اعلان را دریافت میکند و کار خاصی انجام نمیدهد، در حالی که ServiceB باید اطلاعات بیشتری دریافت کند، پس درخواست دیگری به سرویس پرداخت میفرستد.
این الگو در واقع نسخهی asynchronous از مدل REST است. برخلاف REST که دادهها را هنگام نیاز از سرویس درخواست میکنیم (pull)، در اینجا دادهها push میشوند یعنی وقتی تغییری رخ داد، آن تغییر به سایر اجزا فرستاده میشود.
این کار باعث میشود هر سرویس بتواند نسخهی محلی خود از داده را بسازد و دیگر نیازی به ارتباط با سرویس مبدا نداشته باشد.
نمونهای از event-carried state transfer:
type PaymentReceived struct {
PaymentID string
CustomerID string
OrderID string
Amount int
}
در اینجا، event شامل جزئیات بیشتری است: مشتری، سفارش و مبلغ پرداخت. بنابراین، دیگر لازم نیست ServiceB برای دریافت اطلاعات به سرویس پرداخت مراجعه کند؛ همهی آنچه نیاز دارد همینجاست.
در این روش، بهجای ثبت مستقیم تغییرات در رکوردهای نهایی، همهی تغییرات بهعنوان یک event ذخیره میشوند. این eventها در یک event store ذخیره شده و میتوانند بعداً برای بازسازی وضعیت نهایی یک entity استفاده شوند.
یعنی اگر بخواهیم بدانیم وضعیت نهایی یک پرداخت چیست، فقط کافیست همهی eventهای مربوط به آن را بخوانیم و بهترتیب آنها را fold کنیم تا به نتیجهی نهایی برسیم.
برخلاف دو الگوی قبلی که eventها را برای ارتباط بین سرویسها میفرستند، در event sourcing هدف اصلی نگهداری تغییرات است، نه انتقال آنها.
در قلب همهی الگوهای Event-Driven Architecture، چهار مؤلفهی کلیدی دیده میشوند:
در ادامه بهصورت جداگانه به توضیح هرکدام میپردازیم:
Event همان چیزیست که به خاطرش این معماری شکل گرفته: یک اتفاق واقعی که در سیستم رخ داده و حالا باید اطلاعرسانی یا ثبت شود.
در جهان EDA، یک event نمایندهی یک حقیقت غیرقابل تغییر (immutable fact) از گذشته است. مثلاً:
یک مشتری در سرویس ما ثبتنام کرده یک پرداخت انجام شده یا تلاش ناموفقی برای ورود به حساب کاربری صورت گرفته جالب است که consumerهای این رویداد ممکن است هیچ شناختی از منبع آن نداشته باشند. آنها فقط با خود event کار دارند، نه با تولیدکنندهی آن.
در زبانهایی مثل Go، معمولاً eventها با یک ساختار ساده (struct) نمایش داده میشوند. مثلاً:
type PaymentReceived struct {
PaymentID string
OrderID string
Amount int
}
به Queue ممکن است بسته به تکنولوژی یا کاربرد، نامهای مختلفی داده شود: bus، channel، stream، topic و… همگی اصطلاحاتی برای اشاره به چیزی مشابه هستند. در این متن، از واژهی queue برای اشاره به مکانی استفاده میشود که eventها در آن صف میگیرند و مصرف میشوند. حالا خود queue میتواند دو نوع کلی داشته باشد:
در این نوع، eventها فقط برای مدت محدودی نگهداری میشوند. اگر مصرف نشوند یا زمانشان بگذرد، پاک میشوند. به درد سناریوهایی میخورد که subscriberها باید بلافاصله رویداد را دریافت کنند و سیستم سادهای از publish-subscribe کافی است.
مثلاً یک هشدار کوتاهمدت برای یک تراکنش بانکی.
برخلاف message queue، در اینجا رویدادها نگهداری میشوند و میتوان بارها به آنها دسترسی داشت. consumerها میتوانند از ابتدا، از نقطهای خاص، یا از همین حالا شروع به خواندن eventها کنند.
مثلاً برای audit یا تجزیه و تحلیل رفتار کاربران، نیاز داریم همهی رویدادها را نگه داریم و حتی دوباره پخش (replay) کنیم.
یک event store چیزی فراتر از یک صف پیام است. اینجا قرار نیست فقط پیامها ارسال شوند، بلکه تمام تغییرات بهعنوان یک تاریخچهی دائم (append-only) نگهداری میشوند.
در یک event store، میلیونها event stream مختلف میتواند وجود داشته باشد. این سیستم معمولاً برای ارتباط بین سرویسها استفاده نمیشود، بلکه در کنار event sourcing میآید تا وضعیت موجودیتها (entities) را با بازسازی جریان eventها استخراج کنیم.
مثلاً وضعیت نهایی یک سفارش را میتوان با خواندن همهی رویدادهای مربوط به آن از event store به دست آورد.
هر زمان که تغییری در وضعیت سیستم رخ میدهد، producer یک event منتشر میکند. او اطلاعی ندارد که چه کسی قرار است این رویداد را دریافت کند فقط آن را میفرستد، شبیه یک پیام fire-and-forget.
مثلاً سرویسی که وقتی یک سفارش ثبت شد، یک event با اطلاعات سفارش به صف میفرستد.
Consumerها مشترک صفها میشوند و eventها را دریافت میکنند. ممکن است به صورت گروهی کار کنند تا بار پردازش بین آنها تقسیم شود یا به صورت مستقل همهی eventها را پردازش کنند.
در حالت stream، میتوانند از هر نقطهای شروع به خواندن کنند از ابتدا، از زمان فعلی یا ادامهی آخرین خواندهشده.
تا اینجا، سفری مفهومی در جهان معماری Event-Driven داشتیم. دانستیم که در دنیایی که پر از رویداد است، طراحی سیستمهایی که بتوانند با این واقعیت همسو شوند، نه تنها ممکن، بلکه الزامیست.
سه الگوی اصلی event notification، event-carried state transfer و event sourcing همچون سه شیوهی روایت یک داستان هستند: یکی با اشارهای کوتاه، دیگری با جزئیات کامل، و سومی با بازسازی همهی لحظات گذشته.
چه بخواهیم سامانهای را از ابتدا با این نگرش بنا کنیم، یا بخشی از سیستمی موجود را به سوی معماری رویدادمحور هدایت کنیم، باید ابتدا زبان این معماری را بفهمیم: زبان eventها، queueها، producerها و consumerها.
اکنون که با اجزای بنیادین این معماری آشنا شدیم، میتوانیم در ادامه، به بررسی نحوهی بهرهبرداری عملی از آنها بپردازیم از مزایا و انعطافپذیری گرفته تا چالشهایی که ناگزیر در مسیر خواهیم دید.
EDA فقط یک تکنولوژی نیست، بلکه یک طرز فکر است: نگاهی به سامانهها نه بهعنوان یک سازهی ایستا، بلکه بهمثابه جریانی زنده از رویدادها که هر لحظه در حال دگرگونیاند.
معماری Event-Driven در مقایسه با معماریهای مبتنی بر ارتباطات synchronous یا point-to-point (P2P) مزایای متعددی به همراه دارد. در این بخش، مهمترین آنها را مرور میکنیم:
در معماری P2P، یک کامپوننت (مثلاً سرویس Orders) برای عملکرد صحیح خود وابسته به در دسترس بودن سرویس مقصد (مثلاً Depot) است. اگر سرویس مقصد بهموقع پاسخ ندهد یا دچار خطا شود، این اشکال به مبدأ برمیگردد و در زنجیرهای از وابستگیها ممکن است کل عملیات شکست بخورد.
در مقابل، در معماری Event-Driven، اجزا بهصورت loosely coupled طراحی میشوند و ارتباط آنها از طریق یک event broker صورت میگیرد. در این مدل، اگر یک consumer دچار مشکل شود یا حتی کرش کند، تأثیری بر producer نخواهد داشت. پردازش فقط با تأخیر مواجه میشود، نه با شکست.
حتی اگر سرویس Depot به طور کامل غیرفعال شود، سیستم همچنان میتواند سفارشها را دریافت کند و پس از بازیابی Depot، پردازش آنها را ادامه دهد. این یعنی پایداری سیستم در برابر خطاها یا افزایش فشار کاری، بهمراتب بیشتر است.
یکی دیگر از مزایای کلیدی Event-Driven، امکان توسعهی چابکتر است.
در این معماری، تیمهای مختلف میتوانند بدون نیاز به هماهنگی زیاد با سایر تیمها، کامپوننت جدیدی را به سیستم اضافه کنند. برای مثال، یک تیم کوچک میتواند سرویسی جدید را تنها با اتصال به event stream راهاندازی کرده و به دادههای مورد نیاز خود دسترسی داشته باشد بدون اینکه نیاز باشد سایر سرویسها را تغییر دهند یا API جدید تعریف کنند.
حتی اگر نیاز به حذف این سرویس باشد، این کار بدون اختلال در اجزای دیگر انجامپذیر است. این قابلیت، به سازمان اجازه میدهد تا با سرعت بیشتر آزمایش کند، توسعه دهد و تصمیم بگیرد.
در دنیای امروزی با رشد IoT و تلفنهای همراه، کاربران انتظار دارند بهمحض وقوع یک رویداد مثل ثبت سفارش یا تغییر وضعیت حمل مطلع شوند.
معماری Event-Driven، بهطور ذاتی بر پایهی ارسال notification ساخته شده و آمادگی دارد تا همین پیامها را مستقیماً به کاربران نیز منتقل کند. این موضوع در بهبود رضایت کاربر و پاسخدهی سریعتر سیستم مؤثر است، چیزی که در معماریهای سنتی synchronous-first بسیار دشوارتر خواهد بود.
هر سه الگوی اصلی در Event-Driven شامل event notification، event-carried state transfer و event sourcing امکان نظارت دقیق بر ریزترین تغییرات سیستم را فراهم میکنند.
همچنین، دادههای ایجادشده در این مدل، پایهای مناسب برای توسعهی ماژولهای analytics و استخراج بینشهای تجاری (Business Intelligence یا BI) فراهم میکند. در معماریهای سنتی، معمولاً دادههای کافی برای تحلیل رفتار کاربران یا روندهای بازار وجود ندارد یا تنها میتوان تصویر ناقصی از گذشته را بازسازی کرد.
پیادهسازی الگوهای Event-Driven در یک برنامه، علاوه بر مزایای متعدد، با چالشهایی نیز همراه است که برای موفقیت پروژه، باید به آنها توجه و راهحلهایی برایشان تعریف کرد.
در هر برنامهی توزیعشده، eventual consistency یک چالش ذاتی است. تغییراتی که در وضعیت سیستم رخ میدهند، ممکن است بلافاصله در همهجا در دسترس نباشند. در نتیجه، کوئریها ممکن است موقتاً دادههایی منسوخشده (stale) بازگردانند، تا زمانی که تمام تغییرات بهطور کامل پردازش و ثبت شوند.
در هر معماری asynchronous، این مسئله وجود دارد؛ اما در EDA، این واقعیت اجتنابناپذیر است.
چالش dual write، صرفاً محدود به EDA نیست، اما در آن بارزتر میشود. وقتی در طول یک عملیات، وضعیت برنامه در بیش از یک محل تغییر میکند (مثلاً هم در پایگاه داده، هم در ارسال یک event)، احتمال ناسازگاری افزایش مییابد.
در EDA، معمولاً دادهها در دیتابیس ثبت میشوند و همزمان یک event تولید و به event broker فرستاده میشود. اگر ارسال event با خطا مواجه شود، سیستم دچار ناهماهنگی میشود: داده در پایگاه داده هست، ولی event مربوطه هیچوقت به بقیهی اجزا نمیرسد.
راهحل این مشکل استفاده از Outbox pattern است: ثبت event مورد نظر بهعنوان یک رکورد در کنار سایر تغییرات دیتابیس (در همان تراکنش)، تا حالت atomicity حفظ شود. در مراحل بعد، این event از Outbox خوانده شده و به صورت ایمن به broker منتشر میشود.
توضیحات بیشتر دربارهی این الگو در فصل های آینده (Asynchronous Connections) مطالب بیشتر ارائه خواهد شد. ۳. فرایندهای توزیعشده و غیربلادرنگ (Distributed and Asynchronous Workflows) در یک معماری Event-Driven، فرایندها اغلب بهصورت workflow بین چندین کامپوننت مستقل اجرا میشوند و تماماً asynchronous هستند.
این سبک اجرا منجر به eventual consistency میشود، چراکه هیچ کامپوننتی بهتنهایی وضعیت نهایی کل سیستم را در اختیار ندارد. این موضوع دو چالش به همراه دارد:
ماهیت asynchronous باعث میشود نتوان به کاربر پاسخ بیدرنگ داد. برای حل این مشکل میتوان از:
polling در سمت client WebSocket برای پاسخ غیربلادرنگ یا آموزش کاربر برای "بازگشت در آینده" و مشاهدهی نتیجه استفاده کرد.
برای اجرای workflowهای پیچیده بین اجزا، معمولاً یکی از دو الگوی زیر استفاده میشود:
این موضوع بهصورت کامل در فصل های بعد (Message Workflows) بررسی خواهد شد.
در معماری P2P یا synchronous، همیشه میدانیم چه کسی چه چیزی را فراخوانی کرده. مثلاً میتوان یک request ID یکتا از ابتدا تا انتهای زنجیرهی درخواست دنبال کرد.
اما در EDA، ممکن است یک event منتشر شود بدون اینکه دقیقاً بدانیم چه کسی آن را مصرف میکند یا چه عملیاتی در پاسخ به آن اجرا میشود. همچنین، ممکن است یک event باعث چندین عملیات مختلف و غیرمرتبط شود که بازگشت به منشأ اولیه را دشوار میکند.
راهحل این چالش، توسعهی همان راهحلهای tracing است که در معماریهای P2P استفاده میشود با اعمال تکنیکهای جدید. در اینباره در فصل های بعد (Monitoring and Observability) بیشتر صحبت خواهیم کرد.
درک صحیح EDA به تفکری متفاوت نیاز دارد. تیمها باید برنامه را از منظر رفتارها و رویدادها تحلیل کنند، نه فقط عملیات و APIها.
این یعنی دیدن تغییرات ریز اما مهمی که ارزش event شدن دارند، و برنامهریزی برای آنها.
در های بعد (Supporting Patterns in Brief) و (Design and Planning) ابزارهایی معرفی میشوند که به تیمها برای طراحی دقیقتر و قابل نگهداریتر کمک میکنند. ۶. خطر تبدیل سیستم به Big Ball of Mud حتی با استفاده از event هم میتوان دچار Big Ball of Mud (BBoM) شد یعنی سیستمی بیساختار و درهم. اگر رفتارها و رویدادها بهدرستی تفکیک و شناسایی نشوند، نتیجه چیزی جز آشوب توزیعشده نخواهد بود.
الگوهای نرمافزاری زیادی وجود دارند که ممکن است در توسعهی یک برنامهی event-driven از آنها استفاده کنیم یا با آنها مواجه شویم. معماری Event-Driven نباید اولین ابزاری باشد که در جعبهابزار خود به سراغش میروید.
ما قبلاً با معماریهای event-driven آشنا شدهایم، و حالا الگوهایی را خواهیم دید که در کنار EDA به طراحی و توسعهی برنامههایی با کیفیت بالا کمک میکنند. این الگوهای مفید ممکن است همیشه موفق نباشند، اما استفاده از آنها در مکانهای درست و بهصورت متعادل، زمان تولید را کاهش داده و نرخ خطاها را پایین میآورد.
در این فصل، به موضوعات اصلی زیر خواهیم پرداخت:
- Domain-driven design
- Domain-centric architectures
- Command and Query Responsibility Segregation
- Application architectures
طراحی دامنهمحور (DDD) موضوعی بسیار گسترده و پیچیده است که کتابهای کاملی به استفاده و پیادهسازی الگوها و روششناسیهای مختلف آن اختصاص داده شده است. در این فصل نمیخواهیم همهی آن را توضیح دهیم، چه برسد به این بخش، بلکه نگاهی سطحبالا به الگوهای استراتژیک کلیدی خواهیم داشت که هنگام طراحی و توسعهی برنامههای event-driven برای ما مفید هستند.
در مورد الگوهای تاکتیکی، در ادامه مثالهایی از کاربرد آنها را خواهیم دید.
فلسفهها، متدولوژیها و الگوهای DDD برای توسعهی برنامههای event-driven بسیار مناسب هستند. قبل از پرداختن به DDD، میخواهم چند برداشت نادرست را که ممکن است توسعهدهندگان داشته باشند، مطرح کنم:
برای بسیاری از توسعهدهندگان، اولین مواجهه با DDD ممکن است در قالب مشاهدهی یک entity، value object یا الگویی مثل repository در یک کدبیس باشد، یا از طریق یک آموزش وب که یکی دو الگو را توضیح میدهد. اما صرفنظر از تعداد الگوهایی که دیده میشوند، این تصویر کاملی از DDD نیست. بخش بزرگی از DDD هرگز بهطور مستقیم در کد دیده نمیشود، و مقدار زیادی از DDD پیش از نوشتن حتی یک خط کد وارد فرایند میشود.
DDD هیچ معماری خاصی را تجویز نمیکند و به شما نمیگوید که چطور کدتان را در یک زبان برنامهنویسی خاص سازماندهی کنید یا الزامی برای استفاده از یک ساختار خاص ندارد. DDD شما یا تیمتان را مجبور به استفاده از یک معماری یا الگوی خاص نمیکند این چیزیست که شما انجام میدهید. الگوهای استراتژیک DDD در واقع به شما کمک میکنند تا بخشهایی از دامنهی مسئله را شناسایی کنید که نیازی به صرف زمان و منابع زیاد توسعه ندارند.
هر دو برداشت نادرست، حول استفاده و برداشت نادرست از الگوهای تاکتیکی DDD میچرخند. ما بهعنوان توسعهدهندگان، افرادی با ذهنیت فنی هستیم؛ در مواجهه با یک مسئلهی جدید یا چالشبرانگیز، بهدنبال یک راهحل فنی یا راه بهتری برای انجام آن میگردیم. چیزی که یاد گرفتهایم یا استفاده کردهایم، وارد مکالمات ما میشود مخصوصاً زمانی که اسم الگوها را میآوریم.
اگر تنها چیزی که میجوییم یا به اشتراک میگذاریم، الگوهای تاکتیکی DDD باشد، ناگزیر بخش طراحی و الگوهای استراتژیک را از دست میدهیم و بعد از آن، ممکن است گلایه کنیم که DDD یک پروژهی دیگر را هم نابود کرده است.
DDD دربارهی مدلسازی یک ایدهی تجاری پیچیده در قالب نرمافزار است از طریق توسعهی یک درک عمیق از دامنهی مسئله. این درک سپس برای شکستن مسئله به بخشهای کوچکتر و قابلمدیریتتر استفاده میشود.
دو الگوی کلیدی DDD که در اینجا نقش دارند:
Ubiquitous Language Bounded Contexts
برای موفقیت در DDD، باید همکاری میان کارشناسان دامنه (domain experts) و توسعهدهندگان وجود داشته باشد. باید جلساتی برگزار شود که در آن مفاهیم و ایدههای تجاری ترسیم و نمودار شوند، از بالا تا پایین بررسی شوند و بهطور کامل مورد بحث قرار گیرند. نتایج این گفتوگوها سپس مدلسازی میشوند و دوباره بررسی میشوند تا برداشتهای نادرست از جزئیات ضمنی، شناسایی و حذف شوند.
این یک فرایند یکباره قبل از شروع کدنویسی نیست. سیستمهای پیچیده موجودیتهایی زنده هستند آنها تغییر میکنند و تکامل مییابند. وقتی ویژگیهای جدیدی در حال بررسی هستند، همان افراد باید دوباره جلسه داشته باشند تا این تغییرات را در مدل دامنه وارد کنند.
وقتی کارشناسان دامنه با توسعهدهندگان به گفتوگو مینشینند، ممکن است مکالمات به بنبست برسد اگر دو طرف نتوانند درک مشترکی از مفاهیم داشته باشند.
اصل Ubiquitous Language (UL) ایجاب میکند که هر اصطلاح دامنهای، تنها یک معنی مشخص در یک Bounded Context داشته باشد.
با استفاده از یک زبان مشترک، درک عمیقتری از دامنه شکل میگیرد. کارشناسان دامنه زبان تخصصی خودشان را دارند و توسعهدهندگان هم همینطور. ترجیح داده میشود از اصطلاحاتی استفاده شود که کارشناسان دامنه بیان میکنند و این اصطلاحات باید برای نامگذاری و توصیف مدلهای دامنه به کار بروند.
این اصل، هستهی DDD است و یکی از مهمترین اصول نیز هست، اما دستیابی به آن آسان نیست. کلماتی که باید ساده و واضح باشند، ممکن است ناگهان معنای خود را از دست بدهند. کلمات عمق پیدا میکنند که این خودش نشانهایست برای همهی افراد درگیر، که توسعهی یک UL و استفادهی دائم از آن، چقدر حیاتیست.
برای تأکید بیشتر، از UL در همهجا استفاده کنید: در کد، در نام توابع، در ساختارها (structs)، در متغیرها و در فرایندهایی که توسعه میدهید.
زمانی که وظیفهای را نهایی میکنید یا باگ جدیدی را بررسی میکنید، UL باید مبنای درک مشترک باشد. این کار باعث میشود UL در سراسر سازمان همراستا باقی بماند.
وقتی UL صحبت میشود ولی ابهام به وجود میآید، ممکن است نشانهای باشد از اینکه مدل دامنه در حال تکامل است و زمان آن رسیده که جلسهی جدیدی با domain expertها و توسعهدهندگان برگزار شود.
پیچیدگی دامنه را میتوان با شکستن آن به زیر-دامنهها (subdomains) کاهش داد تا بتوان مسئله را به بخشهای کوچکتر و قابلمدیریتتر تبدیل کرد.
هر دامنهای که شناسایی میکنیم، به یکی از سه نوع زیر تعلق دارد:
- Core domains: مؤلفههای حیاتی برنامه که منحصربهفرد هستند یا مزیت رقابتی برای کسبوکار ایجاد میکنند. این بخشها بیشترین تمرکز، منابع و توسعهدهندگان را به خود اختصاص میدهند. دامنهی هستهای همیشه بدیهی نیست و ممکن است با تغییرات کسبوکار، تکامل یا تغییر کند.
- Supporting domains: مؤلفههایی که عملکردهایی در جهت پشتیبانی از هستهی کسبوکار ارائه میدهند. اگر این عملکرد خاص کسبوکار نیست، بهتر است از راهحلهای آماده استفاده شود.
- Generic domains: مؤلفههایی که مستقیماً با هستهی کسبوکار مرتبط نیستند اما برای عملکرد آن ضروریاند. مثل ایمیل، پردازش پرداخت، گزارشگیری و دیگر راهحلهای عمومی. برای این بخشها معمولاً منطقی نیست که تیم توسعهی جداگانهای اختصاص داده شود، وقتی راهحلهای آمادهی زیادی وجود دارد. با تغییرات در پاسخ به رقابت یا عوامل دیگر، ممکن است در گذر زمان، نوع یک دامنه تغییر کند یا آن دامنه به دو یا چند دامنهی جدید تقسیم شود.
مدل دامنه حاصل همکاری بین متخصصان دامنه (domain experts) و توسعهدهندگان با استفاده از Ubiquitous Language (UL) است. آنچه وارد مدل میشود، باید محدود به دادهها و رفتارهایی باشد که به دامنهی مسئله مربوط هستند نه تلاش برای شبیهسازی تمام واقعیت. هدف از مدل دامنه، حل مسائلی است که در دامنه شناسایی شدهاند.
اریک اِوانز پیشنهاد میکند که با چند مدل مختلف آزمایش کنید و روی جزئیات بیش از حد متوقف نشوید. شما باید آنچه را که در مکالمه با متخصصان دامنه مهم است، استخراج کنید: به کلمات رابط برای شناسایی فرآیندها و رفتارها گوش دهید، به عناوین و موقعیتها برای شناسایی نقشها، و البته به نام اشیاء برای شناسایی دادهها. اینها باید روی یک سطح بزرگ مثل تختهسفید، کاغذ طوماری یا دیوار خالی (اگر از EventStorming استفاده میکنید) ثبت شوند. در فصل سوم (Design and Planning) دربارهی استفاده از EventStorming برای توسعهی مدل دامنه بیشتر صحبت خواهیم کرد.
مدل نباید درگیر پیچیدگیهای فنی یا نگرانیهای پیادهسازی مثل دیتابیس یا روشهای ارتباط بینپردازشی باشد؛ تمرکز باید فقط روی دامنهی مسئله باقی بماند.
هر مدل متعلق به یک bounded context است، که خود یک مؤلفه از برنامه است. از آنجا که مدل متعلق به این context است، باید مراقب بود که از نفوذهای بیرونی محافظت شود یا کنترل خارجی بر آن تحمیل نشود.
شما با شکستن پیچیدگی به چندین دامنه و کشف مدلهای نهفته در نرمافزارتان، در حال پیشروی هستید. مرزهایی که از دل این کشفها بهوجود میآیند، حول محور business capabilities در برنامه شما شکل میگیرند.
برای مثال، قابلیتهای کسبوکاری در یک برنامه مانند MallBots عبارتاند از:
- مدیریت سفارشها (Order management)
- پردازش پرداخت (Payment processing)
- عملیات انبار (Depot operations)
- مدیریت موجودی فروشگاه (Store inventory management) هیچکدام از این دامنهها نباید نگاه واحدی به یک مدل داشته باشند؛ بلکه هر کدام باید تنها درگیر بخشهایی از مدل باشند که در context خودشان معنا دارد.
هر bounded context زبان خاص خودش (UL) را دارد. یعنی ممکن است یک واژه در contextهای مختلف، معنای متفاوتی داشته باشد.
محصولاتی که مشتری انتخاب میکند، ممکن است در چندین دامنه وجود داشته باشند؛ اما بسته به context، مدلهای آنها کاملاً متفاوت، با اهداف و ویژگیهای متفاوت خواهند بود.
وقتی کارشناسان دامنه و توسعهدهندگان دربارهی «محصول» صحبت میکنند، باید context مورد اشاره را مشخص کنند: آیا منظورشان موجودی انبار فروشگاه است؟ آیا دربارهی اقلام موجود در یک سفارش صحبت میکنند؟ یا تحویل و ارسال آن در انبار (depot) را مد نظر دارند؟
bounded context به لحاظ فنی هم جلوهای دارد: در یک برنامهی توزیعشده، معمولاً به شکل یک ماژول یا یک microservice پیادهسازی میشود ولی نه همیشه. یک مرز مشخص وجود دارد که دستکاری (mutation) و کوئریهای مربوط به مدل در آن context محدود باقی میماند.
bounded contextها مفهومی دشوار هستند برای اجرای خوب DDD، باید مفهوم bounded context را درک کرد. توصیه میکنم یکی از کتابهای پیشنهادی را مطالعه کرده یا دربارهی این مفهوم جستوجو کنید. یافتن یا تعریف مرزهای مناسب در یک برنامه، یک علم نیست بلکه بیشتر یک هنر است.
ممکن است بهنظر متناقض برسد که اینهمه تلاش برای شکستن دامنه به زیر-دامنهها و bounded contextها انجام میشود، اما در نهایت باید دوباره طراحی شود که چطور اینها با هم کار کنند و یکپارچه شوند.
اینجاست که از context mapping استفاده میکنیم تا روابط بین مدلها و contextهایی را که برای عملکرد برنامه نیاز داریم، ترسیم کنیم.
هدف context mapping، شناسایی روابط بین مدلها و همچنین تیمهای دخیل در آن contextهاست. الگوهایی که در context mapping استفاده میشوند، فقط توصیفی هستند هیچگونه راهنمایی دربارهی پیادهسازی فنی اتصال بین مدلها ارائه نمیدهند:
- Open host service: این context یک قرارداد مشخص در اختیار downstream contextها قرار میدهد تا به آن متصل شوند.
- Event publisher: این context، eventهایی را برای ادغام منتشر میکند که سایر contextها میتوانند آنها را subscribe کنند.
- Shared kernel: دو تیم زیرمجموعهای مشترک از مدل دامنه (و شاید دیتابیس) را به اشتراک میگذارند.
- Published language: یک زبان مستند مشترک که برای ترجمهی مدلها بین contextها استفاده میشود. اغلب با open host service ترکیب میشود.
- Separate ways: contextهایی که بهخاطر هزینهی بالای ادغام، هیچ ارتباطی با هم ندارند. Partnership: رابطهای مشارکتی بین دو context با مدیریت مشترک بر ادغام بین آنها.
- Customer/supplier: رابطهای که در آن downstream میتواند تغییرات در upstream را وتو یا مذاکره کند.
- Conformist: سرویس downstream به مدل context بالادستی (upstream) وابسته است.
- Anticorruption layer: لایهای که سرویس downstream را از تغییرات در مدل upstream ایزوله میکند. با استفاده از الگوهای بالا، میتوانیم روابط بین contextهای مختلف برنامه را مشخص کنیم.
DDD بهطور کلی برای برنامههای event-driven مفید است اگرچه بدون آن هم ممکن است به خوبی کار کنید. اما چیزی که DDD وارد بازی میکند یعنی بررسی دقیق مسئلهی تجاری با کمک domain expertها، و توسعهی UL برای شکستن پیچیدگی به bounded contextها قابل چشمپوشی نیست.
برنامههای event-driven با تلاش برای نامگذاری بهتر eventها، تشخیص eventهای مربوط به یکپارچگی (integration events)، و تبدیل آنها به بخشی از قرارداد یک bounded context، قطعاً منتفع خواهند شد.
معماری دامنهمحور، همانطور که پیشتر هم اشاره شد، معماریایست که دامنه را در مرکز خود قرار میدهد. در اطراف دامنه، لایهای برای منطق کاربرد (application logic) قرار دارد، و بیرون از آن، لایهای برای زیرساخت یا دغدغههای بیرونی.
هدف این معماری، حفظ دامنه از تأثیرات خارجی مانند جزئیات دیتابیس یا وابستگی به فریمورکهاست.
قبل از اینکه بیشتر دربارهی معماری دامنهمحور صحبت کنیم، ابتدا نگاهی خواهیم داشت به برخی معماریهای سنتی یا سازمانی (enterprise).
تیمها در طول زمان متوجه خواهند شد که در معماریهای سنتی، هزینهی نگهداری برنامه بهمرور افزایش مییابد. این معماریها همچنین هنگام تغییر زیرساختها یا نیازمندیهای فنی، بهسختی قابل بهروزرسانی هستند. در هر دو معماری نشاندادهشده در برنامهها به لایههایی تقسیم شدهاند و از نظر مفهومی تفاوت زیادی با هم ندارند. مشکل، خودِ لایهها نیست بلکه شیوهی coupling (وابستگی تنگاتنگ) میان آنهاست. مدلهای دادهای که در لایهی data access قرار دارند، در لایههای application و presentation نیز استفاده میشوند. برعکس آن نیز ممکن است: مدلهای درخواست (request models) که در UI frameworkها استفاده میشوند، وارد سایر لایهها میشوند. نتیجهی این وابستگیها این است که اگر تغییری در لایهی presentation ایجاد شود، به احتمال زیاد باید در data access هم تغییراتی اعمال کرد. درگیر شدن با این شبکهی درهمتنیده از وابستگیها و coupling شدید، باعث میشود که هزینههای سازمان برای نگهداری برنامه در طول زمان بهسرعت افزایش یابد.
Alistair Cockburn در سال ۲۰۰۵ با معرفی hexagonal architecture (در حین توضیح الگوی ports and adapters) راهحلی برای مشکل coupling درون یک برنامه پیشنهاد داد: https://alistair.cockburn.us/hexagonal-architecture در این معماری، برنامه به دو بخش تقسیم میشود:
بخش درونی برنامه (core) بخش بیرونی شامل وابستگیها هر وابستگی خارجی به دو جزء تقسیم میشود:
یک port یا interface و یک adapter یا پیادهسازی (implementation) برای مثال، interfaceای به نام PeopleRepository در تصویر نقش port را دارد، و دو پیادهسازی مختلف به نامهای RedisPeopleRepository و TestPeopleRepository نقش adapter را ایفا میکنند.
با استفاده از این تکنیک، برنامههای ما از تغییرات در وابستگیهای بیرونی ایزوله میشوند.
در سال ۲۰۰۸، Jeffrey Palermo ما را با onion architecture آشنا کرد:
https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/
در این معماری، dependency inversion principle نقش بسیار مهمی دارد (که در بخشهای بعدی بیشتر توضیح داده خواهد شد).
یک برنامه با معماری hexagonal اکنون به لایههای مختلف تقسیم شده است:
- application services
- domain services
- domain model
وابستگیهای بیرونی، خارجیترین لایه را در اطراف هستهی برنامه تشکیل میدهند. تمام وابستگیها به سمت درون و بهسمت مدل دامنه اشاره میکنند، و دایرههای بیرونی، پیادهسازیهای interface های قرارگرفته در لایههای داخلی را در خود جای میدهند.
Palermo همچنین پیشنهاد کرد که از یک inversion of control container برای مدیریت تزریق وابستگیها (dependency injection) استفاده شود. زبان Go پشتیبانی قویای برای DI ندارد، اما در فصل چهارم (Event Foundations) راهحلهایی در اینباره خواهیم دید.
در سال ۲۰۱۲، Robert C. Martin (عمو باب)، پس از بررسی معماریهای hexagonal و onion و چند مدل دیگر، معماری Clean Architecture را معرفی کرد: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
او اشاره کرد که این معماریها چند ویژگی مشترک دارند:
- به هیچ فریمورکی وابسته نیستند که بخواهند برنامه را بر پایهی آن scaffold کنند
- طراحی آنها به تولید کدی testable کمک میکند
- زیرساختها بهعنوان وابستگیهای قابل جایگزینی در نظر گرفته میشوند
مهمترین اصل در معماریهای domain-centric، بهگفتهی مارتین، این است:
«وابستگیهای سورسکد تنها باید به سمت درون اشاره داشته باشند.» هیچچیزی در یک دایرهی درونی، نباید به چیزی در دایرهی بیرونی ارجاع دهد. برنامه باید ارجاعات را تنها با استفاده از اصل dependency inversion حل کند.
اگر از درونیترین لایهها شروع کنیم ، موارد زیر را خواهیم داشت:
مدل و interface هایی مانند IRepository که فقط میتوانند به سایر entity ها در لایهی Entities ارجاع دهند، و نه بیشتر. سپس لایهی Use Cases با component هایی مانند Application و IApplication که فقط مجاز به استفاده از آیتمهای موجود در Use Cases یا Entities هستند. در لایهی Interface Adapters، کنترلرها و پیادهسازیهای repository ها قرار دارند. در نهایت، آخرین لایه ممکن است ظاهراً شامل یک خطا باشد: فلش ارجاع از repository به سمت دیتابیس قرار دارد، در حالی که طبق اصل Dependency Inversion Principle (DIP) نباید اینطور باشد.
در واقع، پیادهسازی واقعی repository حتماً باید ارجاعی به دیتابیس داشته باشد تا کار کند، اما interface IRepository باعث میشود کل برنامه از پیادهسازی خاص دیتابیس ایزوله بماند.
Alistair Cockburn معماری hexagonal را برای مقابله با نفوذ منطق تجاری به بخشهایی از نرمافزار که ارتباطی با آن ندارند، ابداع کرد. او سه عامل اصلی برای این مشکل برشمرد:
- تستنویسی دشوار میشود وقتی تستها وابسته به رابط کاربری (UI) باشند
- coupling (وابستگی تنگاتنگ) جابهجایی بین استفادهی انسانی و ماشینی را غیرممکن میکند
- تغییر یا تعویض زیرساختها هنگام نیاز یا فرصت، بسیار دشوار یا حتی غیرممکن میشود
جداسازی کامل application core از وابستگیهای خارجی. Cockburn پیشنهاد داد که APIها (ports) در مرز برنامه تعریف شوند و برای تعامل با اجزای خارجی، از adapters استفاده شود. این جفت شدن abstraction و پیادهسازی concrete باعث میشود که اجزای خارجی مثل UI جدید، تست هارنس با mock، یا زیرساخت متفاوت بتوانند بهراحتی جایگزین یا افزوده شوند.
ترکیب با Clean Architecture در نمودارهای اولیهی معماری hexagonal، به دامنه بهاندازهی کافی توجه نشده بود. نویسنده یک دامنه را به برنامه اضافه کرده و hexagonی برای UI و زیرساخت طراحی کرده تا تفسیری ترکیبی از این دو معماری ارائه دهد
در مرکز نمودار، دامنه قرار دارد لایهای شامل: مدل دامنه منطق خاص دامنه و سرویسهای دامنه این لایه، کمترین تأثیر را از تغییرات بیرونی میپذیرد. هیچ وابستگی خارجی ندارد و هیچ ارجاعی به اجزای بیرونی یا application serviceها در آن دیده نمیشود.
اطراف دامنه، لایهی application قرار دارد جایی که منطق خاص برنامه و سرویسهای آن تعریف میشود. همچنین، interfaceهایی که اجزای خارجی از آن برای تعامل با برنامه استفاده میکنند، در این لایه تعریف میشوند. قانون مهم: لایهی application فقط میتواند به لایهی دامنه وابستگی داشته باشد نه بیشتر. Ports و Adapters خارج از application، همهی اجزای خارجی قرار دارند: frameworkها پیادهسازی UI دیتابیسها تمام تعاملات با application از طریق port انجام میشود abstractionی که توسط application شناخته شده و مسیر ارتباطی آن با دنیای بیرون است.
در سمت دیگر تعامل، adapter قرار دارد قطعهکدی که دقیقاً میداند چطور با وابستگی خارجی ارتباط برقرار کند.
انواع adapterها:
- Primary (driver): مثل وب UI، API و event consumer که داده را به درون برنامه هدایت میکنند
- Secondary (driven): مثل دیتابیس، logger و event producer که از اطلاعات برنامه تغذیه میشوند گاهی چند adapter ممکن است یک port را استفاده کنند.
ارتباط بین adapterها و application فقط از طریق port و DTOهایی انجام میشود که برای درخواست و پاسخ تعریف شدهاند.
Abstractionهایی که برای ایزولهسازی برنامه و مدل دامنه از اجزای خارجی به کار گرفتهایم، در تستنویسی نیز به ما کمک میکنند: Test harness میتواند جایگزین هر adapter اولیه شود میتوان از mock application برای تست کردن ارتباطات واقعی با دیتابیس استفاده کرد این معماری و جداسازی مسئولیتها که توسط لایهها به ما تحمیل میشود باعث میشود کامپوننتهای کوچکتر و قابلتستتری بنویسیم.
معماری domain-centric به شما قوانینی برای نوشتن کد بهتر میدهد نه راهنمای قدمبهقدم برای انجام آن. نویسنده نمیگوید که دقیقاً چگونه پکیجها یا ماژولها را در Go سازماندهی کنید، یا constructorها را چطور بنویسید، یا از چه روشی برای dependency injection استفاده کنید.
- آیا تستنویسی برایتان مهم است؟
- آیا نگهداری سیستم اهمیت دارد؟ یک برنامهی domain-centric بسیار تستپذیر خواهد بود و در بلندمدت هزینهی نگهداری پایینتری خواهد داشت. اگر برنامهی شما بزرگ باشد و بهویژه اگر از DDD استفاده میکنید سودمندی این معماری بیش از مشکلات آن خواهد بود. داشتن core برنامهای که مستقل از فریمورک، زیرساخت یا vendor خاص (مثل cloud provider) باشد، باعث میشود سیستم شما قابلانتقال و قابلبازاستفادهتر باشد.
معماری domain-centric نیاز به سرمایهگذاری اولیهی بیشتری دارد و برای توسعهدهندگان کمتجربه میتواند چالشبرانگیز باشد. از دید بعضی مهندسان، قوانین و محدودیتهای این معماری ممکن است منجر به پیچیدگی بیشازحد یا over-engineering شود. برای برخی توسعهدهندگان، نگهداری abstractionها برای هر dependency یا استفاده از DI میتواند منجر به boilerplate زیاد و کار بیشتر شود. همانند DDD، پیادهسازی اشتباه یا سختگیرانهی این معماری میتواند منجر به شکست شود و در نهایت این شکست به خودِ معماری نسبت داده میشود.
معماریهای domain-centric بهطور کلی برای برنامههای Event-Driven مفید هستند اما اگر سرویسهای شما کوچک باشند، یا نیازی به مهاجرت از cloud provider یا تعویض دیتابیس نداشته باشید، میتوانید از آن صرفنظر کنید.
الگوی CQRS الگویی ساده برای تعریف است. اشیاء به دو شیء جدید تقسیم میشوند: یکی مسئول فرمانها (commands) و دیگری مسئول پرسوجوها (queries) است. این تصویر نشان میدهد که مفهوم چقدر ساده میتواند باشد، ولی همانطور که میگویند، شیطان در جزئیات پیادهسازی نهفته است. تعاریف فرمان و پرسوجو همانهایی هستند که در اصل الگوی CQS (جداسازی فرمان و پرسوجو) نیز تعریف شدهاند:
-
Command (فرمان): وضعیت برنامه را تغییر میدهد
-
Query (پرسوجو): وضعیت برنامه را به فراخوان بازمیگرداند
توجه: در CQRS، همانند CQS، یک عمل میتواند یا یک فرمان باشد یا یک پرسوجو، ولی نمیتواند هر دو باشد.
مدلهای دامنهای که با کمک متخصصان دامنه طراحی کردهایم ممکن است بسیار پیچیده و بزرگ باشند. این مدلهای پیچیده ممکن است برای پرسوجوهای ما کاربردی نباشند یا بیش از حد پیچیده باشند. برعکس، ممکن است پرسوجوهای پیچیدهای داشته باشیم که باعث شود بخواهیم مدل دامنهای خود را برای پشتیبانی از آن تغییر دهیم — که این ممکن است نقض زبان فراگیر (UL) باشد. همچنین ممکن است نتوانیم یک پرسوجو را با مدل دامنهای موجود انجام دهیم.
تشبیهی که برای توصیف CQRS استفاده میکنم این است که برنامهات را مثل یک روبان تصور کنی: این برنامه، که در تصویر به شکل روبان نشان داده شده، را میتوان بهصورت افقی در هر نقطهای برید، که دو سمت بالا و پایین ایجاد میکند. اینکه کجا ببری و تا چه حد، مشخص میکند که چقدر از الگوی CQRS را روی برنامهات اعمال کردهای.
ممکن است بخواهی CQRS را فقط در کد برنامهات اعمال کنی:
با تقسیم برنامه به سمت فرمان و سمت پرسوجو، میتوانی مدلهای امنیتی متفاوتی برای هر سمت در نظر بگیری یا پیچیدگی سرویسهایت را کاهش دهی. ممکن است همچنان از یک پایگاهداده استفاده کنی، ولی در یک سمت از ORM و در سمت دیگر از SQL خام برای بهبود کارایی بهره ببری. این ممکن است ضعیفترین شکل استفاده از CQRS در برنامه باشد.
میتوانی استفاده از CQRS را به پایگاهداده گسترش دهی:
پرسوجوهای SQL بهینه فقط تا حدی پیش میروند. انتقال پرسوجوها به یک دیتاستور جدید مانند NoSQL، key-value، document یا گراف ممکن است برای مدیریت بار لازم باشد. میتوانی با استفاده از رویکرد event-driven، چندین projection جدید را در چند سرویس مختلف تولید کنی.
اگر تا ته برید، میتوانی سرویس را به دو بخش تقسیم کنی:
اعمال کامل CQRS روی سرویس منجر به دو سرویس مجزا میشود که میتوانند بهصورت مستقل مقیاسپذیر باشند، توسط تیمهای مختلف نگهداری شوند و حتی تکنولوژیهای کاملاً متفاوتی داشته باشند.
بیایید مواردی را بررسی کنیم که CQRS مناسب است:
-
سیستم تو بار خواندن بیشتری نسبت به نوشتن دارد CQRS اجازه میدهد عملیات خواندن و نوشتن را جدا و مستقل مقیاسدهی کنی
-
مدل امنیتی برای خواندن و نوشتن متفاوت است همچنین میتوان دسترسی به دادهها را محدودتر کرد
-
از الگوهای event-driven مثل event sourcing استفاده میکنی → با انتشار eventهای مدلهای event-sourced، میتوانی هر تعداد projection مورد نیاز را برای پرسوجوها بسازی
-
الگوهای خواندن پیچیدهای داری که مدل دامنه را متورم یا پیچیده میکنند → انتقال مدلهای خواندن به بیرون از مدل دامنه، به بهینهسازی آنها کمک میکند
-
میخواهی دادهها حتی وقتی نوشتن ممکن نیست در دسترس باشند → وقتی نوشتن غیرفعال است، خواندن همچنان اجازه بازگرداندن وضعیت سیستم را دارد
از نظر من، CQRS یک الگوی event-driven نیست. میتوان آن را کاملاً بدون هیچ event یا رویکرد asynchronous استفاده کرد. با این حال، اغلب همراه با event sourcing مطرح میشود، زیرا این دو با هم بسیار خوب کار میکنند.
یکی از مزایای تقسیم مدل به دو بخش این است که سمت نوشتن فقط به یک log افزایشی مینویسد. و از سوی دیگر، میتوان به تعداد دلخواه مدلهای خواندن داشت که از همان eventها تغذیه میشوند. این مدلهای خواندن میتوانند دقیقاً برای نیازهای خاص ساخته شوند و در سراسر برنامه پخش شوند.
یکی از اهداف CQRS این است که رفتارهایی که باعث اجرای فرمانها در برنامه میشوند را بهصورت صریح در سمت نوشتن مشخص کند. در UIهای مبتنی بر CRUD این کار سخت است. رفتار مورد نظر کاربر اغلب پشت فرمانهایی مثل UpdateUser پنهان میشود. مثلاً ممکن است همان فراخوانی برای بروزرسانی پروفایل کاربر یا تغییر آدرس استفاده شود، که تشخیص رفتار واقعی را سخت میکند.
در UI مبتنی بر وظیفه، هر عمل معنای واضحتری دارد. مثلاً:
- وقتی پروفایل بروزرسانی میشود:
UpdateProfile
- وقتی آدرس پستی تغییر میکند:
ChangeMailingAddress
برای یک برنامهی event-driven، معماریهای مختلفی برای انتخاب وجود دارد. هر کدام مزایا و معایب خود را دارند. برای پروژههایی که از صفر شروع میشوند (green field)، تنها یک معماری هست که توصیه میکنم.
این نوع برنامه معمولاً از یک پایگاه کد واحد ساخته میشود و بهعنوان یک منبع واحد مستقر میگردد. چنین برنامههایی مزایای زیادی دارند: استقرار آسان، مدیریت و عملیات نسبتاً ساده. بیرون از نیاز به ارتباط با برخی APIهای شخص ثالث، یک رابط کاربری و پایگاهداده واحد بیشتر نیازهای زیرساختی را پوشش میدهند. برنامهای که در تصویر نشان داده شده است، بهراحتی با استقرار در چند نمونه (instance) که به همان پایگاهداده اشاره دارند، میتواند مقیاسپذیر شود.
در طرف مقابل، هرچه مونولیث بزرگتر شود، توسعه آن برای تیمها سختتر میشود؛ افزودن قابلیتهای جدید باعث تداخل میشود و استقرار مداوم تبدیل به یک خاطرهی محو میگردد. همچنین این معماری بهطور ناعادلانهای مورد انتقاد برای کدهای بههمریخته قرار میگیرد. درحالیکه این موضوع ربطی به نوع معماری ندارد، بلکه به طراحی بد در هر پایگاه کدی ممکن است رخ دهد.
مونولیث مدولار بسیاری از مزایا و معایب مونولیث سنتی را دارد، اما همچنین از مزایای معماری میکروسرویس نیز بهرهمند است بدون اینکه همهی معایب آن را داشته باشد.
اگر الگوهای DDD و معماری مبتنی بر دامنه را روی برنامهی مونولیث فعلیمان اعمال کنیم، میتوانیم آن را بهسمت یک معماری مونولیث مدولار بازطراحی کنیم. با شناسایی دامنههای برنامه و تعریف مرزهای محدودشده (bounded contexts)، میتوانیم هستهی مونولیث را به هر تعداد ماژول مورد نیاز تقسیم کنیم.
برنامهی بازطراحیشدهای که در تصویر نشان داده شده است، حالا شامل سه ماژول است که میتوانند بهصورت مستقلتر توسط تیمها یا توسعهدهندگان مختلف توسعه داده شوند. هرگونه ارتباط بین ماژولها باید مانند هر نگرانی خارجی دیگری با interface و پیادهسازی concrete مدیریت شود تا قراردادهای enforceable ایجاد شود.
معماری میکروسرویس به معنای ساخت سرویسهای مجزایی است که ترجیحاً با bounded contextها همراستا هستند تا یک برنامهی توزیعشده ساخته شود. مزایای میکروسرویسها نسبت به مونولیث این است که:
-
بهصورت مستقل قابل استقرار هستند
-
بهصورت مستقل مقیاسپذیرند
-
تابآوری بهتر در برابر خطا دارند به دلیل ایزولاسیون خطا (fault isolation) مزایای وابستگی کم (loose coupling) شاید نسبت به یک مونولیث طراحینشده زیاد باشد، اما در برابر مونولیث مدولار آنچنان برتری ندارد. سرویسها بهصورت مجزا، پایگاه کدهای کوچکتری دارند و تستپذیرترند. البته معایبی نیز دارند:
-
مهمترین آن، پیچیدگی مدیریت سرویسهای متعدد مستقل و مرتبط است
-
یکپارچگی نهایی (eventual consistency) بهدلیل ماهیت توزیعشده، همیشه باید در نظر گرفته شود
-
تستهای گسترده که شامل چند میکروسرویس هستند، به تلاش بیشتری نیاز دارند
معماری مونولیث مدولار توصیهشدهترین انتخاب برای شروع هر پروژه با پیچیدگی منطقی است. تیم میتواند تمرکز بیشتری بر طراحی مدل دامنهای داشته باشد، بدون اینکه نیاز فوری به پشتیبانیهای زیرساختی اضافی برای استقرار برنامه باشد. زمانی که برنامه از معماری مونولیث مدولار فراتر رفت، تیم میتواند بهراحتی ماژولها را بهعنوان میکروسرویس استخراج کند تا از مزایای معماری میکروسرویس بهرهمند شود.
در این فصل، برخی از الگوهای استراتژیک کلیدی در DDD را بررسی کردیم و دیدیم که چگونه میتوانند به توسعه برنامههای بهتر کمک کنند. همچنین با برنامههای مبتنی بر دامنه (domain-centric applications) آشنا شدیم که بعد از طراحی دقیق bounded contextها و مدلهای دامنه، میتوانند به ما کمک کنند برنامه را به شکل سازمانیافتهتری پیادهسازی کنیم. سپس به سراغ الگوی CQRS رفتیم و دیدیم چگونه میتوان آن را همراه با event sourcing برای ساخت برنامههایی با کارایی بهتر استفاده کرد. در نهایت، معماریهای مختلف برنامه را بررسی کردیم که میتوانند از الگوهای معماری event-driven بهرهمند شوند.