پست

Fundamentals of Software Architecture

A Comprehensive Guide to Software Architecture Principles and Practices

Fundamentals of Software Architecture

توضیحات

کتاب Fundamentals of Software Architecture: A Modern Engineering Approach نوشته‌ی Mark Richards و Neal Ford اولین کتاب جامعی است که به‌طور اختصاصی مسیر تبدیل توسعه‌دهنده به معمار نرم‌افزار را پوشش می‌دهد. ویرایش اول در مارس ۲۰۲۰ و ویرایش دوم با ۵ فصل جدید در آوریل ۲۰۲۵ منتشر شده است.
کتاب ادعا می‌کند که «engineering approach» ارائه می‌دهد — یعنی روش‌های تکرارپذیر، متریک‌های قابل اندازه‌گیری، و چارچوب‌های عینی برای تصمیم‌گیری معماری. این ادعا در برخی بخش‌ها خوب اجرا شده و در برخی بخش‌ها با نظرات شخصی جایگزین شده — چیزی که منتقدان به‌درستی به آن اشاره کرده‌اند.

نظر

  • امتیاز : 08/10
  • به دیگران توصیه می‌کنم : بله
  • دوباره می‌خوانم : خیر
  • ایده برجسته : «Architecture is the stuff that’s hard to change» — معماری تصمیم‌هایی است که هزینه‌ی تغییرشان بالاست؛ هر چیزی که بتوان به‌راحتی تغییر داد جزء design است نه architecture. همچنین: هیچ سبک معماری‌ای کامل نیست — وظیفه‌ی architect این است که با آگاهی کامل از trade-offها یکی را انتخاب کند
  • تاثیر در من : جدول‌های مقایسه‌ی architectural styles در Part 2 تبدیل به یک reference دائمی شد. مفهوم connascence و architectural fitness functions نگاهم به coupling و evolutionary architecture را عوض کرد. همچنین فصل ADR برای مستندسازی تصمیم‌های معماری در تیم مستقیماً قابل استفاده است
  • نکات مثبت : جامع‌ترین مرجع موجود برای architectural styles با مقایسه‌ی صادقانه‌ی trade-offها؛ دیاگرام‌های واضح برای هر سبک؛ پوشش soft skills که کتاب‌های فنی معمولاً نادیده می‌گیرند؛ چارچوب ADR برای مستندسازی تصمیم‌ها؛ ویرایش دوم ۲۰۲۵ با فصل‌های جدید به‌روز است
  • نکات منفی : برخی ادعاهای Part 2 بیش از آنکه engineering باشند opinion هستند و بدون مستندات کافی ارائه می‌شوند؛ بخش microservices سطحی است — برای عمق بیشتر باید Software Architecture: The Hard Parts (همین نویسندگان) را بخوانی؛ Part 3 نسبت به Part 1 و 2 کم‌عمق‌تر است؛ برخی توصیف‌های نقش architect بیش از حد idealized است و با واقعیت تیم‌های کوچک فاصله دارد

مشخصات

  • نویسنده :
    • Mark Richards
    • Neal Ford
  • انتشارات : O’Reilly Media
  • صفحه مشخصات :

بخش‌هایی از کتاب

بخش اول: مقدمه — چرا معماری نرم‌افزار تعریف دقیقی ندارد؟

کتاب Fundamentals of Software Architecture نوشته Mark Richards و Neal Ford با یک پرسش اساسی آغاز می‌شود: چرا با آنکه «معمار نرم‌افزار» در رأس بهترین شغل‌های دنیا قرار دارد، هیچ مسیر شغلی مشخصی برای رسیدن به آن وجود ندارد؟

نویسندگان چهار دلیل ریشه‌ای برای این خلاء ارائه می‌دهند:

  1. نبود تعریف دقیق از معماری نرم‌افزار — حتی Martin Fowler در مقاله معروف خود از تعریف آن طفره رفت و تنها به این نقل‌قول اکتفا کرد: “Architecture is about the important stuff… whatever that is.”
  2. وسعت و گستردگی بی‌سابقه مسئولیت‌ها — یک دهه پیش، معمار فقط با جنبه‌های فنی مثل ماژولاریتی و Design Patterns سروکار داشت. امروز این نقش با DevOps، فرآیند توسعه، و حتی سیاست‌های سازمانی تقاطع پیدا کرده است.
  3. هدف متحرک بودن معماری — هر تعریفی که امروز ارائه شود، در چند سال آینده منسوخ خواهد بود. جمله‌ی معروف ویکی‌پدیا که می‌گفت «معماری تصمیماتی است که تغییرشان پرهزینه است» دیگر در دنیای Microservices صادق نیست.
  4. ارتباط تاریخی بودن اغلب منابع موجود — بسیاری از کتاب‌های معماری در دورانی نوشته شده‌اند که شباهت کمی به اکوسیستم امروز دارند.

تعریف معماری نرم‌افزار

نویسندگان معماری نرم‌افزار را نه صرفاً «ساختار»، بلکه ترکیب چهار بُعد می‌دانند:

بُعدتوضیح
Structureسبک معماری (Microservices، Layered، Microkernel و…)
Architecture Characteristics (“-ilities”)مشخصه‌هایی مانند Scalability، Performance، Availability — مستقل از منطق تجاری
Architecture Decisionsقوانین الزامی برای ساختن سیستم (مثلاً: فقط لایه Business می‌تواند به Database دسترسی داشته باشد)
Design Principlesراهنمایی‌های انعطاف‌پذیر، نه قانون سخت (مثلاً: ترجیحاً از Async Messaging استفاده کن)

نکته کلیدی: گفتن “این یک Microservices Architecture است” تنها Structure را توصیف می‌کند، نه معماری کامل سیستم را.

قوانین معماری نرم‌افزار

نویسندگان دو قانون بنیادین را مطرح می‌کنند که مانند ستون فقرات کل کتاب عمل می‌کنند:

قانون اول: “Everything in software architecture is a trade-off.” و اگر فکر می‌کنی چیزی پیدا کرده‌ای که Trade-off ندارد، احتمالاً هنوز آن را کشف نکرده‌ای.

قانون دوم: “Why is more important than how.” یک معمار می‌تواند با نگاه به یک سیستم چگونگی کارکرد آن را درک کند، اما بدون اسناد، توضیح چرایی تصمیمات تقریباً غیرممکن است.


بخش بعدی: انتظارها از یک معمار نرم‌افزار

در ادامه‌ی مقدمه، نویسندگان توضیح می‌دهند که نقش معمار نرم‌افزار فقط «طراحی» نیست؛ بلکه مجموعه‌ای از مسئولیت‌های پیوسته است که باید در سطح فنی، سازمانی و تصمیم‌گیری مدیریت شوند.

1) تصمیم‌های معماری را بگیر

معمار باید تصمیم‌های مهم و اثرگذار را مشخص کند و برای آن‌ها موضع بگیرد. این تصمیم‌ها نباید مبهم، سلیقه‌ای یا صرفاً مبتنی بر ترجیح شخصی باشند؛ بلکه باید روشن، قابل دفاع و قابل پیگیری باشند.

2) معماری را به‌طور مداوم تحلیل کن

معماری یک موجود زنده است، نه یک سند ثابت. معمار باید به‌صورت مستمر سیستم را بررسی کند تا ببیند آیا ساختار فعلی هنوز با نیازهای کسب‌وکار، محدودیت‌های فنی و ویژگی‌های مورد انتظار سازگار است یا نه.

3) با روندهای جدید همگام بمان

نویسندگان تأکید می‌کنند که معمار نباید در گذشته بماند. ابزارها، روش‌ها و معماری‌ها دائماً تغییر می‌کنند؛ بنابراین معمار باید جریان‌های جدید را بشناسد، اما نه با هیجان‌زدگی کورکورانه، بلکه با نگاه تحلیلی.

4) اجرای تصمیم‌ها را تضمین کن

تصمیم معماری فقط زمانی ارزش دارد که در عمل رعایت شود. اگر تیم‌ها تصمیم‌ها را نادیده بگیرند، معماری به‌تدریج فرسوده می‌شود. پس معمار باید مکانیزمی برای رعایت و پیگیری تصمیم‌ها داشته باشد.

5) تجربه و مواجهه‌ی متنوع داشته باش

معمار خوب فقط از یک پروژه یا یک تکنولوژی نمی‌آید. تجربه‌ی متنوع در حوزه‌ها، تیم‌ها و سبک‌های مختلف توسعه، به او کمک می‌کند که trade-offها را بهتر ببیند و تصمیم‌های عاقلانه‌تری بگیرد.

6) دانش دامنه‌ی کسب‌وکار داشته باش

اگر معمار فقط از نظر فنی قوی باشد ولی مسئله‌ی کسب‌وکار را نفهمد، تصمیم‌هایش سطحی خواهد بود. نویسندگان بر این نکته تأکید می‌کنند که معماری باید با نیازهای واقعی دامنه‌ی مسئله هم‌راستا باشد.

7) مهارت‌های بین‌فردی داشته باش

معماری فقط کار فنی نیست؛ بخش بزرگی از آن ارتباط با آدم‌هاست. معمار باید بتواند با توسعه‌دهنده، مدیر محصول، مدیر فنی و ذی‌نفعان مختلف ارتباط مؤثر برقرار کند و اختلاف دیدگاه‌ها را مدیریت کند.

8) سیاست سازمانی را درک و مدیریت کن

نویسندگان صریح‌اند: معماری در خلأ انجام نمی‌شود. تصمیم‌های معماری همیشه در فضای سازمانی، قدرت، اولویت‌ها و محدودیت‌های واقعی شکل می‌گیرند. معمار باید این فضا را بشناسد و در آن حرکت کند، نه اینکه وانمود کند سیاست وجود ندارد.

تقاطع معماری با حوزه‌های دیگر

در ادامه، کتاب نشان می‌دهد که معماری نرم‌افزار در انزوا تعریف نمی‌شود، بلکه با چند حوزه‌ی مهم تلاقی دارد: Engineering Practices، Operations/DevOps، Process و Data.

  • Engineering Practices: معماری تحت تأثیر کیفیت کدنویسی، تست، اتوماسیون و شیوه‌های مهندسی تیم قرار دارد.
  • Operations/DevOps: معماری باید با استقرار، مانیتورینگ، نگهداری و عملیات واقعی سازگار باشد.
  • Process: فرآیند توسعه و تحویل نرم‌افزار بر معماری اثر می‌گذارد و برعکس.
  • Data: داده فقط یک جزئیات پیاده‌سازی نیست؛ در بسیاری از سیستم‌ها، داده تعیین‌کننده‌ی شکل معماری است.

نتیجه‌ی این بخش

پیام اصلی این قسمت این است که معمار نرم‌افزار فقط طراح ساختار نیست؛ بلکه کسی است که باید تصمیم‌های درست بگیرد، آن‌ها را در سازمان جا بیندازد، با تغییرات همراه شود، و هم‌زمان بُعد فنی، بُعد انسانی و بُعد کسب‌وکاری را مدیریت کند.


بخش بعدی: قانون‌های معماری نرم‌افزار

نویسندگان بعد از معرفی نقش معمار، به سراغ دو قانون بنیادین می‌روند که تقریباً تمام کتاب بر آن‌ها بنا شده است: همه‌چیز در معماری نرم‌افزار یک trade-off است، و درک چرا از دانستن چگونه مهم‌تر است. این دو اصل، نگاه کتاب را از یک مجموعه توصیه‌ی فنی ساده به یک چارچوب تحلیلی تبدیل می‌کنند.

قانون اول: همه‌چیز trade-off است

در معماری نرم‌افزار، تقریباً هیچ تصمیمی «رایگان» نیست. اگر یک انتخاب، performance را بهتر کند، ممکن است complexity را بالا ببرد؛ اگر deployment را آسان‌تر کند، شاید observability یا data consistency را سخت‌تر کند. نویسندگان تأکید می‌کنند که معماری یعنی انتخاب آگاهانه میان مزایا و هزینه‌ها، نه پیدا کردن راه‌حل جادویی.

این نگاه مهم است چون معمار را مجبور می‌کند از ذهنیت «بهترین تکنولوژی» فاصله بگیرد. در دنیای واقعی، تصمیم خوب تصمیمی است که در زمینه‌ی مشخص خودش بهترین نسبتِ هزینه به فایده را داشته باشد، نه لزوماً تصمیمی که در همه‌جا برنده باشد.

قانون دوم: چرا مهم‌تر از چگونه است

نویسندگان می‌گویند یک معمار باید بتواند تصمیم‌ها را توجیه کند، نه فقط اجرا کند. دانستن این‌که «سیستم چگونه ساخته شده» کافی نیست؛ باید روشن باشد «چرا این‌طور ساخته شده». چون معماری یک سری پاسخ فنی نیست، بلکه مجموعه‌ای از پاسخ‌های مستند به نیازها، محدودیت‌ها و trade-offهاست.

این اصل از آن جهت حیاتی است که تصمیم‌های معماری با گذشت زمان فراموش می‌شوند. وقتی کسی فقط شکل نهایی سیستم را ببیند، ممکن است خیال کند تصمیم‌های فعلی بدیهی بوده‌اند؛ در حالی که پشت هر انتخاب، محدودیت‌ها و مصالحه‌های خاصی وجود داشته است. به همین دلیل، کتاب روی ثبت و توضیح دلایل تصمیم‌ها تأکید زیادی دارد.

پیام اصلی این دو قانون

ترکیب این دو قانون یک نتیجه‌ی روشن دارد: معمار نباید دنبال «حقیقت مطلق» در معماری باشد، بلکه باید به‌صورت تحلیلی تصمیم بگیرد و برای هر انتخاب، هزینه و فایده را روشن کند. همین نگاه است که معماری را از سلیقه‌ی شخصی جدا می‌کند و به یک discipline مهندسی نزدیک می‌سازد.


بخش بعدی: تفکر معماری (فصل ۲) — تمایز میان معماری و طراحی

با پایان فصل مقدمه، کتاب وارد بخش اول (Part I: Foundations) می‌شود و فصل دوم را با عنوان «Architectural Thinking» آغاز می‌کند. اولین موضوع مطرح‌شده، تفاوت بنیادین میان Architecture و Design است؛ موضوعی که نویسندگان معتقدند بسیاری از تیم‌ها آن را به‌درستی درک نمی‌کنند.

مرز بین معماری و طراحی

نویسندگان استدلال می‌کنند که خط دقیق و ثابتی بین معماری و طراحی وجود ندارد، بلکه این دو روی یک طیف (spectrum) قرار می‌گیرند. تصمیم‌هایی که دامنه‌ی گسترده‌تری دارند و روی چند بخش یا کل سیستم اثر می‌گذارند، معمولاً در حوزه‌ی معماری قرار می‌گیرند؛ در حالی‌که تصمیم‌های محدودتر و جزئی‌تر که فقط یک بخش کوچک را درگیر می‌کنند، بیشتر به طراحی (Design) مربوط می‌شوند.

نکته‌ی کلیدی این است که معمار و توسعه‌دهنده باید به‌طور مداوم با یکدیگر همکاری کنند، چون خطای رایج این است که معماری را کاملاً جدا از کدنویسی روزمره تصور کنیم. در واقع، تصمیم‌های معماری باید در کد پیاده‌سازی شوند و بازخورد از سطح کد باید به تصمیم‌های معماری برگردد.

چرا این مرزبندی مهم است؟

اگر تیم بین معماری و طراحی مرز روشنی قائل نشود، دو خطر رایج پیش می‌آید: یا معمار درگیر جزئیات بیش‌ازحد می‌شود و از تصویر کلان دور می‌ماند (که نویسندگان آن را در فصل‌های بعدی «Control Freak» می‌نامند)، یا برعکس، آن‌قدر از جزئیات فاصله می‌گیرد که تصمیم‌هایش با واقعیت کد فاصله پیدا می‌کند («Armchair Architect»).

نیاز به Technical Breadth

موضوع بعدی که کتاب معرفی می‌کند، ضرورت داشتن Technical Breadth (وسعت دانش فنی) برای معمار است، در تقابل با Technical Depth که بیشتر ویژگی یک متخصص (Specialist) است.

  • یک توسعه‌دهنده‌ی معمولی معمولاً عمق زیادی در یک یا چند تکنولوژی خاص دارد.
  • یک معمار باید Breadth داشته باشد؛ یعنی آگاهی از طیف وسیعی از راه‌حل‌ها، الگوها و تکنولوژی‌ها، حتی اگر در همه‌ی آن‌ها عمق نداشته باشد.

نویسندگان این تفاوت را با مدل “دانش به‌شکل T” توضیح می‌دهند: توسعه‌دهنده معمولاً یک خط عمودی عمیق در یک حوزه دارد، اما معمار باید خط افقی گسترده‌تری از آگاهی داشته باشد تا بتواند trade-offهای بین گزینه‌های مختلف را بسنجد، حتی اگر مجبور شود عمق تخصصی‌اش را کمی کاهش دهد.


بخش بعدی: تحلیل Trade-Off ها

بعد از بحث Technical Breadth، نویسندگان به قلب تفکر معماری می‌رسند: تحلیل trade-off. آن‌ها با نقل‌قول معروف مارک ریچاردز آغاز می‌کنند: «معماری همان چیزی است که نمی‌توانی آن را در گوگل جست‌وجو کنی».

چرا هیچ پاسخ قطعی وجود ندارد

هر سؤال معماری معمولاً با «it depends» (بستگی دارد) پاسخ داده می‌شود، نه به این دلیل که معماران فرار می‌کنند، بلکه چون پاسخ واقعاً به محیط استقرار، انگیزه‌های کسب‌وکار، فرهنگ سازمان، بودجه، زمان‌بندی و مهارت تیم توسعه بستگی دارد. نیل فورد این را این‌گونه بیان می‌کند: «در معماری پاسخ درست یا غلط وجود ندارد، فقط trade-off وجود دارد».

مثال عملی: سیستم مزایده (Auction System)

نویسندگان یک سناریوی واقعی معرفی می‌کنند: سرویس Bid Producer باید مقدار پیشنهاد قیمت را به سه سرویس دیگر (Bid Capture، Bid Tracking، Bid Analytics) ارسال کند. سؤال این است: باید از Topic (الگوی Publish/Subscribe) استفاده شود یا از Queueهای جدا (الگوی Point-to-Point)؟

در نگاه اول، راه‌حل Topic برنده به نظر می‌رسد چون:

  • Bid Producer فقط به یک اتصال نیاز دارد، نه به سه اتصال جدا
  • اگر سرویس جدیدی مثل Bid History اضافه شود، هیچ تغییری در زیرساخت فعلی لازم نیست، فقط subscribe جدید کافی است
  • سرویس‌ها بیشتر decoupled هستند چون Bid Producer نمی‌داند داده‌اش توسط چه کسی و چگونه استفاده می‌شود

اما trade-off کجاست؟

نویسندگان تأکید می‌کنند: «برنامه‌نویسان مزایای همه‌چیز را می‌دانند اما هزینه‌های هیچ‌چیز را نمی‌دانند؛ معماران باید هر دو را بفهمند» (نقل از Rich Hickey). با تحلیل عمیق‌تر، معایب Topic هم روشن می‌شود:

ویژگیTopic (Pub/Sub)Queue (Point-to-Point)
توسعه‌پذیری معماریبالا 
امنیت و کنترل دسترسی دادهضعیف — هرکسی می‌تواند به داده گوش دهد 
قرارداد داده (Contract)همگن — همه باید یک ساختار را بپذیرند 
مانیتورینگ و Auto-scalingضعیف‌تر (به فناوری بستگی دارد) 

نتیجه‌ی این تحلیل چیزی است که هدف اصلی این فصل است: تفکر معماری یعنی پرسیدن این سؤال که «کدام مهم‌تر است: توسعه‌پذیری یا امنیت؟» و پاسخ همیشه به business drivers بستگی دارد، نه به یک قانون ثابت.

درک محرک‌های کسب‌وکار (Business Drivers)

تفکر معماری یعنی توانایی ترجمه‌ی نیازهای کسب‌وکار به architecture characteristics مثل scalability، performance و availability؛ کاری که نیاز به دانش دامنه‌ی کسب‌وکار و رابطه‌ی نزدیک با ذی‌نفعان دارد.

توازن بین معماری و کدنویسی

در پایان این فصل، نویسندگان به یک چالش عملی می‌پردازند: چگونه معمار می‌تواند هم‌زمان دستی در کد داشته باشد و هم مسئولیت معماری را انجام دهد؟ آن‌ها هشدار می‌دهند از bottleneck trap بپرهیزید — یعنی وقتی معمار مالکیت کد مسیر بحرانی (critical path) را می‌گیرد و خودش تنگنای تیم می‌شود.

راهکارهای پیشنهادی برای حفظ درگیری فنی معمار عبارت‌اند از: واگذاری کد framework به تیم توسعه و پرداختن به یک قطعه کد کسب‌وکاری چند iteration بعد، ساخت proof-of-concept با کیفیت production، رفع بدهی فنی (technical debt) و باگ‌ها، اتوماسیون کارهای تکراری تیم، و انجام مرتب code review.


فصل ۳: ماژولاریتی (Modularity)

نویسندگان فصل جدید را با یک نقل‌قول تاریخی از Glenford Myers آغاز می‌کنند: «۹۵ درصد نوشته‌ها درباره‌ی معماری صرف تعریف فواید ماژولاریتی می‌شود، اما درباره‌ی چگونگی رسیدن به آن تقریباً چیزی گفته نمی‌شود». این فصل دقیقاً همان خلأ را پر می‌کند.

تعریف ماژول

نویسندگان ماژول را این‌گونه تعریف می‌کنند: گروه‌بندی منطقی از کد مرتبط به‌هم؛ می‌تواند مجموعه‌ای از کلاس‌ها در زبان شی‌گرا یا مجموعه‌ای از توابع در زبان تابعی باشد. نکته‌ی مهم این است که ماژولاریتی یک مفهوم منطقی است، نه لزوماً یک جداسازی فیزیکی؛ برای مثال قرار دادن چند کلاس در یک assembly به‌خاطر راحتی، از نظر منطقی ممکن است هنوز ماژولار به‌حساب بیاید، اما هنگام تلاش برای شکستن یک Monolith، همین کوپلینگ ناشی از بی‌دقتی در پارتیشن‌بندی، تبدیل به مانع می‌شود.

سه ابزار سنجش ماژولاریتی

برای بررسی کیفی ماژولاریتی، کتاب سه مفهوم اصلی معرفی می‌کند: Cohesion، Coupling و Connascence.

Cohesion (همبستگی درونی)

Cohesion یعنی «تا چه حد بخش‌های یک ماژول باید در همان ماژول باقی بمانند». نویسندگان هفت سطح از بهترین تا بدترین را فهرست می‌کنند:

نوع Cohesionتوضیح
Functionalبهترین حالت؛ همه‌ی اجزا برای یک هدف واحد کار می‌کنند
Sequentialخروجی یک ماژول ورودی ماژول دیگر می‌شود
Communicationalچند ماژول در یک زنجیره‌ی ارتباطی روی داده مشترک عمل می‌کنند
Proceduralماژول‌ها باید به ترتیب خاصی اجرا شوند
Temporalارتباط فقط بر اساس زمان‌بندی است (مثل عملیات Startup)
Logicalداده‌ها منطقاً مرتبط‌اند اما تابعی نیستند (مثل کلاس StringUtils)
Coincidentalبدترین حالت؛ فقط در یک فایل قرار دارند، ارتباط واقعی ندارند

نویسندگان یک متریک ساختاری به نام LCOM (Lack of Cohesion in Methods) را معرفی می‌کنند که از مجموعه‌ی متریک‌های Chidamber و Kemerer گرفته شده است. این متریک نشان می‌دهد چه‌قدر متدهای یک کلاس، فیلدهای مشترک را واقعاً به اشتراک می‌گذارند؛ اگر امتیاز LCOM بالا باشد، یعنی کلاس باید به چند کلاس کوچک‌تر تفکیک شود. نکته‌ی مهم: LCOM فقط ناهمبستگی ساختاری را پیدا می‌کند، نه ناهمبستگی منطقی — این دقیقاً بازتاب قانون دوم کتاب است: «چرا» مهم‌تر از «چگونه».

Coupling (وابستگی)

کتاب دو نوع کوپلینگ کلاسیک از کتاب Yourdon و Constantine (۱۹۷۹) را معرفی می‌کند:

  • Afferent coupling: تعداد اتصالات ورودی به یک ماژول
  • Efferent coupling: تعداد اتصالات خروجی از یک ماژول به سایر ماژول‌ها

سپس مفاهیم پیشرفته‌تر رابرت مارتین یعنی Abstractness، Instability و Distance from the Main Sequence معرفی می‌شوند که به معمار کمک می‌کنند تعادل بین انعطاف‌پذیری (abstractness) و پایداری (stability) یک کامپوننت را اندازه‌گیری کند.

Connascence (هم‌زایی)

Connascence مفهومی است که Jim Weirich دوباره محبوبش کرد و توضیح می‌دهد چگونه دو بخش از کد باید هم‌زمان تغییر کنند تا سیستم درست کار کند. کتاب دو قانون کلیدی از Meilir Page-Jones نقل می‌کند:

Rule of Degree: انواع قوی connascence را به انواع ضعیف‌تر تبدیل کن. Rule of Locality: هرچه فاصله‌ی بین اجزای نرم‌افزار بیشتر شود، باید از انواع ضعیف‌تر connascence استفاده کنی.

نویسندگان تأکید می‌کنند که این مفاهیم اگرچه از دهه‌های قبل به‌جا مانده‌اند، اما هنوز ناقص هستند و پاسخ سؤال مدرن معماران — ارتباط Synchronous یا Asynchronous بین سرویس‌ها — را نمی‌دهند؛ موضوعی که در فصل‌های بعدی به آن پرداخته می‌شود.


بخش بعدی: از ماژول به کامپوننت

بخش پایانی فصل ۳ به این پرسش می‌پردازد: بعد از این‌که معمار ماژول‌ها را با ابزارهای Cohesion، Coupling و Connascence تحلیل کرد، چگونه این مفاهیم نظری تبدیل به بلوک‌های ساختاری واقعی معماری، یعنی Componentها، می‌شوند؟

تفاوت ماژول و کامپوننت

نویسندگان روشن می‌کنند که Module یک مفهوم منطقی برای گروه‌بندی کد مرتبط است (کلاس‌ها یا توابع)، اما Component واحد فیزیکی معماری است — چیزی که واقعاً استقرار (deploy) می‌شود. در دنیای جاوا این معمولاً یک JAR file است، در .NET یک Assembly یا DLL، و در Node.js یک ماژول npm.

نکته‌ی کلیدی این است که کامپوننت، ظرف فیزیکی است که ماژول‌های منطقی درون آن قرار می‌گیرند. یعنی معمار باید تصمیم بگیرد که کدام گروه از کلاس‌های منسجم (Cohesive) باید در کدام artifact فیزیکی بسته‌بندی شوند.

چرا این گذار مهم است

نویسندگان تأکید می‌کنند این‌جا دقیقاً جایی است که تئوری مدولاریتی وارد عمل معماری واقعی می‌شود:

  • کیفیت پارتیشن‌بندی کامپوننت‌ها مستقیماً بر توانایی تیم برای تفکیک آینده‌ی Monolith به سرویس‌های کوچک‌تر اثر می‌گذارد.
  • اگر ماژول‌های به‌ظاهر جدا، در سطح کامپوننت به‌شدت coupled بسته‌بندی شوند، این کوپلینگ مخفی، بعدها هنگام refactor یا migration به معماری توزیع‌شده، مانع بزرگی ایجاد می‌کند.
  • بنابراین، تصمیم درباره‌ی نحوه‌ی گروه‌بندی فیزیکی کامپوننت‌ها، خودش یک architecture decision حساس است، نه صرفاً یک انتخاب سازمانی ساده.

این مفهوم پلی است به فصل ۴ (Architecture Characteristics Defined) و فصل ۸ (Component-Based Thinking)، جایی که کتاب با جزئیات بیشتری نشان می‌دهد چگونه یک معمار باید کامپوننت‌ها را شناسایی، طراحی و پارتیشن‌بندی کند.


فصل ۴: تعریف مشخصه‌های معماری (Architecture Characteristics)

نویسندگان این فصل را با نقدی بر عبارت قدیمی «-ilities» آغاز می‌کنند و توضیح می‌دهند چرا ترجیح می‌دهند به‌جای این اصطلاح، از عبارت دقیق‌تر Architecture Characteristics استفاده کنند؛ چون واژه‌ی «-ility» بیشتر به ارزیابی پس از ساخت اشاره دارد، در حالی‌که این مشخصه‌ها باید از ابتدای طراحی در نظر گرفته شوند.

سه معیار تعریف یک Architecture Characteristic

کتاب یک تعریف دقیق و سه‌بخشی ارائه می‌دهد؛ چیزی فقط زمانی «مشخصه‌ی معماری» محسوب می‌شود که هر سه شرط زیر را داشته باشد:

  1. یک ملاحظه‌ی طراحی غیردامنه‌ای (Nondomain) را مشخص می‌کند — یعنی به «چگونگی» ساخت سیستم مربوط است، نه به «چه‌کاری» سیستم باید انجام دهد. برای مثال، هیچ سند نیازمندی نمی‌گوید «از بدهی فنی جلوگیری کن»، اما این یک دغدغه‌ی طراحی همیشگی است.
  2. بر یک جنبه‌ی ساختاری طراحی اثر می‌گذارد — یعنی نیاز به تصمیم ساختاری خاصی دارد. نویسندگان مثال زیبایی می‌زنند: اگر سیستم فقط به یک پردازشگر پرداخت شخص‌ثالث متصل شود، امنیت فقط نیاز به رعایت hygiene استاندارد دارد؛ اما اگر خودِ سیستم پردازش پرداخت را انجام دهد، آن‌وقت امنیت باید به یک ماژول یا سرویس مجزا تبدیل شود — یعنی ساختار را تغییر می‌دهد.
  3. برای موفقیت اپلیکیشن حیاتی یا مهم است — چون هر مشخصه‌ی اضافه، پیچیدگی بیشتری به طراحی تحمیل می‌کند، وظیفه‌ی معمار انتخاب کمترین مشخصه‌های ضروری است، نه بیشترین مشخصه‌های ممکن.

تفکیک Implicit و Explicit

کتاب مشخصه‌ها را به دو دسته تقسیم می‌کند:

نوعتوضیحمثال
Explicitمستقیماً در سند نیازمندی ذکر شده‌اند«سیستم باید از هزاران کاربر همزمان پشتیبانی کند»
Implicitهرگز در نیازمندی نوشته نمی‌شوند، اما برای موفقیت پروژه ضروری‌اندAvailability، Reliability، Security

مثال گویا: یک شرکت High-Frequency Trading معمولاً «latency پایین» را در سند نیازمندی نمی‌نویسد، چون برایشان بدیهی است؛ اما معمار باید از دانش دامنه‌ی کسب‌وکار خودش این مشخصه‌ی implicit را کشف کند.

مثال Case Study: Silicon Sandwiches

در ادامه، کتاب یک مطالعه‌ی موردی (فروشگاه ساندویچ با اپلیکیشن سفارش آنلاین) را معرفی می‌کند تا نشان دهد چگونه از عبارت‌های ساده‌ی business requirements، مشخصه‌های معماری استخراج می‌شوند:

  • وقتی نیازمندی می‌گوید «سیستم باید امروز هزاران و شاید روزی میلیون‌ها کاربر را پشتیبانی کند»، معمار باید این را به Scalability ترجمه کند.
  • اما نکته‌ی ظریف‌تر این‌جاست: کتاب بین Scalability و Elasticity تفاوت قائل می‌شود. Scalability یعنی سیستم بتواند تعداد بالای کاربر همزمان را بدون افت شدید کارایی مدیریت کند (مثل یک سیستم رزرو هتل که ترافیک نسبتاً پایدار دارد)؛ اما Elasticity یعنی سیستم بتواند انفجار ناگهانی ترافیک را تحمل کند (مثل لحظه‌ی شروع فروش بلیت کنسرت).
  • نکته‌ی مهم: نیازمندی هرگز کلمه‌ی Elasticity را به‌کار نمی‌برد، اما معمار باهوش باید از ماهیت دامنه (ساندویچ‌فروشی که احتمالاً موج ترافیک در وقت ناهار دارد) این مشخصه‌ی implicit را استخراج کند.

این مثال دقیقاً نشان می‌دهد که کار معمار فقط خواندن سند نیازمندی نیست؛ بلکه رمزگشایی از زبان دامنه‌ی کسب‌وکار به معادل‌های مهندسی است.


فصل ۵: شناسایی مشخصه‌های معماری

این فصل به پرسش عملی می‌پردازد: معمار چگونه از میان انبوه جملات کسب‌وکاری، مشخصه‌های معماری واقعی را استخراج می‌کند؟ نویسندگان دو مسیر اصلی را معرفی می‌کنند: استخراج از دغدغه‌های کلان دامنه (Domain Concerns) و استخراج از نیازمندی‌های مشخص (Requirements).

ترجمه‌ی دغدغه‌های کسب‌وکار به مشخصه‌های معماری

کتاب یک جدول کلیدی ارائه می‌دهد که نشان می‌دهد چگونه دغدغه‌های سطح بالای کسب‌وکار باید به چند مشخصه‌ی معماری هم‌زمان ترجمه شوند، نه فقط یکی:

دغدغه‌ی کسب‌وکارمشخصه‌های معماری معادل
ادغام و تملیک شرکت‌هاInteroperability، Scalability، Adaptability، Extensibility
ورود سریع به بازارAgility، Testability، Deployability
رضایت کاربرPerformance، Availability، Fault tolerance، Testability، Deployability، Agility، Security
مزیت رقابتیAgility، Testability، Deployability، Scalability، Availability، Fault tolerance
زمان و بودجهSimplicity، Feasibility

دام رایج: تک‌بعدی دیدن یک دغدغه

نویسندگان هشدار مهمی می‌دهند: «Agility» با «Time to Market» یکی نیست؛ در واقع Time to Market برابر است با Agility + Testability + Deployability. آن‌ها مثالی واقعی می‌زنند: وقتی یک ذی‌نفع می‌گوید «به‌دلیل الزامات قانونی، باید قیمت‌گذاری پایان‌روز صندوق‌ها به‌موقع انجام شود»، معمار ضعیف فقط روی Performance تمرکز می‌کند. اما معمار خوب می‌داند که این نیاز هم‌زمان به Availability، Scalability، Reliability، Recoverability و Auditability هم وابسته است — چون سرعت بی‌فایده است اگر سیستم موقع محاسبه از کار بیفتد یا نتواند از نقطه‌ی قطع‌شده ادامه دهد.

ابزار یادگیری: Architecture Katas

برای تربیت معماران تازه‌کار، Ted Neward مفهوم Architecture Kata را ابداع کرد (برگرفته از هنرهای رزمی ژاپنی، به‌معنای تمرین فردی برای تقویت تکنیک). ایده این است که تیم‌های کوچک در ۴۵ دقیقه یک مشکل دامنه‌محور را طراحی کنند، سپس نتیجه را با سایر گروه‌ها مقایسه کنند. هر kata شامل چهار بخش استاندارد است:

  • Description: توضیح کلی مشکل دامنه
  • Users: تعداد و نوع کاربران مورد انتظار
  • Requirements: نیازمندی‌های سطح دامنه
  • Additional context: نکاتی که در نیازمندی‌ها نیستند اما بر طراحی اثر می‌گذارند

مطالعه‌ی موردی: Silicon Sandwiches

کتاب یک kata واقعی معرفی می‌کند: فروشگاه ساندویچ ملی‌ای که می‌خواهد سفارش آنلاین راه‌اندازی کند، با هزاران و شاید روزی میلیون‌ها کاربر.

از میان نیازمندی‌ها، اولین نکته‌ای که باید توجه معمار را جلب کند، تعداد کاربران است که مستقیماً به Scalability اشاره دارد. اما نویسندگان تأکید می‌کنند که این تنها نیمی از داستان است؛ باید Elasticity (تحمل انفجار ناگهانی ترافیک) را هم در نظر گرفت، چون یک ساندویچ‌فروشی احتمالاً موج ترافیک در وقت ناهار دارد، حتی اگر این نکته هرگز در نیازمندی نوشته نشده باشد.

نویسندگان تفاوت این دو را با دو نمودار متفاوت نشان می‌دهند: Scalability رفتار سیستم را در برابر تعداد کاربران همزمان اندازه می‌گیرد (مثل سیستم رزرو هتل با ترافیک نسبتاً ثابت)، در حالی‌که Elasticity رفتار سیستم را در برابر موج ناگهانی ترافیک می‌سنجد (مثل سیستم فروش بلیت کنسرت). برخی سیستم‌ها Scalable هستند اما Elastic نیستند، و برخی هر دو را نیاز دارند.


بخش بعدی: تحلیل کامل مطالعه‌ی موردی Silicon Sandwiches

نویسندگان اکنون نیازمندی‌های فروشگاه ساندویچ را یک‌به‌یک بررسی می‌کنند تا نشان دهند چگونه هر جمله‌ی به‌ظاهر ساده‌ی کسب‌وکاری می‌تواند (یا نمی‌تواند) به یک مشخصه‌ی معماری تبدیل شود.

تحلیل نیازمندی‌های Explicit

کتاب نه نیازمندی مشخص را بررسی می‌کند و نتیجه‌ی معماری هر یک را استخراج می‌کند:

نیازمندینتیجه‌ی معماری
مسیر و ترافیک از سرویس نقشه‌ی خارجینگرانی درباره‌ی Reliability؛ اما معمار نباید سیستم را بیش‌ازحد fragile طراحی کند — اگر سرویس نقشه در دسترس نبود، سایت باید بدون آن هم کار کند، نه از کار بیفتد
ارسال سفارش با پیکهیچ مشخصه‌ی خاصی نیاز نیست
دسترسی از موبایلبه‌جای چند اپلیکیشن Native، یک وب‌اپلیکیشن Mobile-optimized با تمرکز روی Performance صفحه پیشنهاد می‌شود
تخفیف‌های ملی و محلیاستخراج مشخصه‌ی Customizability — می‌تواند با معماری Microkernel یا با Design Pattern مثل Template Method حل شود
پرداخت آنلاین/حضوری/هنگام تحویلامنیت implicit، بدون نیاز به سطح خاصی فراتر از حد معمول
فرنچایز با مالکان مختلفممکن است محدودیت هزینه تحمیل کند؛ معمار باید Feasibility را بررسی کند
برنامه‌ی گسترش بین‌المللینیاز به i18n، اما نیازی به ساختار خاص معماری ندارد؛ فقط Design را هدایت می‌کند
هدف استخدام نیروی ارزانبیشتر به Usability مربوط است تا معماری

سومین مشخصه‌ی کلیدی که از این تحلیل استخراج می‌شود، Performance است؛ اما نویسندگان تأکید می‌کنند Performance باید همیشه در ارتباط با Scalability تعریف شود، نه به‌تنهایی — یعنی باید مشخص شود «چه سطحی از Performance در چه سطحی از بار».

تحلیل مشخصه‌های Implicit

سه مشخصه‌ی implicit که هیچ‌جا در نیازمندی نوشته نشده‌اند، اما برای موفقیت حیاتی‌اند:

  • Availability: کاربران باید همیشه بتوانند به سایت دسترسی داشته باشند
  • Reliability: ارتباط طی تعامل قطع نشود (کسی دوست ندارد وسط خرید دوباره لاگین کند)
  • Security: چون پرداخت به شخص‌ثالث سپرده شده، فقط رعایت hygiene استاندارد امنیتی کافی است، نه ساختار خاص

آخرین مشخصه، Customizability است که از سه نیازمندی مختلف (رسپی، تخفیف محلی، مسیر محلی) استخراج می‌شود.

نکته‌ی طلایی: هیچ پاسخ درستی وجود ندارد، فقط پاسخ‌های گران وجود دارد

نویسندگان با نقل‌قول معروف مارک ریچاردز این تحلیل را جمع‌بندی می‌کنند: «هیچ پاسخ غلطی در معماری وجود ندارد، فقط پاسخ‌های گران‌قیمت وجود دارد». سپس یک تکنیک عملی معرفی می‌کنند: بعد از شناسایی مشخصه‌ها، از تیم بپرسید «اگر مجبور بودیم یکی را حذف کنیم، کدام بود؟» — این تمرین به شناسایی مشخصه‌های واقعاً ضروری کمک می‌کند. برای Silicon Sandwiches، معمار می‌تواند Customizability را حذف کند و آن را به سطح Design (نه Architecture) منتقل کند.

Design در برابر Architecture: مثال Customizability

نویسندگان با مثال Customizability نشان می‌دهند که مرز بین Architecture و Design همیشه روشن نیست: می‌توان آن را با یک سبک معماری کامل (Microkernel) حل کرد، یا با یک Design Pattern ساده (Template Method) در دل یک معماری دیگر. تصمیم بین این دو به تحلیل trade-off در سه بعد بستگی دارد: هزینه، تأثیر بر سایر مشخصه‌ها، و همکاری با تیم توسعه.


Governance و Fitness Functions

بعد از شناسایی و اولویت‌بندی مشخصه‌ها، سؤال بعدی این است: چگونه معمار می‌تواند تضمین کند که توسعه‌دهندگان همیشه این اولویت‌ها را رعایت می‌کنند؟ نویسندگان Governance را از واژه‌ی یونانی kubernan (به‌معنای «هدایت‌کردن») ریشه‌یابی می‌کنند و آن را یکی از مسئولیت‌های اصلی معمار می‌دانند.

مفهوم Fitness Function

راه‌حل اصلی کتاب برای این چالش، وام‌گرفتن مفهومی از کتاب دیگر نویسندگان (Building Evolutionary Architectures) است: Fitness Function. این مفهوم از دنیای Evolutionary Computing و الگوریتم‌های ژنتیک گرفته شده؛ در آن‌جا fitness function معیاری است که نشان می‌دهد یک راه‌حل چقدر به هدف مطلوب نزدیک است (مثلاً در مسئله‌ی فروشنده‌ی دوره‌گرد، طول مسیر).

نویسندگان این تعریف رسمی را ارائه می‌دهند:

Architecture Fitness Function: هر مکانیزمی که یک ارزیابی عینی (Objective) از یک یا چند مشخصه‌ی معماری ارائه می‌دهد.

نکته‌ی کلیدی، عبارت «هر مکانیزمی» است — یعنی Fitness Function چارچوب یا ابزار جدیدی برای دانلود نیست، بلکه زاویه‌ی دید تازه‌ای روی ابزارهای موجود است: می‌تواند metric، monitor، کتابخانه‌ی unit testing، یا حتی Chaos Engineering باشد.

مثال عملی: Cyclic Dependencies

کتاب یک مثال بسیار کاربردی می‌زند: در IDEهای جاوا یا دات‌نت، وقتی یک کلاس import نشده استفاده می‌شود، IDE پیشنهاد auto-import می‌دهد. توسعه‌دهندگان معمولاً بدون فکر این پیشنهاد را می‌پذیرند، که می‌تواند منجر به Cyclic Dependencies بین کامپوننت‌ها شود — فاجعه‌ای برای Modularity، چون دیگر نمی‌توان یک کامپوننت را بدون آورد بقیه استفاده کرد.

Code Review برای جلوگیری از این مشکل خیلی دیر است، چون آسیب از قبل به کد پایه وارد شده. راه‌حل نوشتن یک fitness function با ابزار JDepend است که در هر build بررسی می‌کند آیا cycle ای بین پکیج‌ها وجود دارد یا نه:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CycleTest {
    private JDepend jdepend;
    @BeforeEach
    void init() {
        jdepend = new JDepend();
        jdepend.addDirectory("/path/to/project/persistence/classes");
        jdepend.addDirectory("/path/to/project/web/classes");
    }
    @Test
    void testAllPackages() {
        Collection packages = jdepend.analyze();
        assertEquals("Cycles exist", false, jdepend.containsCycles());
    }
}

این fitness function را می‌توان در continuous build پروژه سیم‌کشی کرد تا معمار دیگر لازم نباشد دائماً بالای سر توسعه‌دهندگان بایستد. این نمونه‌ی خوبی از یک fitness function است که یک دغدغه‌ی مهم اما نه فوری (Important but not Urgent) را محافظت می‌کند.

ابزارهای تخصصی: ArchUnit و NetArchTest

برای حفاظت از مرزهای معماری لایه‌ای، ابزار ArchUnit (در جاوا) اجازه می‌دهد معمار قوانین دسترسی بین لایه‌ها را به‌صورت کد بنویسد:

1
2
3
4
5
6
7
layeredArchitecture()
    .layer("Controller").definedBy("..controller..")
    .layer("Service").definedBy("..service..")
    .layer("Persistence").definedBy("..persistence..")
    .whereLayer("Controller").mayNotBeAccessedByAnyLayer()
    .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
    .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service")

معادل این ابزار در دات‌نت، NetArchTest است که امکان مشابهی برای جلوگیری از دسترسی مستقیم لایه‌ی Presentation به لایه‌ی Data فراهم می‌کند.

Fitness Function در سطح Production: Netflix Simian Army

نویسندگان مثال معروف Netflix را معرفی می‌کنند که وقتی به Cloud مهاجرت کرد، نگران از‌دست‌دادن کنترل عملیاتی بود. راه‌حل، خلق رشته‌ی Chaos Engineering و ساخت Simian Army بود:

  • Conformity Monkey: بررسی می‌کند سرویس‌ها قوانین حاکمیتی را رعایت می‌کنند (مثلاً پاسخ‌گویی صحیح به verbهای RESTful)
  • Security Monkey: سرویس‌ها را از نظر آسیب‌پذیری‌های امنیتی شناخته‌شده بررسی می‌کند
  • Janitor Monkey: سرویس‌های یتیم (بدون هیچ consumer) را شناسایی و حذف می‌کند تا هزینه‌ی cloud کاهش یابد
  • Chaos Monkey / Chaos Kong: به‌ترتیب شبیه‌سازی خرابی تصادفی سرویس‌ها و کل یک data center برای تست تاب‌آوری سیستم

نویسندگان با ارجاع به کتاب The Checklist Manifesto این رویکرد را توجیه می‌کنند: fitness function مثل چک‌لیست خلبانان است — نه به‌دلیل نادانی توسعه‌دهندگان، بلکه چون در کارهای تکراری و پرجزئیات، جزئیات مهم به‌راحتی از قلم می‌افتند.


فصل ۷: دامنه‌ی مشخصه‌های معماری (Scope)

این فصل یک بازاندیشی بنیادین در نحوه‌ی نگاه به مشخصه‌های معماری ارائه می‌دهد: نویسندگان توضیح می‌دهند که تا یک دهه‌ی پیش، فرض رایج در صنعت این بود که مشخصه‌های معماری (مثل Scalability) باید در سطح کل سیستم تعریف شوند، چون تقریباً همه‌ی سیستم‌ها Monolithic بودند. اما با ظهور سبک‌هایی مثل Microservices، این دامنه به‌شدت باریک‌تر شده است.

مشکل معیارهای قدیمی

معیارهای سطح کد که در فصل قبل معرفی شدند (مثل Afferent/Efferent Coupling)، فقط جزئیات درون کد را نشان می‌دهند و نمی‌توانند اجزای وابسته‌ی خارج از کد (مثل دیتابیس) را ارزیابی کنند — در حالی‌که این اجزا مستقیماً روی مشخصه‌های عملیاتی اثر می‌گذارند. نویسندگان مثال می‌زنند: هرقدر هم که یک معمار روی طراحی یک کد‌بیس elastic تلاش کند، اگر آن سیستم به دیتابیسی متصل باشد که با این ویژگی هم‌خوانی ندارد، کل سیستم موفق نخواهد بود.

Connascence: ابزار تحلیل کوپلینگ

برای حل این مشکل، نویسندگان مفهوم Connascence را از کتاب Meilir Page-Jones (۱۹۹۶) وام می‌گیرند و آن را این‌گونه تعریف می‌کنند: دو کامپوننت connascent هستند اگر تغییر در یکی، نیازمند تغییر در دیگری برای حفظ صحت کلی سیستم باشد.

آن‌ها یک نوع جدید از Dynamic Connascence معرفی می‌کنند که ویژه‌ی معماری‌های توزیع‌شده است:

  • Synchronous Connascence: فراخوانی که caller منتظر پاسخ callee می‌ماند
  • Asynchronous Connascence: ارتباط fire-and-forget که در معماری‌های Event-Driven دیده می‌شود و به دو سرویس اجازه می‌دهد در ویژگی‌های عملیاتی خود تفاوت داشته باشند

تعریف Architectural Quantum

با ترکیب این مفاهیم، نویسندگان واحد اندازه‌گیری جدیدی معرفی می‌کنند: Architecture Quantum — یک artifact که به‌طور مستقل قابل‌استقرار (Independently Deployable) است، دارای Functional Cohesion بالا، و دارای Synchronous Connascence است. این تعریف سه بخش کلیدی دارد:

  • Independently deployable: quantum شامل همه‌ی اجزای لازم برای کارکرد مستقل است؛ برای مثال دیتابیس هم بخشی از quantum محسوب می‌شود، چون بدون آن سیستم کار نمی‌کند. این یعنی سیستم‌های Legacy با یک دیتابیس مشترک، در واقع فقط یک quantum دارند؛ اما در Microservices که هر سرویس دیتابیس خودش را دارد، چندین quantum مجزا شکل می‌گیرد.
  • High functional cohesion: یعنی کد موجود در quantum هدف واحدی را دنبال می‌کند (مثل یک کامپوننت Customer در برابر یک کامپوننت Utility بی‌هدف).
  • Synchronous connascence: اگر یک سرویس به‌صورت synchronous سرویس دیگری را صدا بزند، این دو نمی‌توانند تفاوت شدیدی در مشخصه‌های عملیاتی داشته باشند؛ وگرنه timeout و مشکلات Reliability رخ می‌دهد.

مثال Payment و Auction

نویسندگان مثالی می‌زنند: اگر سرویس Payment فقط بتواند هر ۵۰۰ میلی‌ثانیه یک پرداخت را مدیریت کند، وقتی چند حراج هم‌زمان تمام شوند چه اتفاقی می‌افتد؟ اگر ارتباط Synchronous باشد، یا صف تشکیل می‌شود یا درخواست‌ها timeout می‌خورند؛ اما اگر معمار یک لینک Asynchronous بین این دو سرویس طراحی کند، صف پیام می‌تواند این تفاوت را به‌طور موقت جبران کند و معماری منعطف‌تری شکل می‌گیرد.

ارتباط با Bounded Context در DDD

کتاب این بحث را به مفهوم Bounded Context از کتاب Domain-Driven Design اریک ایوانز مرتبط می‌کند: پیش از DDD، توسعه‌دهندگان به دنبال بازاستفاده‌ی یکپارچه از موجودیت‌های مشترک بودند (مثلاً یک کلاس Customer واحد در کل سازمان)، که باعث Coupling و پیچیدگی هماهنگی می‌شد. Bounded Context به هر دامنه‌ی مشکل اجازه می‌دهد نسخه‌ی خودش را بسازد و اختلافات را در نقاط یکپارچه‌سازی حل کند — دقیقاً همان چیزی که مفهوم Architecture Quantum دامنه‌ی جدیدی برای تعریف مشخصه‌های معماری به آن می‌دهد.

مطالعه‌ی موردی: Going, Going, Gone

برای نمایش این مفاهیم، کتاب یک Architecture Kata جدید معرفی می‌کند: یک شرکت حراجی که می‌خواهد حراج‌های خود را به‌صورت آنلاین و در مقیاس ملی برگزار کند، با هزاران شرکت‌کننده‌ی هم‌زمان در هر حراج. نیازمندی‌های کلیدی شامل واقعی‌بودن هرچه‌بیشتر (Real-time)، ثبت کارت اعتباری، ردیابی شاخص اعتبار شرکت‌کنندگان، پخش زنده‌ی ویدیو، و ثبت دقیق ترتیب پیشنهادها است؛ نکته‌ی جالب این‌که این شرکت به‌تازگی از یک دعوی حقوقی مربوط به کلاه‌برداری خارج شده، که این خودش یک implicit concern مهم درباره‌ی Auditability و Security ایجاد می‌کند.


فصل ۸: تفکر کامپوننت‌محور و تحلیل کامل Going, Going, Gone

با تعریف Architecture Quantum در فصل قبل، اکنون کتاب نشان می‌دهد چگونه این مفهوم عملاً روی طراحی کامپوننت‌های سیستم حراج آنلاین اثر می‌گذارد.

شناسایی کامپوننت‌های اولیه

معمار در گام اول، کامپوننت‌های اولیه‌ی سیستم را صرفاً بر اساس عملکرد (Functionality) شناسایی می‌کند:

  • BidCapture: دریافت پیشنهادها هم از حراج‌گر (Auctioneer) و هم از شرکت‌کنندگان
  • BidTracker: ردیابی پیشنهادها و عمل به‌عنوان system of record
  • AuctionSession: شروع و پایان حراج، شامل مراحل پرداخت و اطلاع‌رسانی پایان به شرکت‌کنندگان
  • Payment: پردازشگر پرداخت شخص‌ثالث برای کارت اعتباری

نقطه‌ی چرخش: تحلیل مشخصه‌های معماری روی کامپوننت‌ها

اینجا دقیقاً جایی است که مفهوم Architecture Quantum وارد عمل می‌شود. معمار با خودش می‌پرسد: آیا BidCapture که پیشنهادهای هم حراج‌گر و هم شرکت‌کنندگان را می‌گیرد، از نظر منطق کسب‌وکار درست است؟ بله. اما از نظر مشخصه‌های معماری چطور؟

نویسندگان نشان می‌دهند این دو جریان داده نیازهای بسیار متفاوتی دارند:

  • حراج‌گر نیازی به همان سطح Scalability یا Elasticity ندارد که هزاران شرکت‌کننده نیاز دارند
  • اما Reliability (عدم قطع ارتباط) و Availability برای حراج‌گر باید حتی بالاتر از سایر بخش‌های سیستم باشد؛ چون اگر یک شرکت‌کننده نتواند لاگین کند بد است، اما اگر خود حراج‌گر ارتباطش قطع شود، فاجعه‌بار است

به‌همین دلیل، معمار تصمیم می‌گیرد BidCapture را به دو کامپوننت مجزا تقسیم کند: BidCapture (برای شرکت‌کنندگان) و AuctioneerCapture (برای حراج‌گر)، تا هرکدام بتوانند مجموعه‌ی متفاوتی از مشخصه‌های معماری را پشتیبانی کنند. در طراحی جدید، کامپوننت BidTracker نقش یکپارچه‌سازی این دو جریان کاملاً متفاوت اطلاعاتی را ایفا می‌کند.

درس کلیدی: هیچ طراحی «درست» واحدی وجود ندارد

نویسندگان تأکید می‌کنند این طراحی احتمالاً نهایی نیست و نیازمندی‌های بیشتری (مثل ثبت‌نام کاربران یا مدیریت پرداخت) باید کشف شوند. اما نکته‌ی مهم‌تر این جمله‌ی راهنماست: به‌عنوان معمار، هرگز به‌دنبال «یک طراحی درست واحد» نباش، چون بسیاری از طراحی‌ها کافی هستند (و به‌احتمال کمتر over-engineered خواهند بود)؛ به‌جای آن، trade-offها را عینی ارزیابی کن و طراحی‌ای را انتخاب کن که کمترین مجموعه‌ی بد از trade-offها را داشته باشد.

تصمیم نهایی: مونولیت یا توزیع‌شده؟

با بازگشت به تعریف Architecture Quantum، نویسندگان نشان می‌دهند این مفهوم مستقیماً به یک تصمیم بنیادین منجر می‌شود: آیا معماری باید Monolithic باشد یا Distributed؟

قاعده‌ی کلی این‌گونه است: اگر سیستم بتواند با یک quantum واحد (یعنی یک مجموعه‌ی مشترک از مشخصه‌های معماری) کار کند، معماری Monolithic مزایای زیادی دارد. اما اگر کامپوننت‌های مختلف نیاز به مشخصه‌های متفاوتی داشته باشند — مثل تفاوت بین BidCapture و AuctioneerCapture، یا بین VideoStreamer/BidStreamer (که فقط جریان read-only هستند) و بخش‌های high-scale update — این تفاوت‌ها معمار را به‌سوی معماری Distributed سوق می‌دهد.

این دقیقاً ارزش عملی مفهوم Architecture Quantum را نشان می‌دهد: تشخیص یک ویژگی بنیادین طراحی (مونولیت در برابر توزیع‌شده) در همان مراحل اولیه‌ی فرآیند طراحی، پیش از آن‌که تیم زمان و هزینه‌ی زیادی صرف یک مسیر اشتباه کند.


Part II: سبک‌های معماری — پایه و شروع

بعد از تثبیت مفاهیم بنیادین (مشخصه‌ها، Quantum، کامپوننت)، کتاب وارد بخش دوم می‌شود: بررسی سبک‌های معماری مشخص. فصل ۹ زمینه‌ی تاریخی و مفهومی لازم برای فهم این سبک‌ها را فراهم می‌کند.

از Unitary تا Client/Server

نویسندگان با یک نگاه تاریخی شروع می‌کنند: در آغاز، سخت‌افزار و نرم‌افزار یک موجودیت واحد بودند (Unitary Architecture)؛ اما با پیچیده‌ترشدن نیازها، این دو از هم جدا شدند و امروز معماری Unitary تقریباً فقط در سیستم‌های Embedded دیده می‌شود.

نقطه‌ی عطف بعدی، Client/Server یا معماری دو-لایه (Two-Tier) بود که با تحول فناوری، چند نسخه به خود دید:

  • Desktop + Database Server: منطق UI روی دسکتاپ، پردازش سنگین روی سرور دیتابیس
  • Browser + Web Server: مشابه قبلی، اما با کلاینت نازک‌تر (مرورگر) و دسترسی گسترده‌تر داخل و خارج فایروال
  • Three-Tier: با ظهور Application Serverها در جاوا و دات‌نت، لایه‌ی سوم (Application Tier) بین دیتابیس و رابط کاربری اضافه شد، همراه با پروتکل‌هایی مثل CORBA و DCOM

درس تاریخی: عواقب بلندمدت تصمیمات معماری

نویسندگان مثال جذابی از زبان جاوا می‌زنند: در دوران طراحی جاوا، معماری سه‌لایه بسیار رایج بود، پس طراحان زبان قابلیت Serialization را در هسته‌ی زبان جای دادند (هر Object باید از یک اینترفیس خاص پیاده‌سازی می‌کرد). آن سبک معماری آمد و رفت، اما این میراث هنوز در جاوا باقی مانده و طراحان امروزی زبان را برای حفظ Backward Compatibility محدود می‌کند — نمونه‌ای گویا از این‌که چگونه تصمیمات معماری می‌توانند برای دهه‌ها اثر بگذارند.

دسته‌بندی اصلی: Monolithic در برابر Distributed

نویسندگان یک طبقه‌بندی کلیدی ارائه می‌دهند که مبنای کل بخش دوم کتاب است: سبک‌های معماری به دو گروه اصلی تقسیم می‌شوند:

گروهسبک‌های معماری
Monolithic (یک واحد استقرار واحد)Layered، Pipeline، Microkernel
Distributed (چند واحد استقرار مرتبط از طریق پروتکل‌های remote)Service-Based، Event-Driven، Space-Based، Orchestration-Driven SOA، Microservices

این تقسیم‌بندی به این دلیل انتخاب شده که تمام سبک‌های Distributed مجموعه‌ی مشترکی از چالش‌ها و مسائل را دارند که در سبک‌های Monolithic دیده نمی‌شوند — نکته‌ای که کتاب در فصل بعدی به‌تفصیل با هشت Fallacy of Distributed Computing به آن می‌پردازد.


Part II: سبک‌های معماری — پایه و شروع

بعد از تثبیت مفاهیم بنیادین (مشخصه‌ها، Quantum، کامپوننت)، کتاب وارد بخش دوم می‌شود: بررسی سبک‌های معماری مشخص. فصل ۹ زمینه‌ی تاریخی و مفهومی لازم برای فهم این سبک‌ها را فراهم می‌کند.

از Unitary تا Client/Server

نویسندگان با یک نگاه تاریخی شروع می‌کنند: در آغاز، سخت‌افزار و نرم‌افزار یک موجودیت واحد بودند (Unitary Architecture)؛ اما با پیچیده‌ترشدن نیازها، این دو از هم جدا شدند و امروز معماری Unitary تقریباً فقط در سیستم‌های Embedded دیده می‌شود.

نقطه‌ی عطف بعدی، Client/Server یا معماری دو-لایه (Two-Tier) بود که با تحول فناوری، چند نسخه به خود دید:

  • Desktop + Database Server: منطق UI روی دسکتاپ، پردازش سنگین روی سرور دیتابیس
  • Browser + Web Server: مشابه قبلی، اما با کلاینت نازک‌تر (مرورگر) و دسترسی گسترده‌تر داخل و خارج فایروال
  • Three-Tier: با ظهور Application Serverها در جاوا و دات‌نت، لایه‌ی سوم (Application Tier) بین دیتابیس و رابط کاربری اضافه شد، همراه با پروتکل‌هایی مثل CORBA و DCOM

درس تاریخی: عواقب بلندمدت تصمیمات معماری

نویسندگان مثال جذابی از زبان جاوا می‌زنند: در دوران طراحی جاوا، معماری سه‌لایه بسیار رایج بود، پس طراحان زبان قابلیت Serialization را در هسته‌ی زبان جای دادند (هر Object باید از یک اینترفیس خاص پیاده‌سازی می‌کرد). آن سبک معماری آمد و رفت، اما این میراث هنوز در جاوا باقی مانده و طراحان امروزی زبان را برای حفظ Backward Compatibility محدود می‌کند — نمونه‌ای گویا از این‌که چگونه تصمیمات معماری می‌توانند برای دهه‌ها اثر بگذارند.

دسته‌بندی اصلی: Monolithic در برابر Distributed

نویسندگان یک طبقه‌بندی کلیدی ارائه می‌دهند که مبنای کل بخش دوم کتاب است: سبک‌های معماری به دو گروه اصلی تقسیم می‌شوند:

گروهسبک‌های معماری
Monolithic (یک واحد استقرار واحد)Layered، Pipeline، Microkernel
Distributed (چند واحد استقرار مرتبط از طریق پروتکل‌های remote)Service-Based، Event-Driven، Space-Based، Orchestration-Driven SOA، Microservices

این تقسیم‌بندی به این دلیل انتخاب شده که تمام سبک‌های Distributed مجموعه‌ی مشترکی از چالش‌ها و مسائل را دارند که در سبک‌های Monolithic دیده نمی‌شوند — نکته‌ای که کتاب در فصل بعدی به‌تفصیل با هشت Fallacy of Distributed Computing به آن می‌پردازد.


هشت Fallacy معماری‌های توزیع‌شده

نویسندگان توضیح می‌دهند که سبک‌های Distributed در ازای قدرت بیشتر در Performance، Scalability و Availability، مجموعه‌ای از چالش‌های ویژه‌ی خودشان را به همراه دارند که در معماری‌های Monolithic دیده نمی‌شوند. این چالش‌ها اولین بار در سال ۱۹۹۴ توسط L. Peter Deutsch و همکارانش در Sun Microsystems به‌عنوان Fallacies of Distributed Computing مطرح شدند؛ یعنی باورهایی که همه فکر می‌کنند درست است، اما درست نیست.

فهرست کامل هشت Fallacy

  • شبکه قابل‌اعتماد است: در واقع شبکه هیچ‌وقت کاملاً reliable نیست؛ به‌همین دلیل مفاهیمی مثل timeout و circuit breaker به‌وجود آمده‌اند. هرقدر سیستم بیشتر به شبکه متکی باشد (مثل Microservices)، پتانسیل عدم‌اعتماد بالاتر می‌رود.
  • Latency صفر است: یک فراخوانی local در حد نانوثانیه طول می‌کشد، اما فراخوانی remote (REST، messaging، RPC) در حد میلی‌ثانیه است. نویسندگان تأکید می‌کنند معمار باید هم میانگین و هم صدک ۹۵ تا ۹۹ام تأخیر شبکه‌ی خودش را بداند، چون معمولاً همین «long tail» عامل اصلی افت کارایی است؛ زنجیره‌کردن ۱۰ فراخوانی سرویس با میانگین ۱۰۰ میلی‌ثانیه، ۱۰۰۰ میلی‌ثانیه به درخواست اضافه می‌کند.
  • Bandwidth بی‌کران است: نویسندگان مفهوم Stamp Coupling را معرفی می‌کنند: مثلاً سرویس Customer Profile فقط برای نمایش نام مشتری، ۵۰۰ کیلوبایت داده (۴۵ فیلد) بازمی‌گرداند در حالی‌که فقط ۲۰۰ بایت لازم است؛ در مقیاس ۲۰۰۰ درخواست بر ثانیه، این معادل ۱ گیگابیت پهنای باند تلف‌شده در هر ثانیه است. راه‌حل‌ها شامل private RESTful endpoints، field selectors، GraphQL و contract-های مبتنی‌بر مصرف‌کننده است.
  • شبکه امن است: در معماری توزیع‌شده، هر endpoint باید مجزا امن شود؛ سطح حمله به‌شدت نسبت به مونولیت افزایش می‌یابد.
  • توپولوژی هرگز تغییر نمی‌کند: نویسندگان مثالی می‌زنند: یک به‌روزرسانی شبکه در ساعت ۲ بامداد می‌تواند فرضیات latency را باطل کند و باعث timeout سراسری شود؛ معمار باید در ارتباط دائم با تیم عملیات باشد.
  • فقط یک ادمین وجود دارد: در شرکت‌های بزرگ، ده‌ها ادمین شبکه‌ی مختلف وجود دارد؛ این پیچیدگی هماهنگی، مستقیماً به Fallacy توپولوژی مرتبط است.
  • هزینه‌ی انتقال صفر است: این با latency فرق دارد — به هزینه‌ی واقعی مالی زیرساخت (سرور، gateway، firewall، subnet) اشاره می‌کند که در معماری توزیع‌شده به‌مراتب بالاتر از مونولیت است.
  • شبکه همگن است: بسیاری از شرکت‌ها چند vendor سخت‌افزار شبکه دارند که همیشه به‌خوبی با هم کار نمی‌کنند؛ این fallacy به تمام fallacyهای قبلی (reliability، latency، bandwidth) بازمی‌گردد و یک حلقه‌ی بی‌پایان پیچیدگی ایجاد می‌کند.

پیام کلی این هشت Fallacy

نکته‌ی محوری این است که این‌ها صرفاً نکات تئوریک نیستند؛ هرکدام مستقیماً روی مشخصه‌های معماری (Reliability، Performance، Security) اثر می‌گذارند و معمار باید آن‌ها را در هر تصمیم مربوط به Distributed Architecture در نظر بگیرد. نویسندگان همچنین اشاره‌ی کوتاهی به مسائل دیگر مثل Distributed Logging می‌کنند: در مونولیت فقط یک لاگ وجود دارد، اما در معماری توزیع‌شده ده‌ها تا صدها لاگ باید برای ریشه‌یابی خطا بررسی شوند.


فصل ۱۰: سبک معماری Layered (لایه‌ای)

اولین سبک معماری مونولیتیک که کتاب معرفی می‌کند، Layered Architecture است؛ سبکی که به‌دلیل سادگی و آشنایی گسترده، رایج‌ترین نقطه‌ی شروع برای بسیاری از پروژه‌هاست.

توپولوژی و مفهوم Technical Partitioning

این معماری بر پایه‌ی Technical Partitioning بنا شده، یعنی کامپوننت‌ها بر اساس نقش فنی‌شان (مثل Presentation، Business، Persistence) دسته‌بندی می‌شوند نه بر اساس دامنه‌ی کسب‌وکار. هر لایه یک انتزاع حول کاری که باید انجام شود می‌سازد؛ مثلاً لایه‌ی Presentation نیازی ندارد بداند داده‌ی مشتری چگونه واکشی می‌شود، فقط باید آن را نمایش دهد. عارضه‌ی این طراحی این است که یک دامنه‌ی کسب‌وکار واحد مثل «Customer» در تمام لایه‌ها (از Presentation تا Database) پخش می‌شود، به‌همین دلیل Domain-Driven Design به‌خوبی با این سبک هم‌خوانی ندارد.

لایه‌های باز و بسته (Layers of Isolation)

مفهوم کلیدی این فصل، تفاوت بین لایه‌ی Closed و Open است:

  • Closed layer: درخواست باید حتماً از لایه‌ی زیرین بگذرد و نمی‌تواند لایه‌ای را رد کند
  • Open layer: درخواست می‌تواند از این لایه عبور کند و مستقیم به لایه‌ی پایین‌تر برسد (مانند الگوی قدیمی fast-lane reader)

نکته‌ی مهم اینجاست: برای حفظ Layers of Isolation (یعنی تغییر در یک لایه روی لایه‌های دیگر اثر نگذارد)، لایه‌های اصلیِ مسیر درخواست باید Closed باشند؛ در غیر این‌صورت تغییری در Persistence مستقیماً روی Presentation اثر می‌گذارد و سیستم بسیار Brittle و پرهزینه برای تغییر می‌شود. نویسندگان مثال عملی می‌زنند: با این تکنیک می‌توان لایه‌ی قدیمی JSF را با React.js جایگزین کرد بدون اینکه هیچ لایه‌ی دیگری تحت‌تأثیر قرار بگیرد.

چرا گاهی لایه باید Open باشد

گاهی برای مدیریت اشیای مشترک (مثل کلاس‌های Utility یا Logging) در لایه‌ی Business، معمار یک لایه‌ی Services جدید اضافه می‌کند تا Presentation نتواند مستقیم به آن اشیای مشترک دسترسی پیدا کند. در این حالت، لایه‌ی Business باید Closed بماند، اما لایه‌ی Services جدید باید Open علامت‌گذاری شود تا لایه‌ی Business بتواند از آن عبور کند و مستقیم به Persistence برسد.

آنتی‌پترن Architecture Sinkhole

یکی از خطرات اصلی این سبک، Architecture Sinkhole Anti-Pattern است: زمانی که درخواست فقط بدون هیچ منطق کسب‌وکاری از لایه‌ای به لایه‌ی دیگر عبور می‌کند (Pass-through خالص). نویسندگان قاعده‌ی ۸۰-۲۰ را پیشنهاد می‌دهند: اگر تنها ۲۰٪ درخواست‌ها sinkhole باشند مشکلی نیست، اما اگر ۸۰٪ درخواست‌ها این‌گونه باشند، این نشانه‌ی روشنی است که Layered Architecture انتخاب درستی برای آن دامنه نیست.

ارزیابی مشخصه‌های معماری

جدول امتیازدهی نویسندگان تصویر دقیقی از نقاط قوت و ضعف این سبک ارائه می‌دهد:

مشخصهامتیازدلیل
Simplicity و Costبالامونولیتیک، بدون پیچیدگی توزیع‌شده
Deployability و Testabilityخیلی پایین (۲ ستاره)یک تغییر ۳ خطی نیاز به استقرار کامل واحد دارد، ریسک بالا
Reliabilityمتوسط (۳ ستاره)نبود ترافیک شبکه، اما محدود به‌دلیل ریسک استقرار
Elasticity و Scalabilityخیلی پایین (۱ ستاره)به‌دلیل quantum واحد و دیتابیس مونولیتیک
Performanceپایین (۲ ستاره)نبود پردازش موازی و ریسک sinkhole
Fault Toleranceخیلی پایینیک خطای حافظه کل اپلیکیشن را از کار می‌اندازد، MTTR بالا (تا ۱۵ دقیقه)

نویسندگان توصیه می‌کنند این سبک برای اپلیکیشن‌های کوچک و ساده، یا زمانی که بودجه و زمان محدود است، بهترین انتخاب است؛ همچنین به‌عنوان نقطه‌ی شروع مناسب وقتی هنوز مطمئن نیستید کدام سبک معماری (مثلاً Microservices) درست است.


فصل ۱۱: سبک معماری Pipeline

دومین سبک معماری مونولیتیک که کتاب معرفی می‌کند، Pipeline Architecture است؛ سبکی که ریشه در Unix Shell دارد و برپایه‌ی دو مفهوم بنیادین یعنی Pipes و Filters بنا شده است.

Pipes: کانال ارتباطی یک‌طرفه

Pipeها کانال ارتباطی بین فیلترها را تشکیل می‌دهند. هر Pipe معمولاً Unidirectional و Point-to-Point است (نه Broadcast) تا کارایی بالاتری داشته باشد؛ ورودی را از یک منبع می‌گیرد و همیشه خروجی را به منبع بعدی هدایت می‌کند. نویسندگان تأکید می‌کنند معماران باید حجم داده‌ی کمتری روی Pipeها حمل کنند تا Performance بالاتری حاصل شود.

چهار نوع Filter

فیلترها مستقل، خودکفا و معمولاً Stateless هستند و باید فقط یک وظیفه انجام دهند؛ وظایف ترکیبی باید با زنجیره‌ای از فیلترها پیاده‌سازی شوند، نه یک فیلتر واحد. چهار نوع فیلتر در این سبک وجود دارد:

  • Producer: نقطه‌ی شروع فرآیند، فقط خروجی دارد (Source)
  • Transformer: ورودی می‌گیرد، تبدیلی روی داده انجام می‌دهد و به Pipe بعدی می‌فرستد (مشابه map در برنامه‌نویسی تابعی)
  • Tester: ورودی می‌گیرد، یک یا چند معیار را می‌آزماید و به‌صورت اختیاری خروجی تولید می‌کند (مشابه reduce)
  • Consumer: نقطه‌ی پایان جریان، معمولاً نتیجه‌ی نهایی را در دیتابیس ذخیره یا در UI نمایش می‌دهد

داستان جذاب Knuth در برابر McIlroy

نویسندگان یک داستان معروف از وبلاگ «More Shell, Less Egg» نقل می‌کنند: از Donald Knuth خواسته شد برنامه‌ای بنویسد که پرتکرارترین کلمات یک فایل متنی را بشمارد و او بیش از ۱۰ صفحه کد Pascal همراه با طراحی یک الگوریتم جدید نوشت. سپس Doug McIlroy با یک اسکریپت Shell که در یک توییت هم جا می‌شد، همان مسئله را ساده‌تر، شیک‌تر و قابل‌فهم‌تر حل کرد. این مثال قدرت شگفت‌انگیز انتزاع‌های ترکیبی pipes و filters را نشان می‌دهد.

مثال عملی: پردازش Telemetry با Kafka

کتاب یک مثال کاربردی ارائه می‌دهد: داده‌های Telemetry سرویس‌ها به یک Kafka Topic استریم می‌شود. فیلتر Producer به نام Service Info Capture داده را می‌گیرد و به فیلتر Tester به نام Duration Filter می‌فرستد تا تشخیص دهد داده مربوط به مدت‌زمان (Duration) است یا نه؛ اگر بله، به Transformer به نام Duration Calculator می‌رود، در غیر این‌صورت به Tester دیگری به نام Uptime Filter ارجاع می‌شود، و در نهایت نتیجه در فیلتر Consumer به نام Database Output در MongoDB ذخیره می‌شود. این طراحی نشان می‌دهد افزودن یک فیلتر جدید (مثلاً برای معیار جدید Database Connection Wait Time) بدون تأثیر روی فیلترهای دیگر ممکن است — نمونه‌ای از Extensibility بالا.

ارزیابی مشخصه‌های معماری

مشخصهامتیازدلیل
Cost و Simplicityبالامونولیتیک، بدون پیچیدگی توزیع‌شده
Modularityبالاجداسازی از طریق انواع فیلتر؛ هرکدام مستقل قابل تغییر
Deployability و Testabilityمتوسطکمی بهتر از Layered به‌دلیل ماژولاریتی فیلترها، اما همچنان محدودیت‌های مونولیت باقی است
Reliabilityمتوسط (۳ ستاره)نبود ترافیک شبکه، اما محدودیت‌های تست و استقرار مونولیت
Elasticity و Scalabilityخیلی پایین (۱ ستاره)یک Quantum واحد، تنها با تکنیک‌های پیچیده مثل Multithreading قابل بهبود
Fault Toleranceخیلی پایینیک خطای Out-of-Memory کل اپلیکیشن را از کار می‌اندازد، MTTR از ۲ تا بیش از ۱۵ دقیقه

فصل ۱۲: سبک معماری Microkernel

سومین سبک مونولیتیک، Microkernel Architecture است؛ سبکی که برخلاف دو سبک قبلی، توانایی منحصربه‌فردی در ترکیب Domain Partitioning و Technical Partitioning دارد.

Core System: کمینه‌ی عملکردی

Core System به‌عنوان حداقل عملکرد لازم برای اجرای سیستم تعریف می‌شود. نویسندگان مثال Eclipse IDE را می‌زنند: هسته‌ی Eclipse صرفاً یک ویرایشگر متن ساده است (باز کردن، ویرایش، ذخیره‌ی فایل)؛ تمام قابلیت‌های واقعی از طریق Plug-inها اضافه می‌شوند. تعریف دیگر Core System، «مسیر شادِ» پردازش (Happy Path) با پیچیدگی سیکلوماتیک کم است؛ منطق سفارشی و شرطی سنگین باید به Plug-inهای مجزا منتقل شود.

نویسندگان یک مثال کد از سیستم بازیافت الکترونیک ارائه می‌دهند: به‌جای زنجیره‌ی طولانی if-else برای ارزیابی هر مدل دستگاه در Core System، هر مدل به یک Plug-in مستقل تبدیل می‌شود که Core System از طریق یک Registry آن را پیدا و اجرا می‌کند. این الگو مستقیماً پیچیدگی سیکلوماتیک را کاهش می‌دهد و Testability و Extensibility را بهبود می‌بخشد.

Plug-in Components: استقلال و ارتباط

کامپوننت‌های Plug-in باید مستقل، خودکفا و بدون وابستگی به یکدیگر باشند. ارتباط آن‌ها با Core System معمولاً به‌صورت Point-to-Point (فراخوانی متد یا تابع) است، اما می‌تواند Compile-based یا Runtime-based (با فریم‌ورک‌هایی مثل OSGi، Jigsaw یا Prism) باشد.

نویسندگان یک نکته‌ی معماری مهم مطرح می‌کنند: Plug-inها همچنین می‌توانند از طریق REST یا Messaging با Core System ارتباط برقرار کنند و حتی به‌صورت میکروسرویس مستقل پیاده‌سازی شوند. اما هشدار می‌دهند که این کار همچنان یک Architecture Quantum واحد باقی می‌ماند، چون هر درخواست ابتدا باید از Core System عبور کند؛ در واقع این معماری را از مونولیتیک به توزیع‌شده تبدیل می‌کند و پیچیدگی و هزینه‌ی استقرار را افزایش می‌دهد.

Registry و Contracts

Registry اطلاعاتی درباره‌ی هر Plug-in (نام، قرارداد داده، جزئیات پروتکل دسترسی) را نگه می‌دارد و می‌تواند از یک Map ساده تا ابزارهای پیچیده‌ی Discovery مثل Apache ZooKeeper یا Consul باشد. Contracts بین Plug-in و Core System معمولاً برای یک دامنه استاندارد است، مگر زمانی که Plug-in توسط شخص‌ثالث توسعه‌یافته باشد که در این‌صورت یک Adapter لازم است.

نکته‌ی طراحی مهم دیگر: معمولاً Plug-inها مستقیماً به دیتابیس مشترک مرکزی متصل نمی‌شوند؛ این مسئولیت با Core System است تا تغییرات دیتابیس فقط روی Core اثر بگذارد نه روی Plug-inها. با این‌حال، هر Plug-in می‌تواند دیتابیس مخصوص به خودش (Embedded یا External) داشته باشد.

مثال‌های کاربردی واقعی

نویسندگان طیف وسیعی از مثال‌ها ارائه می‌دهند:

  • ابزارهای توسعه مثل Eclipse IDE، PMD، Jira و Jenkins
  • مرورگرهای وب مثل Chrome و Firefox که Viewerها و افزونه‌ها را اضافه می‌کنند
  • سیستم پردازش خسارت بیمه، که در آن قوانین هر Jurisdiction به‌عنوان Plug-in مجزا پیاده‌سازی می‌شود تا از تبدیل‌شدن Rules Engine به یک Big Ball of Mud پیچیده جلوگیری شود
  • نرم‌افزار محاسبه‌ی مالیات، که فرم اصلی ۱۰۴۰ به‌عنوان Core System و هر فرم و worksheet فرعی به‌عنوان Plug-in عمل می‌کند

ارزیابی مشخصه‌های معماری

مشخصهامتیازدلیل
Simplicity و Costبالامشابه Layered، استقرار مونولیتیک
Scalability، Fault Tolerance، Extensibility (به‌عنوان ذاتی)پایینبه‌دلیل استقرار مونولیتیک و quantum واحد
Testability، Deployability، Reliabilityکمی بالاتر از میانگین (۳ ستاره)چون عملکرد در Plug-inهای مستقل ایزوله می‌شود
Modularity و Extensibilityکمی بالاتر از میانگین (۳ ستاره)افزودن/حذف قابلیت از طریق Plug-in مستقل و بدون تأثیر روی بقیه
Performanceکمی بالاتر از میانگین (۳ ستاره)اپلیکیشن‌های کوچک‌تر، عدم درگیری با Sinkhole Anti-Pattern، امکان حذف قابلیت‌های غیرضروری (مثل Wildfly)

نکته‌ی کلیدی جدول امتیازدهی این است که برخلاف Layered و Pipeline، این تنها سبک معماری است که هم می‌تواند Domain Partitioned باشد (مثل مثال بیمه یا مالیات) و هم Technically Partitioned، که آن را برای مسائل با نیاز شدید به شخصی‌سازی و توسعه‌پذیری (مثل IDEها) بسیار مناسب می‌کند.


فصل ۱۳: سبک معماری Service-Based

Service-Based Architecture یکی از عمل‌گراترین سبک‌های معماری است؛ نوعی هیبرید از Microservices که پیچیدگی و هزینه‌ی بسیار کمتری دارد و به‌همین دلیل انتخابی محبوب برای بسیاری از اپلیکیشن‌های کسب‌وکاری محسوب می‌شود.

توپولوژی پایه

ساختار این سبک از سه بخش اصلی تشکیل شده: یک رابط کاربری مستقل و جداگانه‌مستقر، مجموعه‌ای از Domain Servicesِ درشت‌دانه (Coarse-Grained) که به‌صورت remote مستقرند، و یک دیتابیس مونولیتیک مشترک. این سرویس‌ها معمولاً بین ۴ تا ۱۲ عدد هستند (میانگین ۷ سرویس) و برخلاف Microservices، نیازی به Containerization ندارند و می‌توانند مثل هر اپلیکیشن مونولیتیک دیگری (WAR، EAR) مستقر شوند. ارتباط رابط کاربری با سرویس‌ها معمولاً از طریق REST انجام می‌شود، اما Messaging، RPC یا SOAP هم ممکن است.

تنوع توپولوژی: انعطاف‌پذیری بی‌نظیر

یکی از نقاط قوت اصلی این سبک، تنوع توپولوژی آن است:

  • رابط کاربری: می‌تواند یک UI مونولیتیک واحد باشد یا به دامنه‌های جداگانه (حتی هم‌راستا با هر Domain Service) تفکیک شود
  • دیتابیس: می‌تواند یک دیتابیس مشترک باشد یا به دیتابیس‌های جداگانه‌ی دامنه‌محور تقسیم شود (مشابه Microservices)، به‌شرط آن‌که داده‌ی هر دامنه فقط توسط همان سرویس نیاز باشد تا از ارتباط بین‌سرویسی جلوگیری شود
  • API Layer: می‌توان یک لایه‌ی Reverse Proxy یا Gateway بین UI و سرویس‌ها اضافه کرد، مفید برای تجمیع دغدغه‌های Cross-Cutting مثل امنیت و متریک‌ها

طراحی سرویس و مدیریت دیتابیس مشترک

هر Domain Service معمولاً خودش با یک Layered Architecture (لایه‌ی API Facade، Business، Persistence) طراحی می‌شود و تراکنش‌های ACID سنتی را حفظ می‌کند، برخلاف Microservices که معمولاً به Eventual Consistency و Saga متکی است. برای مدیریت دیتابیس مشترک، نویسندگان Federated Shared Libraries را پیشنهاد می‌دهند: به‌جای یک کتابخانه‌ی مشترک واحد برای تمام Entity Objectها (که هر تغییر جدول را به تمام سرویس‌ها سرایت می‌دهد)، دیتابیس به‌صورت منطقی به چند دامنه تقسیم می‌شود و هر دامنه کتابخانه‌ی مخصوص خودش را دارد؛ به‌این‌ترتیب تغییر در جداول Invoicing فقط سرویس Invoicing را تحت‌تأثیر قرار می‌دهد.

مثال عملی: سیستم بازیافت الکترونیک

نویسندگان یک مثال کامل ارائه می‌دهند: فرآیند بازیافت گوشی‌های قدیمی شامل مراحل Quoting، Receiving، Assessment، Accounting، Item Status، Recycling و Reporting است که هر یک به یک Domain Service مستقل تبدیل می‌شود. در این طراحی، تنها سرویس‌های Quoting و Item Status (که مستقیماً با مشتری در تعامل‌اند) نیاز به Scale دارند و بقیه تک‌نمونه باقی می‌مانند؛ همچنین دو دیتابیس جداگانه (داخلی و خارج از فایروال) امنیت داده را تقویت می‌کند. این مثال نشان می‌دهد سیستم می‌تواند بیش از یک Quantum داشته باشد؛ در این‌جا دقیقاً دو Quantum وجود دارد: یکی برای بخش مشتری (Quoting و Item Status) و یکی برای عملیات داخلی.

ارزیابی مشخصه‌های معماری

مشخصهامتیازدلیل
Agility، Testability، Deployabilityبالا (۴ ستاره)دامنه‌های مستقل، ریسک استقرار کمتر از مونولیت
Fault Tolerance و Availabilityبالا (۴ ستاره)سرویس‌ها خودکفا هستند و از ارتباط بین‌سرویسی اجتناب می‌کنند
Scalabilityمتوسط (۳ ستاره)به‌دلیل درشت‌دانگی سرویس‌ها
Elasticityپایین (۲ ستاره)تکرار عملکرد بیشتر نسبت به سرویس‌های ریزدانه
Simplicity و Costبالاساده‌ترین و کم‌هزینه‌ترین معماری توزیع‌شده در مقایسه با Microservices یا Event-Driven
Reliabilityبالاترافیک شبکه‌ی کمتر به‌دلیل سرویس‌های درشت‌دانه

چه زمانی از این سبک استفاده کنیم

این سبک به‌ویژه برای Domain-Driven Design مناسب است، چون هر دامنه به‌طور طبیعی در یک Domain Service مستقل جای می‌گیرد. همچنین نویسندگان تشبیه جذابی دارند: استفاده از یک معماری قدرتمندتر مثل Microservices برای مسئله‌ای که این سطح از قدرت را نیاز ندارد، مثل رانندگی با یک فراری در ترافیک شهری با سرعت ۵۰ کیلومتر بر ساعت است — باکلاس، اما اتلاف منابع. در نهایت، این سبک تراکنش‌های ACID را بهتر از هر معماری توزیع‌شده‌ی دیگری حفظ می‌کند و نیاز کمتری به پیچیدگی Orchestration/Choreography دارد، چون سرویس‌ها درشت‌دانه‌اند.


فصل ۱۴: سبک معماری Event-Driven

Event-Driven Architecture یکی از رایج‌ترین معماری‌های توزیع‌شده و ناهمگام است که در دو توپولوژی اصلی پیاده‌سازی می‌شود: Broker و Mediator، و انتخاب بین این دو یکی از مهم‌ترین Trade-offهای معماری این سبک است.

توپولوژی Broker: مدل رله‌ای غیرمتمرکز

در توپولوژی Broker چهار جزء اصلی وجود دارد: Initiating Event (رخداد آغازین)، Event Broker، Event Processor و Processing Event. هیچ Mediator مرکزی وجود ندارد؛ یک Event Processor رخداد آغازین را می‌گیرد، کار خودش را انجام می‌دهد و سپس به‌صورت ناهمگام یک Processing Event جدید تولید می‌کند تا سایر Event Processorهای علاقه‌مند بتوانند به آن واکنش دهند.

نویسندگان این مدل را به یک مسابقه‌ی دوی امدادی تشبیه می‌کنند: وقتی یک دونده چوب را تحویل می‌دهد، دیگر نقشی در مسابقه ندارد و آزاد است برای کارهای دیگر؛ به‌همین ترتیب هر Event Processor پس از انتشار رویداد خودش، مستقل از باقی سیستم عمل می‌کند. نکته‌ی کلیدی طراحی این است که هر Event Processor باید کاری که انجام داده را اعلام کند، حتی اگر فعلاً هیچ‌کس به آن گوش نمی‌دهد؛ این «قلاب‌های از پیش‌آماده» Extensibility فوق‌العاده‌ای فراهم می‌کند — مثلاً افزودن یک Event Processor جدید برای تحلیل ایمیل‌های ارسالی بدون هیچ تغییری در زیرساخت موجود ممکن است.

مثال کامل سفارش خرده‌فروشی نشان می‌دهد چطور یک رخداد PlaceOrder به‌صورت موازی توسط Notification، Payment و Inventory پردازش می‌شود، و چگونه هر پردازش بعدی (مثل OrderFulfillment یا Shipping) به رویدادهای قبلی واکنش می‌دهد.

نویسندگان چهار مزیت اصلی و پنج عیب اصلی این توپولوژی را برمی‌شمارند:

مزایامعایب
کوپلینگ بسیار پایین بین Event Processorها 
Scalability بالا 
Responsiveness و Performance بالا 
Fault Tolerance بالا 

توپولوژی Mediator: کنترل متمرکز گردش کار

توپولوژی Mediator این ضعف‌ها را با اضافه‌کردن یک Event Mediator مرکزی حل می‌کند که مراحل پردازش هر رخداد را می‌داند و Processing Eventها را به‌صورت Point-to-Point (نه Broadcast) به کانال‌های اختصاصی ارسال می‌کند. برخلاف Broker، در اینجا رویدادهای پردازشی مثل place-order یا send-email در واقع Command هستند (چیزی که باید اتفاق بیفتد)، نه Event (چیزی که اتفاق افتاده)؛ و یک Command باید پردازش شود، در حالی‌که یک Event در توپولوژی Broker می‌تواند نادیده گرفته شود.

نویسندگان توصیه می‌کنند رویدادها را بر اساس پیچیدگی به Simple، Hard و Complex دسته‌بندی کرد: رویدادهای ساده توسط یک Mediator ساده مثل Apache Camel یا Mule پردازش شوند؛ رویدادهای پیچیده و پویا با BPEL Engineهایی مثل Apache ODE؛ و رویدادهایی با دخالت انسانی طولانی‌مدت (مثل تأیید یک معامله‌ی سهام با ارزش بالا) نیازمند یک BPM Engine مثل jBPM هستند. یک الگوی رایج، Mediator Delegation است: یک Mediator ساده ابتدا رخداد را دریافت و طبقه‌بندی می‌کند و سپس آن را به Mediator تخصصی‌تر ارجاع می‌دهد.

مزایامعایب
کنترل کامل گردش کار 
مدیریت خطای بهتر و امکان Recoverability/Restart 
Data Consistency بهتر 
توانایی مدل‌سازی گردش‌کارهای پیچیده 

نویسندگان تأکید می‌کنند انتخاب بین این دو توپولوژی اساساً یک Trade-off بین کنترل گردش‌کار/مدیریت خطا در برابر Performance و Scalability بالاتر است.

قابلیت‌های ناهمگام و پارادوکس Responsiveness

یک مثال قدرتمند این را نشان می‌دهد: ثبت یک کامنت که ۳۰۰۰ میلی‌ثانیه پردازش (بررسی کلمات نامناسب، گرامر و context) طول می‌کشد. با فراخوانی همزمان REST، کاربر ۳۱۰۰ میلی‌ثانیه منتظر می‌ماند؛ اما با پیام‌رسانی ناهمگام، کاربر تنها ۲۵ میلی‌ثانیه احساس تأخیر می‌کند (هرچند پردازش واقعی هنوز ۳۰۲۵ میلی‌ثانیه طول می‌کشد). نویسندگان تمایز مهمی قائل می‌شوند: این تفاوت به Responsiveness مربوط است نه Performance واقعی؛ چیزی که تغییر کرده احساس سرعت کاربر است، نه سرعت واقعی پردازش.

اما این سرعت ظاهری هزینه‌ای دارد: مدیریت خطا. اگر کامنت به‌دلیل کلمه‌ی نامناسب رد شود، چگونه باید به کاربر که قبلاً پیام «موفق» دریافت کرده، خبر داد؟ نویسندگان راه‌حل این مسئله را الگوی Workflow Event Pattern معرفی می‌کنند که با تکنیک‌های Delegation، Containment و Repair از طریق یک Workflow Delegate، امکان Resiliency بدون از‌دست‌دادن Responsiveness را فراهم می‌کند.


فصل ۱۴ (ادامه): مدیریت خطا و جلوگیری از از‌دست‌رفتن داده

پس از بررسی دو توپولوژی، نویسندگان به یکی از دشوارترین چالش‌های معماری Event-Driven می‌پردازند: چگونه در یک جریان کاملاً ناهمگام، خطاهایی را مدیریت کنیم که هیچ کاربری برای پاسخ‌دادن همزمان به آن‌ها وجود ندارد؟

الگوی Workflow Event Pattern

راه‌حل نویسندگان Workflow Event Pattern است: وقتی یک Event Processor با خطا مواجه می‌شود، به‌جای متوقف‌کردن پردازش برای رفع خطا (که Responsiveness کل سیستم را مختل می‌کند)، فوراً خطا را به یک Workflow Delegate تفویض می‌کند و بلافاصله سراغ پیام بعدی در صف می‌رود. این تفویض از طریق سه تکنیک انجام می‌شود: Delegation (ارسال خطا به سرویس تخصصی مثل Trade Placement Error)، Containment (نگه‌داشتن پیام معیوب بدون مسدود کردن بقیه‌ی صف) و Repair (اصلاح برنامه‌ریزی‌شده یا ماشینی داده و ارسال دوباره‌ی آن به صف اصلی).

نویسندگان یک مثال کامل از سیستم معاملات سهام ارائه می‌دهند: زمانی‌که یک دستور خرید سهام Apple به‌دلیل فرمت نادرست (کلمه‌ی اضافی SHARES) با خطای NumberFormatException مواجه می‌شود، سرویس Trade Placement بلافاصله خطا را به سرویس Trade Placement Error می‌فرستد و بدون توقف، دستورات بعدی را پردازش می‌کند؛ سرویس خطا کلمه‌ی اضافی را حذف کرده و پیام اصلاح‌شده را دوباره به صف اصلی ارسال می‌کند. نویسندگان یک عارضه‌ی جانبی مهم این الگو را هشدار می‌دهند: پیام‌های اصلاح‌شده خارج از ترتیب اصلی پردازش می‌شوند، که در سناریوهایی مثل معاملات سهام (که ترتیب BUY/SELL در یک حساب اهمیت دارد) باید با صف‌های موقت مبتنی بر شماره‌حساب مدیریت شود.

جلوگیری از از‌دست‌رفتن داده

نویسندگان سه نقطه‌ی بحرانی برای از‌دست‌رفتن داده در یک جریان پیام‌رسانی ناهمگام شناسایی می‌کنند:

  • پیام هرگز به صف نمی‌رسد، یا Broker پیش از دریافت توسط Consumer بعدی از کار می‌افتد
  • Event Processor پیام را از صف خارج می‌کند اما پیش از پردازش کامل، Crash می‌کند
  • Event Processor به‌دلیل خطای داده نمی‌تواند پیام را در دیتابیس ذخیره کند

برای هر مورد یک راه‌حل مشخص وجود دارد: مسئله‌ی اول با Persistent Message Queue به‌همراه Synchronous Send حل می‌شود (پیام هم در حافظه و هم روی دیسک ذخیره می‌شود و Producer تا تأیید Persist‌شدن پیام منتظر می‌ماند)؛ مسئله‌ی دوم با Client Acknowledge Mode (پیام تا تأیید کامل پردازش در صف باقی می‌ماند، برخلاف حالت پیش‌فرض Auto Acknowledge)؛ و مسئله‌ی سوم با تراکنش‌های ACID به‌همراه Last Participant Support (LPS) که تنها پس از Commit موفق دیتابیس، پیام را از صف حذف می‌کند.

قابلیت Broadcast

یکی از ویژگی‌های منحصربه‌فرد این معماری، توانایی Broadcast رویدادها بدون آگاهی از اینکه کدام Consumerها (اگر باشند) پیام را دریافت می‌کنند و با آن چه می‌کنند، است؛ این بالاترین سطح Decoupling بین Event Processorها را ایجاد می‌کند. نویسندگان مثال قیمت لحظه‌ای سهام را می‌زنند: سرویسی که قیمت را منتشر می‌کند هیچ اطلاعی از نحوه‌ی استفاده‌ی آن اطلاعات ندارد، و این الگو پایه‌ی تکنیک‌هایی مثل Eventual Consistency و Complex Event Processing (CEP) است.

ارزیابی نهایی مشخصه‌ها

نویسندگان امتیاز ۵ ستاره را به Performance، Scalability، Fault Tolerance و Evolutionary Architecture می‌دهند، چون پردازش ناهمگام و موازی این سبک آن‌ها را ذاتاً پشتیبانی می‌کند؛ در مقابل Simplicity و Testability امتیاز پایینی می‌گیرند، زیرا جریان‌های رویدادی غیرقطعی (Nondeterministic) می‌توانند صدها یا هزاران سناریوی تست ایجاد کنند که Governance را دشوار می‌سازد.


فصل ۱۴ (پایان): Request-Reply و ارزیابی نهایی

بخش پایانی این فصل به ارتباطات همزمان درون یک معماری ناهمگام، انتخاب استراتژیک بین دو مدل، و جمع‌بندی امتیازات معماری می‌پردازد.

الگوی Request-Reply: همزمانی درون ناهمزمانی

نویسندگان ابتدا یک پرسش کلیدی مطرح می‌کنند: در بسیاری موارد (مثل نیاز به شماره‌ی سفارش هنگام ثبت کتاب یا کد رزرو هنگام بلیت پرواز)، Event Consumer باید بلافاصله پاسخی به Producer بدهد؛ این نوعی ارتباط Pseudosynchronous است که از طریق دو صف پیاده‌سازی می‌شود: یک Request Queue و یک Reply Queue. در این مدل، Producer پیام را به‌صورت ناهمگام به Request Queue می‌فرستد، سپس در حالت Blocking Wait روی Reply Queue منتظر پاسخ می‌ماند.

دو تکنیک اصلی برای پیاده‌سازی این الگو معرفی می‌شود:

  • Correlation ID: یک شناسه در Header پیام پاسخ که برابر با Message ID درخواست اصلی تنظیم می‌شود؛ Producer با یک Message Selector فقط پیامی با CID منطبق را از میان چندین پیام موجود در صف پاسخ دریافت می‌کند
  • Temporary Queue: یک صف موقت که مخصوص همان درخواست ایجاد و پس از پاسخ حذف می‌شود؛ ساده‌تر است اما ایجاد و حذف مکرر صف در حجم بالا Performance و Responsiveness را کاهش می‌دهد، به‌همین دلیل نویسندگان معمولاً تکنیک Correlation ID را توصیه می‌کنند

انتخاب بین مدل Request-Based و Event-Based

نویسندگان یک قاعده‌ی راهنمای ساده ارائه می‌دهند: مدل Request-Based برای درخواست‌های ساختاریافته و داده‌محور (مثل دریافت پروفایل مشتری) که نیاز به قطعیت و کنترل روی Workflow دارند مناسب است؛ مدل Event-Based برای رویدادهای منعطف و اکشن‌محور که به Responsiveness و Scale بالا در پردازش‌های پویا و پیچیده نیاز دارند برتری دارد.

مزایای Event-Based نسبت به Request-BasedTrade-offها
واکنش بهتر به محتوای دینامیک کاربرفقط از Eventual Consistency پشتیبانی می‌کند
Scalability و Elasticity بهترکنترل کمتر روی جریان پردازش
Agility و مدیریت تغییر بهترقطعیت کمتر روی نتیجه‌ی جریان رویداد
Adaptability و Extensibility بهتردشواری در Test و Debug
Responsiveness و Performance بهتر—
تصمیم‌گیری بلادرنگ بهتر—

معماری‌های هیبرید Event-Driven

نویسندگان تأکید می‌کنند که Event-Driven Architecture در عمل بیشتر به‌عنوان جزئی از سبک‌های دیگر (مثل Microservices یا Space-Based) استفاده می‌شود تا به‌تنهایی؛ افزودن آن به هر سبک دیگر به رفع Bottleneck، ایجاد نقطه‌ی Back Pressure، و بالابردن Responsiveness کمک می‌کند. ترکیب‌های دیگری مثل Event-Driven Microkernel یا Event-Driven Pipeline نیز ممکن است.

ارزیابی نهایی مشخصه‌های معماری

نکته‌ی مفهومی مهم این است که Event-Driven Architecture ذاتاً یک معماری Technically Partitioned است، نه Domain Partitioned؛ چون هر دامنه معمولاً بین چندین Event Processor پخش می‌شود و از طریق Mediator، صف و Topic به‌هم متصل است، به‌همین دلیل تغییر یک دامنه اغلب چندین جزء را تحت‌تأثیر قرار می‌دهد.

نکته‌ی دیگر درباره‌ی Quantum است: اگرچه تمام ارتباطات ناهمگام است، اگر چند Event Processor یک نمونه‌ی دیتابیس مشترک داشته باشند یا از Request-Reply استفاده کنند، همچنان در یک Quantum واحد قرار می‌گیرند، چون Event Processor اول باید منتظر پاسخ Event Processor دوم بماند.

مشخصهامتیازدلیل
Performance، Scalability، Fault Tolerance۵ ستارهپردازش موازی و ناهمگام، Load Balancing برنامه‌ای (Competing Consumers)، Consistency نهایی به‌جای وابستگی سخت
Evolutionary Architecture۵ ستارهافزودن قابلیت جدید بدون تغییر زیرساخت موجود، به‌ویژه در توپولوژی Broker
Simplicity و Testabilityپایینجریان‌های رویدادی غیرقطعی می‌توانند هزاران سناریوی تست ایجاد کنند که Governance را دشوار می‌سازد

فصل ۱۵: سبک معماری Space-Based

Space-Based Architecture سبکی است که مستقیماً برای حل مشکل Elastic Scale طراحی شده؛ مشکلی که نویسندگان با داستان شرکت Pets.com توضیح می‌دهند: شکست این شرکت نه از کمبود مشتری، بلکه از ناتوانی زیرساخت در مواجهه با موفقیت ناگهانی بود.

توپولوژی کلی و اجزای اصلی

این معماری از پنج جزء اصلی تشکیل شده: Processing Unit (حاوی کد اپلیکیشن)، Virtualized Middleware (مدیریت و هماهنگی Processing Unitها)، Data Pumps (ارسال ناهمگام داده‌های به‌روزشده به دیتابیس)، Data Writers (اعمال آپدیت‌ها از Data Pumps) و Data Readers (خواندن داده از دیتابیس و تحویل به Processing Unit هنگام Startup). ایده‌ی محوری این است که دیتابیس به‌عنوان یک محدودیت (Constraint) از مسیر پردازش اصلی حذف می‌شود.

Processing Unit: قلب معماری

Processing Unit شامل منطق اپلیکیشن (وب و Backend) به‌همراه یک In-Memory Data Grid و Replication Engine است که معمولاً با محصولاتی مثل Hazelcast، Apache Ignite یا Oracle Coherence پیاده‌سازی می‌شود. بسته به اندازه‌ی اپلیکیشن، می‌توان یک Processing Unit واحد یا چند Processing Unit مبتنی بر حوزه‌های عملکردی مختلف (مشابه Microservices) داشت.

Virtualized Middleware: چهار جزء کلیدی

Virtualized Middleware دغدغه‌های زیرساختی مربوط به همگام‌سازی داده و مدیریت درخواست را کنترل می‌کند و شامل چهار بخش است:

  • Messaging Grid: مدیریت درخواست‌های ورودی و وضعیت Session؛ با الگوریتم‌هایی از Round-Robin ساده تا Next-Available پیچیده، درخواست را به یک Processing Unit فعال هدایت می‌کند (معمولاً با HA Proxy یا Nginx)
  • Data Grid: مهم‌ترین جزء این معماری؛ تضمین می‌کند تمام Processing Unitها دقیقاً داده‌ی یکسانی در Cache داخلی خود دارند؛ همگام‌سازی به‌صورت ناهمگام و در کمتر از ۱۰۰ میلی‌ثانیه انجام می‌شود
  • Processing Grid: در صورت نیاز به Orchestration بین چند Processing Unit استفاده می‌شود
  • Deployment Manager: مدیریت راه‌اندازی و خاتمه‌ی Processing Unitها بر اساس بار سیستم

نویسندگان یک مثال کد جاوا با Hazelcast ارائه می‌دهند که نشان می‌دهد چگونه یک ReplicatedMap برای پروفایل مشتری در تمام Processing Unitها به‌طور خودکار همگام می‌شود؛ هر تغییری که در یک Processing Unit روی این Cache نام‌گذاری‌شده اعمال شود، به تمام Processing Unitهای دیگر با همان Cache تکثیر می‌گردد.

مثال‌های کاربردی: چرا این معماری؟

دو مثال کلاسیک نویسندگان — سیستم فروش بلیت کنسرت و سیستم حراج آنلاین — هر دو ویژگی مشترکی دارند: افزایش ناگهانی و غیرقابل‌پیش‌بینی حجم کاربران همزمان. در سیستم بلیت کنسرت، حجم کاربران از چند صد به ده‌ها هزار در عرض دقایق افزایش می‌یابد؛ دسترسی همزمان به یک دیتابیس مرکزی برای این حجم درخواست عملاً کار نمی‌کند، در حالی‌که Deployment Manager می‌تواند پیش از شروع فروش بلیت، Processing Unitهای لازم را از پیش آماده کند.

ارزیابی مشخصه‌های معماری

مشخصهامتیازدلیل
Elasticity، Scalability، Performance۵ ستاره (بالاترین)حذف دیتابیس به‌عنوان Constraint، استفاده از In-Memory Caching؛ امکان پردازش میلیون‌ها کاربر همزمان
Simplicity و Testability۱ ستاره (پایین‌ترین)پیچیدگی Caching و Eventual Consistency؛ تست حجم بالای همزمانی بسیار پرهزینه و اغلب فقط در محیط Production ممکن است
Costپایینهزینه‌ی بالای لایسنس محصولات Caching و مصرف منابع بالا

نکته‌ی مفهومی مهم این است که Space-Based Architecture هم می‌تواند Domain Partitioned باشد (چون Processing Unit می‌تواند مثل یک Domain Service عمل کند) و هم Technically Partitioned (چون جدایی بین پردازش تراکنشی در Cache و ذخیره‌ی نهایی در دیتابیس، یک لایه‌بندی فنی مشابه معماری Layered ایجاد می‌کند). همچنین چون Processing Unitها به‌صورت همگام با دیتابیس ارتباط ندارند، دیتابیس در محاسبه‌ی Quantum لحاظ نمی‌شود؛ Quantumها بر اساس ارتباط بین رابط کاربری و Processing Unitها یا ارتباط همزمان بین خود Processing Unitها مشخص می‌شوند.


فصل ۱۵ (ادامه): مسیرهای داده و مدیریت تعارض

بخش پایانی این فصل به نحوه‌ی هماهنگی نهایی داده بین Cache درون‌حافظه‌ای و دیتابیس، و چالش‌های اجرایی این هماهنگی می‌پردازد.

Data Writers: نوشتن داده در دیتابیس

Data Writer پیام‌ها را از یک Data Pump دریافت می‌کند و دیتابیس را با اطلاعات آن به‌روزرسانی می‌کند؛ این جزء می‌تواند به‌صورت سرویس، اپلیکیشن یا Data Hub (مثل Ab Initio) پیاده‌سازی شود. نویسندگان دو مدل طراحی را مقایسه می‌کنند:

  • Domain-Based Data Writer: یک Data Writer واحد که منطق دیتابیسی کامل یک دامنه (مثل Customer) را در خود دارد و به چند Data Pump مختلف (مثل Profile، WishList، Wallet، Preferences) گوش می‌دهد
  • Dedicated Data Writer: هر Processing Unit یک Data Writer اختصاصی خودش دارد که فقط منطق دیتابیسی همان بخش را می‌داند؛ این مدل تعداد اجزا را افزایش می‌دهد اما Scalability و Agility بهتری فراهم می‌کند به‌دلیل هم‌راستایی کامل Processing Unit، Data Pump و Data Writer

Data Readers: بازیابی داده هنگام نیاز

Data Reader برخلاف Data Writer، مسئول خوانمن داده از دیتابیس و ارسال آن به Processing Unit از طریق یک Reverse Data Pump است، اما تنها در سه سناریو فعال می‌شود: Crash تمام نمونه‌های یک Cache هم‌نام، Redeploy کامل Processing Unitها، یا بازیابی داده‌ی آرشیوی که در Cache موجود نیست. فرآیند بازیابی جالب است: هنگام راه‌اندازی مجدد، هر نمونه سعی می‌کند یک Lock روی Cache بگیرد؛ اولین نمونه‌ای که موفق شود، Temporary Cache Owner می‌شود، داده را از طریق Data Reader بارگذاری می‌کند، و پس از تکمیل، Lock را آزاد می‌کند تا سایر نمونه‌ها همگام‌سازی شوند.

نویسندگان یک تمایز مفهومی مهم مطرح می‌کنند: مجموعه‌ی Data Writer و Data Reader یک Data Abstraction Layer تشکیل می‌دهند (نه صرفاً Data Access Layer)؛ این به این معناست که Processing Unit از ساختار جدول‌های دیتابیس مستقل است و تغییرات تدریجی در Schema دیتابیس بدون تأثیر مستقیم روی Cache قابل انجام است.

تعارض داده: مشکل اجتناب‌ناپذیر Replicated Caching

وقتی چند نمونه‌ی Cache به‌صورت Active/Active قابل به‌روزرسانی باشند، Data Collision رخ می‌دهد: اگر دو Service به‌طور همزمان روی یک داده تغییر ایجاد کنند، پیش از تکمیل Replication، هرکدام داده‌ی نادرست دیگری را جای‌گزین می‌کنند. نویسندگان مثال کلاسیک انبار کالا را می‌زنند: موجودی ۵۰۰ عدد ویجت آبی، وقتی سرویس A آن را به ۴۹۰ و همزمان سرویس B به ۴۹۵ کاهش می‌دهد، در نهایت هر دو Cache به مقدار نادرست (نه ۴۸۵ واقعی) می‌رسند.

نویسندگان یک فرمول احتمالاتی برای نرخ تعارض ارائه می‌دهند که به چهار عامل بستگی دارد: نرخ به‌روزرسانی (Update Rate)، تعداد نمونه‌ها (Number of Instances)، اندازه‌ی Cache و تأخیر Replication. دو نمونه‌ی عددی نشان می‌دهد که کاهش تعداد نمونه از ۵ به ۲ نرخ تعارض را از حدود ۷۲ در ساعت به ۵.۸ در ساعت کاهش می‌دهد، در حالی‌که کاهش اندازه‌ی Cache از ۵۰,۰۰۰ به ۱۰,۰۰۰ ردیف، نرخ را به‌شدت افزایش می‌دهد (رابطه‌ی معکوس اندازه‌ی Cache با نرخ تعارض).

پیاده‌سازی هیبرید Cloud و On-Premises

یکی از قابلیت‌های منحصربه‌فرد این سبک، امکان استقرار Processing Unitها و Virtualized Middleware در محیط Cloud در حالی‌که دیتابیس فیزیکی On-Premises باقی می‌ماند؛ این مدل به‌لطف ماهیت ناهمگام Data Pumpها و مدل Eventual Consistency، پردازش تراکنشی را در محیط پویا و Elastic ابری انجام می‌دهد و در عین‌حال مدیریت داده، گزارش‌گیری و تحلیل داده را در محیط امن و محلی نگه می‌دارد.

Replicated در برابر Distributed Caching

Replicated Caching (مدل استاندارد این سبک) هر Processing Unit را با یک In-Memory Data Grid کامل مجهز می‌کند که با سایر نمونه‌ها همگام است؛ این مدل سریع است و به‌دلیل نداشتن سرور مرکزی، Single Point of Failure ندارد. اما در حجم داده‌ی بالا (بیش از ۱۰۰ مگابایت در هر نمونه) یا نرخ به‌روزرسانی بسیار بالا، Distributed Caching با یک سرور Cache مرکزی جای‌گزین می‌شود. نویسندگان همچنین مدل Near-Cache (ترکیب یک Cache محلی کوچک با یک Backing Cache کامل) را به‌طور صریح برای این سبک توصیه نمی‌کنند، چون Cacheهای محلی بین Processing Unitها همگام نمی‌شوند و ناهماهنگی در Performance و Responsiveness ایجاد می‌کنند.


فصل ۱۶: معماری Orchestration-Driven Service-Oriented

نویسندگان این سبک را با یک لحن نسبتاً انتقادی معرفی می‌کنند: برخلاف سبک‌های دیگر که هنوز کاربرد عملی دارند، این معماری نمونه‌ای است از این‌که چگونه یک فلسفه‌ی سازمانی منطقی می‌تواند در عمل به فاجعه ختم شود.

زمینه‌ی تاریخی و فلسفه‌ی Reuse

این سبک در اواخر دهه‌ی ۱۹۹۰ ظهور کرد، دورانی که منابع محاسباتی گران و محدود بودند و سیستم‌عامل‌ها و پایگاه‌داده‌های تجاری با مدل‌های لایسنسینگ پیچیده به ازای هر ماشین قیمت‌گذاری می‌شدند. این محدودیت‌های بیرونی، معماران را به‌سوی فلسفه‌ی Reuse در تمام سطوح سوق داد؛ ایده‌ای که در ظاهر منطقی بود اما در عمل عوارض جانبی مخربی روی Coupling سیستم ایجاد کرد.

تاکسونومی چهارلایه‌ی سرویس‌ها

معماری حول یک تاکسونومی مشخص از سرویس‌ها شکل می‌گیرد که هر لایه مسئولیت متفاوتی دارد:

  • Business Services: نقطه‌ی ورود معماری؛ سرویس‌های درشت‌دانه مانند ExecuteTrade یا PlaceOrder که رفتار دامنه را نشان می‌دهند و معمولاً بدون کد، فقط با تعریف ورودی/خروجی توسط کاربران کسب‌وکار تعیین می‌شوند
  • Enterprise Services: پیاده‌سازی‌های ریزدانه و مشترک مثل CreateCustomer یا CalculateQuote؛ این‌ها اجزای سازنده‌ای هستند که از طریق Orchestration Engine به Business Services متصل می‌شوند تا هدف اصلی معماری (بازاستفاده) محقق شود
  • Application Services: سرویس‌های یک‌بارمصرف و تک‌پیاده‌سازی که تحت مالکیت یک تیم اپلیکیشن مشخص قرار دارند، مثل یک سرویس Geo-location که نیازی به تعمیم‌پذیری ندارد
  • Infrastructure Services: دغدغه‌های عملیاتی مثل Monitoring، Logging، Authentication و Authorization که توسط تیم زیرساخت مشترک نگهداری می‌شوند

نویسندگان یک نکته‌ی مهم درباره‌ی Enterprise Services مطرح می‌کنند: فرض بنیادین این معماری این بود که اجزای کسب‌وکار مانند مصالح ساختمانی، برای دهه‌ها ثابت باقی می‌مانند؛ اما واقعیت پویای بازار، فناوری و رویه‌های مهندسی این فرض را به‌طور مداوم نقض کرد.

Orchestration Engine: قلب معماری و منبع بحران

Orchestration Engine قلب این معماری توزیع‌شده است که پیاده‌سازی‌های Business Services را از طریق Orchestration به‌هم متصل می‌کند، شامل هماهنگی تراکنشی و تبدیل پیام. این معماری معمولاً به یک یا چند دیتابیس رابطه‌ای مشترک متصل است (نه یک دیتابیس مجزا به‌ازای هر سرویس مثل Microservices)، و رفتار تراکنشی به‌صورت Declarative در خود Orchestration Engine مدیریت می‌شود، نه در دیتابیس.

نویسندگان با استناد به قانون کانوی توضیح می‌دهند که تیم معماران Integration مسئول این Engine، به‌ناچار به یک نیروی سیاسی درون سازمان و در نهایت یک Bottleneck بروکراتیک تبدیل می‌شود. مشکل اصلی این بود که یافتن سطح مناسب Granularity برای تراکنش‌ها به‌طور فزاینده دشوار شد؛ اگرچه Wrap‌کردن چند سرویس در یک تراکنش توزیع‌شده در ابتدا ممکن به‌نظر می‌رسید، اما با رشد معماری، تعیین مرز صحیح تراکنش بین سرویس‌ها به یک چالش پیچیده تبدیل شد.


فصل ۱۶ (ادامه): مسیر پیام و بحران Coupling

بخش پایانی این فصل نشان می‌دهد چگونه فلسفه‌ی Reuse، در عمل به یک Coupling غیرقابل‌کنترل بین سرویس‌ها منجر شد.

Message Flow: مسیر تمام درخواست‌ها

در این معماری، تمام درخواست‌ها باید از مسیر Orchestration Engine عبور کنند تا به پیاده‌سازی صحیح Enterprise Service برسند؛ این بدان معناست که حتی ساده‌ترین درخواست کسب‌وکار نیز به‌طور اجباری از یک نقطه‌ی مرکزی می‌گذرد. این طراحی که در ابتدا برای تضمین Reuse و هماهنگی تراکنشی طراحی شده بود، عملاً یک نقطه‌ی تمرکز اجباری (Mandatory Centralization) ایجاد کرد که هر تغییر در منطق کسب‌وکار را به یک عملیات پرهزینه و کند تبدیل می‌کرد.

Reuse…and Coupling: تناقض بنیادین

عنوان این بخش به‌عمد با علامت سه‌نقطه نوشته شده تا نشان دهد Reuse به‌طور طبیعی Coupling به‌همراه می‌آورد؛ این همان درسی است که معماران بعدها آن را در قالب یک اصل بنیادین صورت‌بندی کردند:

هرچه سطح Reuse در یک سیستم بالاتر برود، سطح Coupling نیز به همان نسبت افزایش می‌یابد؛ زیرا هر مصرف‌کننده‌ی جدید از یک Enterprise Service مشترک، به‌طور ضمنی به تمام تغییرات آینده‌ی آن سرویس گره می‌خورد. این تناقض توضیح می‌دهد که چرا هدف اصلی این معماری (بازاستفاده‌ی حداکثری) در نهایت خودِ عامل شکست آن شد: تیم‌های مختلف که به یک Enterprise Service مشترک مثل CalculateQuote متکی بودند، هر تغییری در آن سرویس را به یک ریسک سازمانی بزرگ تبدیل می‌کردند، چون احتمال شکستن قراردادهای چندین Business Service دیگر وجود داشت.

نویسندگان این وضعیت را با اصطلاح Coupling ناخواسته اما اجتناب‌ناپذیر توصیف می‌کنند؛ برخلاف Microservices که هدف صریح آن Bounded Context و ایزوله‌سازی داده و منطق است، این سبک از ابتدا برای حداکثر اشتراک‌گذاری طراحی شده بود، و همین انتخاب طراحی، چابکی (Agility) را قربانی هماهنگی متمرکز کرد.

درس تاریخی این معماری

نکته‌ی کلیدی که نویسندگان در این فصل به آن تأکید می‌کنند این است که Orchestration-Driven SOA نمونه‌ی بارز یک معماری است که از یک فرض غلط (ثبات دائمی اجزای کسب‌وکار) آغاز شد و پیچیدگی سازمانی و فنی آن به‌مرور از فوایدش پیشی گرفت. این تحلیل زمینه‌ساز مقایسه‌ی مستقیم با Microservices در فصل بعدی است، جایی که رویکردی کاملاً معکوس (استقلال کامل سرویس‌ها به‌جای Reuse متمرکز) اتخاذ می‌شود.


بر اساس متن کتاب، بخش بعدی که باید بررسی کنیم، آغاز فصل ۱۷ با عنوان معماری میکروسرویس (Microservices Architecture) است. این فصل با دو مفهوم بنیادی شروع می‌شود: تاریخچه (History) و Bounded Context. این دو مفهوم را با دقت تشریح می‌کنم.

تاریخچه پیدایش میکروسرویس

نویسندگان تأکید می‌کنند که هیچ سبک معماری را نباید جدا از بستر زمانی‌اش فهمید. میکروسرویس دقیقاً محصول یک تغییر بنیادین در رابطه‌ی معماری و عملیات (Operations) است.

در سبک‌های قدیمی‌تر مانند SOA مبتنی بر ESB، معماران مجبور بودند دغدغه‌های عملیاتی مثل مقیاس‌پذیری الاستیک (elastic scale) را درون خودِ معماری طراحی کنند، چون فرض بر این بود که کنترلی روی عملیات ندارند (معمولاً به دلیل برون‌سپاری عملیات به شرکت‌های ثالث برای صرفه‌جویی هزینه). نتیجه‌ی این طراحی دفاعی، پیچیدگی شدید معماری بود.

نکته‌ی کلیدی اینجاست: طراحان سبک میکروسرویس متوجه شدند که این دغدغه‌های عملیاتی را بهتر است خودِ تیم عملیات مدیریت کند، نه معماری. با ایجاد یک رابط (liaison) بین معماری و DevOps، معماران توانستند طراحی را ساده‌تر کنند و مسائل مقیاس‌پذیری، کارایی و الاستیسیتی را به DevOps بسپارند. به بیان دیگر، میکروسرویس محصول تشخیص یک «سوءتخصیص منابع» (misappropriation of resources) بود که پیچیدگی تصادفی (accidental complexity) ایجاد می‌کرد.

Bounded Context: قلب فلسفه‌ی میکروسرویس

این مفهوم مستقیماً از کتاب Domain-Driven Design اریک ایوانز وام گرفته شده و تأثیر عمیقی بر تفکر معماری مدرن گذاشته است.

پیش از DDD، رویکرد غالب توسعه‌دهندگان استفاده‌ی مجدد کل‌گرا (holistic reuse) در سطح سازمان بود؛ یعنی ساختن یک کلاس واحد و مشترک مثلاً Customer برای استفاده در تمام سیستم‌ها. اما این رویکرد مشترک‌سازی، مشکلات جدی ایجاد می‌کند:

  • افزایش Coupling بین بخش‌های مختلف سازمان
  • دشواری در هماهنگی تغییرات
  • افزایش پیچیدگی کلی سیستم

مفهوم Bounded Context این واقعیت را می‌پذیرد که هر موجودیت (Entity) بهترین عملکرد را در یک بستر محلی‌شده (localized context) دارد. یعنی به‌جای یک کلاس Customer سراسری، هر دامنه‌ی مسئله (problem domain) نسخه‌ی خودش را می‌سازد و تفاوت‌ها فقط در نقاط یکپارچه‌سازی (integration points) حل‌وفصل می‌شوند.

از منظر شی‌گرایی، این یعنی هر Bounded Context، همه‌چیز مربوط به دامنه‌اش را درونی و قابل‌مشاهده نگه می‌دارد، اما نسبت به سایر Bounded Contextها مات (opaque) است — دقیقاً همان اصل Encapsulation اما در مقیاس معماری، نه صرفاً کلاس.

این مفهوم پایه‌ای است برای فصل بعدی که وارد بحث Granularity و Data Isolation در میکروسرویس می‌شویم، جایی که هر سرویس معمولاً پایگاه‌داده‌ی مخصوص خودش را دارد؛ دقیقاً به این خاطر که هر سرویس، یک Bounded Context مستقل است.


با ادامه‌ی فصل ۱۷، اکنون به بخش‌های توپولوژی (Topology) و Distributed می‌رسیم که مشخصه‌ی ساختاری میکروسرویس را تعریف می‌کنند.

توپولوژی و ویژگی Distributed بودن

نکته‌ی محوری که پیش‌تر در فصل ۷ کتاب (در بحث Architectural Quanta) نیز اشاره شد و اینجا به‌طور کامل بسط پیدا می‌کند، این است: در معماری میکروسرویس، هر سرویس پایگاه‌داده‌ی مخصوص به خود را دارد، برخلاف معماری‌های سنتی که یک پایگاه‌داده‌ی واحد و مشترک دارند.

این طراحی مستقیماً از فلسفه‌ی Bounded Context که در بخش قبل توضیح دادیم نتیجه می‌شود. نویسندگان تعریف می‌کنند که هر Quantum معماری باید مستقل از سایر بخش‌ها قابل استقرار (Independently deployable) باشد؛ یعنی اگر سیستم از یک دیتابیس استفاده می‌کند، آن دیتابیس جزء لاینفک همان Quantum است، چون سیستم بدون آن کار نمی‌کند.

نتیجه‌ی مهم این اصل:

  • در سیستم‌های Legacy با یک دیتابیس مشترک، کل سیستم یک Quantum واحد محسوب می‌شود (Quantum of one)
  • در میکروسرویس، چون هر سرویس دیتابیس اختصاصی خودش را دارد، سیستم از چندین Quantum مستقل تشکیل می‌شود

از منظر معماری توزیع‌شده (Distributed)، این جداسازی داده باعث می‌شود هر سرویس بتواند به‌صورت کاملاً مستقل از نظر عملیاتی (Operational) تغییر کند، مقیاس بگیرد، و دیپلوی شود — بدون اینکه به دیتابیس مشترک یا سرویس‌های دیگر وابسته باشد. این دقیقاً نقطه‌ی تمایز اصلی میکروسرویس با Service-Based Architecture است که در فصل ۱۳ بررسی کردیم؛ در آنجا سرویس‌ها معمولاً یک دیتابیس مونولیتیک مشترک را استفاده می‌کردند، اما اینجا هر سرویس، دیتابیس اختصاصی و منطق کسب‌وکار مستقل خودش را دارد.

نکته‌ی فنی مهم برای شما به‌عنوان Senior Developer: این استقلال کامل به معنای هزینه‌ی بالاتری در پیچیدگی تراکنش‌ها (Transactions) است — چون تراکنش‌های ACID سنتی که در یک دیتابیس واحد کار می‌کردند، اینجا دیگر قابل استفاده نیستند و باید به سمت الگوهایی مثل Saga حرکت کرد که در ادامه‌ی همین فصل (بخش Transactions and Sagas) به آن می‌رسیم.


بخش: گرانولاریتی (Granularity) در معماری میکروسرویس

در ادامه‌ی فصل ۱۷ کتاب «Fundamentals of Software Architecture»، نویسندگان به یکی از سخت‌ترین و پرچالش‌ترین تصمیمات در طراحی میکروسرویس می‌پردازند: تعیین اندازه‌ی صحیح هر سرویس (Service Granularity).

چرا گرانولاریتی دشوار است

بر خلاف Service-Based Architecture که در آن سرویس‌ها به‌صورت Coarse-grained (دانه‌بزرگ) طراحی می‌شوند و معمولاً بین ۴ تا ۱۲ سرویس در هر Application Context قرار می‌گیرند، در میکروسرویس تمایل غالب به سمت طراحی Fine-grained (دانه‌ریز) است. این تفاوت مستقیماً روی سه محور اصلی اثر می‌گذارد:

  • Orchestration داخلی در برابر خارجی: در Service-Based Architecture، یک درخواست تجاری (مثلاً ثبت سفارش) به‌صورت Orchestration کلاسی و درون‌سرویسی مدیریت می‌شود؛ اما در میکروسرویس همان درخواست نیازمند هماهنگی چندین سرویس مجزا و ریموت است.
  • نوع تراکنش: سرویس‌های Coarse-grained به دلیل به اشتراک‌گذاری دیتابیس، از تراکنش‌های ACID سنتی (Atomicity، Consistency، Isolation، Durability) بهره می‌برند، در حالی‌که سرویس‌های Fine-grained میکروسرویس به سمت تراکنش‌های BASE (Basic Availability، Soft State، Eventual Consistency) و Sagas سوق پیدا می‌کنند.
  • ریسک تغییر: در معماری میکروسرویس، چون هر سرویس مسئولیت واحدی دارد، تغییر در یک سرویس ریسک کمتری برای شکستن سایر بخش‌ها دارد؛ اما همین ریزدانگی هزینه‌ی هماهنگی (Coordination) و پیچیدگی توزیع‌شده را افزایش می‌دهد.

پیوند گرانولاریتی با مفهوم Architectural Quantum

نکته‌ی کلیدی که کتاب پیش‌تر در فصل «Scope of Architecture Characteristics» مطرح کرده و در اینجا اهمیت پیدا می‌کند، مفهوم Architecture Quantum است: واحدی که به‌صورت مستقل قابل استقرار (Independently Deployable) است، دارای انسجام کارکردی بالا (High Functional Cohesion) است، و Connascence همزمان (Synchronous Connascence) دارد. در میکروسرویس، هر سرویس معمولاً دیتابیس اختصاصی خودش را دارد که این موضوع منجر به شکل‌گیری چندین Quantum مستقل در سطح کل سیستم می‌شود؛ در حالی‌که در معماری‌های Coarse-grained با دیتابیس مشترک، کل سیستم یک Quantum واحد محسوب می‌شود.

از منظر مهندسی، این یعنی هرچه سرویس ریزدانه‌تر شود، Architectural Characteristics (مانند Scalability یا Reliability) باید در سطح هر Quantum جداگانه تعریف و سنجیده شوند، نه در سطح کل سیستم؛ همین اصل، ریشه‌ی توصیه‌ی Bounded Context در Domain-Driven Design است که پیش‌تر در فصل ۷ کتاب معرفی شده بود.


بخش: ایزوله‌سازی داده (Data Isolation) در میکروسرویس

پس از تعیین گرانولاریتی سرویس‌ها، دومین رکن اساسی معماری میکروسرویس این است که هر سرویس باید داده‌ی خودش را به‌طور کامل در اختیار داشته باشد و هیچ سرویس دیگری اجازه‌ی دسترسی مستقیم به آن دیتابیس را نداشته باشد. این اصل دقیقاً نقطه‌ی مقابل چیزی است که در Service-Based Architecture دیدیم، جایی که چندین سرویس Coarse-grained (معمولاً ۴ تا ۱۲ سرویس) یک دیتابیس مشترک را از طریق کتابخانه‌های اشتراکی (Shared Entity Libraries) به اشتراک می‌گذاشتند.

چرا ایزوله‌سازی داده ضروری است

نویسندگان کتاب پیش‌تر در فصل مقدمه، مثال بسیار گویایی از یک سیستم CRM مطرح می‌کنند: وقتی یک آرکیتکت کنترل دسترسی به دیتابیس، امنیت داده‌های مشتری، و تغییرات Schema را از دست می‌دهد (چون سیستم‌های زیاد دیگری به همان دیتابیس متصل‌اند)، راه‌حل ایجاد Application Silos است؛ یعنی هر دیتابیس فقط از طریق اپلیکیشنی که مالک آن است در دسترس باشد. این همان فلسفه‌ی بنیادین Data Isolation در میکروسرویس است، با این تفاوت که در میکروسرویس این اصل از ابتدا و به‌صورت پیش‌فرض اعمال می‌شود، نه به‌عنوان یک راه‌حل اصلاحی بعدی.

پیامدهای مستقیم این تصمیم

  • از بین رفتن تراکنش‌های ACID سراسری: چون هر سرویس دیتابیس مجزا دارد، دیگر نمی‌توان یک تراکنش اتمیک واحد روی چند سرویس اجرا کرد؛ به همین دلیل میکروسرویس‌ها به سمت مدل BASE (Basic Availability، Soft State، Eventual Consistency) و الگوی Saga حرکت می‌کنند.
  • مثال عملی چک‌اوت سفارش: اگر سرویس OrderPlacement سفارش را ثبت کند و سپس سرویس PaymentService به دلیل انقضای کارت اعتباری با خطا مواجه شود، داده در حالت ناسازگار باقی می‌ماند (سفارش ثبت شده اما تأیید نشده) و باید با مکانیزم‌های جبرانی (Compensating Transactions) مدیریت شود، نه با یک Rollback ساده‌ی پایگاه‌داده.
  • افزایش استقلال در برابر افزایش پیچیدگی هماهنگی: تغییر در Schema یک سرویس دیگر هیچ سرویس دیگری را درگیر نمی‌کند (بر خلاف کتابخانه‌ی مشترک Entity در Service-Based Architecture)، اما هزینه‌اش مدیریت سازگاری داده در سطح توزیع‌شده است.

از منظر مهندسی نرم‌افزار، Data Isolation در واقع تحقق عملی همان مفهوم Bounded Context در Domain-Driven Design است: مرز سرویس دقیقاً همان مرز مالکیت داده است، و عبور از این مرز فقط از طریق API مجاز خواهد بود که موضوع بخش بعدی (API Layer) است.


بخش: لایه API (API Layer)

سومین رکن کلیدی معماری میکروسرویس، وجود یک لایه‌ی API است که به‌عنوان نقطه‌ی ورود واحد (Single Entry Point) بین مصرف‌کننده‌ها (کلاینت‌ها یا سایر سرویس‌ها) و منطق داخلی هر سرویس عمل می‌کند. این لایه معمولاً با استفاده از یک Proxy یا Reverse Proxy سبک در جلوی هر میکروسرویس یا در سطح کل سیستم (به‌شکل API Gateway) پیاده‌سازی می‌شود.

وظایف اصلی لایه API

  • پنهان‌سازی جزئیات داخلی: پیاده‌سازی داخلی سرویس (نوع دیتابیس، زبان برنامه‌نویسی، ساختار داده‌های داخلی) از دید مصرف‌کننده کاملاً پوشیده می‌ماند و فقط قرارداد API (Contract) در معرض دید قرار می‌گیرد.
  • مسیریابی و ترکیب درخواست‌ها (Routing و Aggregation): در سطح کل سیستم، یک API Gateway می‌تواند درخواست‌های ورودی را به سرویس مناسب هدایت کند یا داده از چند سرویس را با هم ترکیب کند تا نیاز کلاینت را با یک فراخوانی واحد برطرف سازد.
  • کنترل دسترسی و امنیت: احراز هویت (Authentication)، مجوزدهی (Authorization)، محدودسازی نرخ درخواست (Rate Limiting) و سایر Cross-Cutting Concerns معمولاً در همین لایه متمرکز می‌شوند تا هر سرویس مجزا مجبور به تکرار این منطق نباشد.

تفاوت با معماری‌های Coarse-grained

در Service-Based Architecture، معمولاً یک لایه‌ی API واحد و متمرکز روی کل مجموعه‌ی سرویس‌های Coarse-grained قرار می‌گیرد؛ اما در میکروسرویس، به دلیل تعداد بالای سرویس‌های ریزدانه، معمولاً از الگوی API Gateway در ترکیب با Service Mesh استفاده می‌شود تا مدیریت ارتباطات، Load Balancing، و Circuit Breaking به‌صورت خودکار و در سطح زیرساخت انجام شود، نه در کد هر سرویس. این رویکرد مستقیماً از فلسفه‌ی Operational Reuse نشأت می‌گیرد که موضوع بخش بعدی کتاب است؛ یعنی به‌جای تکرار منطق مشترک در کد هر سرویس، آن منطق به ابزارهای عملیاتی و زیرساختی سپرده می‌شود.


بخش: استفاده مجدد عملیاتی (Operational Reuse)

پس از سه رکن قبلی (Granularity، Data Isolation، API Layer)، کتاب به یکی از تناقض‌های ظاهری معماری میکروسرویس می‌پردازد: چگونه می‌توان بدون نقض اصل Bounded Context، دغدغه‌های مشترک عملیاتی (مثل Logging، Monitoring، Authentication) را بین صدها سرویس مستقل به اشتراک گذاشت؟

چالش اصلی

در معماری‌های قدیمی‌تر مثل Orchestration-Driven SOA، راه‌حل ایجاد یک لایه‌ی متمرکز به نام Infrastructure Services بود که توسط یک تیم مشترک نگهداری می‌شد و دغدغه‌های عملیاتی مثل Logging، Monitoring، Authentication و Authorization را به‌صورت پیاده‌سازی‌های Concrete در اختیار همه‌ی سرویس‌ها قرار می‌داد. اما این رویکرد در میکروسرویس مشکل‌ساز است، چون هر گونه کد یا کتابخانه‌ی مشترک بین سرویس‌ها، Coupling ایجاد می‌کند و دقیقاً همان استقلالی را که میکروسرویس به دنبال آن است، از بین می‌برد.

راه‌حل: جداسازی نگرانی از طریق زیرساخت

به همین دلیل، در معماری میکروسرویس، Operational Reuse معمولاً نه از طریق کتابخانه‌های کد مشترک (Shared Libraries)، بلکه از طریق ابزارهای زیرساختی و الگوهایی مانند Sidecar Pattern و Service Mesh محقق می‌شود. در این مدل، هر سرویس یک Proxy کمکی (Sidecar) در کنار خود دارد که دغدغه‌های عملیاتی مشترک (Circuit Breaking، Retry، Rate Limiting، Distributed Tracing) را بدون آنکه سرویس اصلی از وجود آن آگاه باشد، مدیریت می‌کند.

  • جداسازی از کد کسب‌وکار: چون منطق عملیاتی در Sidecar پیاده‌سازی می‌شود نه در کد اصلی سرویس، تغییر در ابزارهای عملیاتی نیازی به تغییر یا Redeploy کردن خود سرویس ندارد.
  • استقلال تکنولوژیک حفظ می‌شود: هر سرویس همچنان می‌تواند با هر زبان یا فریمورکی نوشته شود، چون Sidecar در سطح شبکه (نه در سطح کد) عمل می‌کند.
  • مدیریت متمرکز، اجرای غیرمتمرکز: تیم پلتفرم می‌تواند سیاست‌های عملیاتی را به‌صورت متمرکز تعریف کند (مثلاً از طریق Control Plane در Istio)، اما اجرای آن در کنار هر سرویس به‌صورت مجزا انجام می‌شود.

این الگو دقیقاً همان درسی است که از شکست Orchestration-Driven SOA گرفته شد: به‌جای اجبار به Reuse در سطح کد که منجر به Coupling می‌شود، میکروسرویس Reuse را به سطح عملیات و زیرساخت منتقل می‌کند. این رویکرد پایه‌ای است برای فصل بعدی کتاب که به موضوع Frontends و نحوه‌ی تعامل کاربر نهایی با این مجموعه‌ی توزیع‌شده از سرویس‌ها می‌پردازد.


بخش: فرانت‌اند (Frontends) در میکروسرویس

فلسفه‌ی اصلی میکروسرویس، حداکثرسازی Decoupling است؛ در نگاه ایده‌آل، این جداسازی حتی باید تا سطح رابط کاربری (User Interface) هم گسترش یابد و هر Bounded Context، UI خودش را نیز در خود جای دهد. اما نویسندگان تأکید می‌کنند که واقعیت‌های عملی توسعه‌ی وب و محدودیت‌های خارجی، رسیدن به این ایده‌آل را دشوار می‌کند؛ به همین دلیل دو الگوی رایج برای طراحی فرانت‌اند در میکروسرویس شکل گرفته است.

الگوی اول: فرانت‌اند یکپارچه (Monolithic UI)

در این رویکرد، یک اپلیکیشن واحد (دسکتاپ، موبایل یا وب) از طریق لایه‌ی API با تمام میکروسرویس‌های Backend ارتباط برقرار می‌کند. این الگو ساده‌تر است اما در عمل، تیم توسعه‌ی فرانت‌اند مجبور می‌شود دانش و منطق مربوط به دامنه‌های مختلف را در یک کدبیس واحد تجمیع کند، که این خودش نوعی Coupling پنهان بین تیم‌های Backend مستقل ایجاد می‌کند.

الگوی دوم: میکروفرانت‌اند (Microfrontends)

در این الگو، هر میکروسرویس بخش UI مربوط به خودش را نیز تولید می‌کند و فرانت‌اند نهایی از ترکیب این قطعات مجزا شکل می‌گیرد. مزیت اصلی این است که مرز سرویس از Backend تا UI امتداد پیدا می‌کند و کل دامنه (هم منطق و هم رابط کاربری) در اختیار یک تیم واحد قرار می‌گیرد؛ این الگو معمولاً با فریمورک‌های مبتنی بر کامپوننت مانند React یا کتابخانه‌های متن‌باز اختصاصی Microfrontend پیاده‌سازی می‌شود.

ویژگیفرانت‌اند یکپارچهمیکروفرانت‌اند
مالکیت تیمیتیم فرانت‌اند مجزا از تیم‌های Backendهر تیم، هم Backend و هم UI دامنه‌ی خودش را می‌سازد
هم‌راستایی با Bounded Contextضعیفقوی (کاملاً هم‌راستا)
پیچیدگی یکپارچه‌سازیکم (یک کدبیس UI)بیشتر (نیاز به هماهنگی قطعات UI در Runtime)
ریسک Coupling پنهانبالاتر (منطق دامنه‌ها در یک UI جمع می‌شود)پایین‌تر

این تصمیم مستقیماً به بخش بعدی کتاب گره خورده است: نحوه‌ی ارتباط بین این سرویس‌ها و قطعات UI، که موضوع Communication و انتخاب بین ارتباط Synchronous و Asynchronous است.


بخش: تراکنش‌ها و ساگا (Transactions and Sagas)

آخرین و شاید سخت‌ترین چالش معماری میکروسرویس این است: وقتی هر سرویس دیتابیس مستقل خود را دارد، چگونه می‌توان یکپارچگی داده را در یک فرآیند کسب‌وکاری که چند سرویس را درگیر می‌کند، تضمین کرد؟

توصیه‌ی صریح نویسندگان

نکته‌ی کلیدی و شاید غیرمنتظره‌ی کتاب این است که معماران باید تا حد امکان از ساخت تراکنش‌های میان‌سرویسی (Cross-Service Transactions) خودداری کنند. عبور تراکنش از مرز سرویس‌ها، اصل بنیادین Decoupling در میکروسرویس را نقض می‌کند و بدترین نوع Dynamic Connascence یعنی Connascence of Value را ایجاد می‌کند. به همین دلیل توصیه‌ی مستقیم نویسندگان این است: «بهترین راهکار برای معمارانی که می‌خواهند تراکنش بین سرویس‌ها داشته باشند این است که این کار را نکنند! به‌جای آن، گرانولاریتی اجزا را اصلاح کنید». به بیان دیگر، نیاز مکرر به تراکنش بین چند سرویس معمولاً نشانه‌ای است که آن سرویس‌ها بیش از حد ریزدانه (Overly Fine-grained) طراحی شده‌اند و باید در قالب یک سرویس بزرگ‌تر با مرز تراکنشی واحد بازطراحی شوند.

وقتی تراکنش واقعاً اجتناب‌ناپذیر است: الگوی Saga

با این حال، در برخی فرآیندهای کسب‌وکاری واقعی (مانند مثال چک‌اوت سفارش که پیش‌تر بررسی شد)، حذف کامل هماهنگی بین سرویسی ممکن نیست. در این موارد، به‌جای تراکنش ACID سنتی، از الگوی Saga استفاده می‌شود که یک زنجیره از عملیات محلی را در هر سرویس اجرا می‌کند و در صورت شکست هر مرحله، به‌جای Rollback خودکار پایگاه‌داده، از Compensating Transactions (تراکنش‌های جبرانی) برای بازگرداندن سیستم به حالت پایدار استفاده می‌شود. برای مثال در سناریوی سفارش، اگر پرداخت شکست بخورد، به‌جای Rollback سفارش ثبت‌شده، یک عملیات جبرانی جداگانه (مثلاً لغو سفارش و بازگرداندن موجودی انبار) به‌صورت صریح فراخوانی می‌شود.

این جمع‌بندی، پایان ارکان اصلی معماری میکروسرویس در این کتاب است. نکته‌ی نهایی و مهم برای شما به‌عنوان یک توسعه‌دهنده‌ی ارشد این است: تصمیم به استفاده از Saga نباید راه‌حل پیش‌فرض باشد، بلکه باید ابتدا گرانولاریتی سرویس‌ها بازبینی شود؛ Saga هزینه‌ی پیچیدگی قابل‌توجهی (مدیریت حالت‌های میانی، Idempotency، و ردیابی خطا) بر دوش تیم می‌گذارد که باید در برابر مزایای Decoupling سنجیده شود.


معیارهای تصمیم‌گیری در انتخاب سبک معماری (Decision Criteria)

پس از بحث درباره‌ی تغییر مد در معماری، نویسندگان وارد بخش عملی‌تری می‌شوند: چه عواملی باید در ذهن معمار باشد تا او سبک معماری مناسب را انتخاب کند.

پنج پیش‌نیاز شناختی معمار

قبل از هر تصمیمی، معمار باید به این پنج حوزه تسلط داشته باشد:

  • دامنه (Domain): معمار لازم نیست متخصص دامنه باشد، اما باید درک کلی و درستی از جنبه‌های اصلی دامنه داشته باشد، به‌ویژه آن‌هایی که روی ویژگی‌های عملیاتی (Operational Architecture Characteristics) اثر می‌گذارند.
  • ویژگی‌های معماری موثر بر ساختار: معمار باید ویژگی‌هایی (-ilities) را که دامنه و فاکتورهای بیرونی نیاز دارند، کشف و تصریح کند.
  • معماری داده: معمار و DBA باید در مورد پایگاه‌داده، schema و مسائل مرتبط با داده همکاری کنند؛ به‌ویژه اگر سیستم جدید باید با معماری داده‌ی قدیمی/موجود تعامل داشته باشد.
  • فاکتورهای سازمانی: عناصر بیرونی مانند هزینه‌ی یک ارائه‌دهنده‌ی خاص Cloud، یا برنامه‌های ادغام و تصاحب (M&A) که معمار را به سمت راه‌حل‌های باز و یکپارچه‌سازی سوق می‌دهد.
  • آگاهی از فرایند، تیم‌ها و مسائل عملیاتی: مثلاً اگر سازمان بلوغ کافی در تمرین‌های مهندسی Agile نداشته باشد، سبک‌های معماری‌ای که وابسته به این تمرین‌ها برای موفقیت هستند، با مشکل مواجه می‌شوند.

همسانی توپولوژیک دامنه و معماری (Domain-Architecture Isomorphism)

نکته‌ی بسیار مهم و کاربردی این بخش، مفهوم Domain-Architecture Isomorphism است: گاهی توپولوژی خودِ دامنه با توپولوژی یک سبک معماری خاص هم‌شکل است.

  • Microkernel برای سیستم‌هایی که نیاز به شخصی‌سازی (Customizability) دارند ایده‌آل است؛ معمار می‌تواند شخصی‌سازی‌ها را به‌صورت Plug-in طراحی کند.
  • Space-Based Architecture برای دامنه‌هایی که تعداد زیادی عملیات مجزا و مستقل دارند (مثل تحلیل ژنوم) مناسب است، چون تعداد زیادی پردازنده‌ی مجزا ارائه می‌دهد.
  • در مقابل، دامنه‌ای با Semantic Coupling بالا (مثلاً یک فرم چندصفحه‌ای در یک شرکت بیمه که هر صفحه به Context صفحات قبلی وابسته است) با یک معماری بسیار Decoupled و توزیع‌شده مثل Microservices سازگاری ضعیفی دارد؛ در این حالت یک معماری با Coupling کمتر مثل Service-Based گزینه‌ی بهتری است.

سه پرسش کلیدی که معمار باید پاسخ دهد

با در نظر گرفتن موارد فوق، معمار باید سه تصمیم اصلی را اتخاذ کند:

  1. مونولیت یا توزیع‌شده؟ با استفاده از مفهوم Quantum، آیا یک مجموعه‌ی واحد از ویژگی‌های معماری کافی است (پس مونولیت مناسب است) یا بخش‌های مختلف سیستم به ویژگی‌های متفاوتی نیاز دارند (پس معماری توزیع‌شده لازم است).
  2. داده کجا باید زندگی کند؟ در مونولیت معمولاً یک پایگاه‌داده‌ی رابطه‌ای واحد فرض می‌شود؛ در معماری توزیع‌شده باید تصمیم گرفت کدام سرویس‌ها داده را Persist می‌کنند و داده چگونه در سیستم جریان می‌یابد.
  3. سبک ارتباط بین سرویس‌ها چیست — همزمان یا ناهمزمان؟ ارتباط همزمان (Synchronous) راحت‌تر است اما می‌تواند به مشکلات مقیاس‌پذیری و قابلیت اطمینان منجر شود؛ ارتباط ناهمزمان (Asynchronous) مزایای عملکرد و مقیاس‌پذیری دارد اما چالش‌هایی مثل همگام‌سازی داده، Deadlock، Race Condition و دشواری Debug به همراه می‌آورد.

نویسندگان یک قاعده‌ی طلایی بسیار مهم و پرکاربرد ارائه می‌دهند که در صنعت به‌شدت نقل می‌شود:

“Use synchronous by default, asynchronous when necessary.”

خروجی نهایی این فرایند طراحی سه چیز است: Architecture Topology، Architecture Decision Records (ADR) برای بخش‌هایی که بیشترین تلاش معمار را طلبیده‌اند، و Fitness Functions برای محافظت از اصول و ویژگی‌های عملیاتی مهم.


مطالعه‌ی موردی: Silicon Sandwiches (طراحی مونولیتی)

پس از بحث نظری درباره‌ی معیارهای تصمیم‌گیری، نویسندگان با یک کیس واقعی (Architecture Kata) این مفاهیم را عملیاتی می‌کنند. در این کاتا، پس از بررسی ویژگی‌های معماری مورد نیاز، تیم به این نتیجه رسید که یک Quantum واحد برای این سیستم کافی است؛ به‌علاوه، از آنجا که این یک اپلیکیشن ساده و بدون بودجه‌ی زیاد است، سادگی یک مونولیت جذابیت خاصی دارد.

نویسندگان برای همین دامنه‌ی واحد، دو طراحی متفاوت ارائه می‌دهند تا Trade-off بین آن‌ها را نشان دهند: یکی Domain-Partitioned و دیگری Technically-Partitioned.

طراحی اول: Modular Monolith

Modular Monolith کامپوننت‌هایی Domain-Centric را با یک پایگاه‌داده‌ی واحد می‌سازد و به‌صورت یک Quantum واحد Deploy می‌شود.

  • این معماری یک پایگاه‌داده‌ی رابطه‌ای واحد دارد و با یک UI وب پیاده‌سازی می‌شود، با در نظر گرفتن ملاحظات طراحی برای دستگاه‌های موبایل تا هزینه‌ی کلی پایین بماند.
  • هر یک از دامنه‌هایی که پیش‌تر شناسایی شده بودند، به‌صورت یک Component ظاهر می‌شوند.
  • توصیه‌ی مهم نویسندگان این است: اگر زمان و منابع کافی باشد، معمار باید همان جداسازی که در Components دامنه انجام داده را در سطح جداول و دیگر Assets پایگاه‌داده نیز رعایت کند؛ این کار مهاجرت آینده به یک معماری توزیع‌شده را در صورت نیاز به سادگی ممکن می‌سازد.
  • نکته‌ی فنی مهم: چون خودِ سبک Modular Monolith ذاتاً مکانیزمی برای Customization ندارد، معمار باید این ویژگی را بخشی از طراحی دامنه کند. راه‌حل ارائه‌شده یک endpoint به نام Override است که توسعه‌دهندگان می‌توانند شخصی‌سازی‌های فردی را در آن بارگذاری کنند، و هر Component دامنه باید برای هر ویژگی قابل‌شخصی‌سازی به Component Override ارجاع دهد — این خود یک نامزد عالی برای Fitness Function است.

طراحی دوم: Microkernel

از آنجا که یکی از ویژگی‌های شناسایی‌شده برای Silicon Sandwiches Customizability بود، با نگاه به Domain-Architecture Isomorphism، معمار می‌تواند این‌بار از سبک Microkernel استفاده کند.

  • در این طراحی، هسته‌ی سیستم (Core) از همان Domain Components و یک پایگاه‌داده‌ی رابطه‌ای واحد تشکیل شده است؛ همان‌طور که در طراحی قبلی، همگام‌سازی دقیق بین دامنه‌ها و طراحی داده امکان مهاجرت آینده‌ی Core به یک معماری توزیع‌شده را فراهم می‌کند.
  • هر Customization به‌صورت یک Plug-in ظاهر می‌شود: شخصی‌سازی‌های مشترک در یک مجموعه Plug-in با پایگاه‌داده‌ی متناظر خود، و مجموعه‌ای از Plug-inهای محلی که هرکدام داده‌ی خودشان را دارند. چون هیچ Plug-in‌ای نیازی به Coupling با Plug-in دیگر ندارد، همگی می‌توانند داده‌ی خودشان را نگه دارند و Decoupled باقی بمانند.
  • عنصر طراحی منحصربه‌فرد دیگر، استفاده از الگوی Backends for Frontends (BFF) است که لایه‌ی API را به یک آداپتور نازک برای Microkernel تبدیل می‌کند: بک‌اند اطلاعات عمومی ارائه می‌دهد و آداپتورهای BFF این اطلاعات کلی را به فرمت مناسب هر دستگاه Frontend ترجمه می‌کنند (مثلاً BFF مربوط به iOS، خروجی عمومی بک‌اند را متناسب با انتظارات اپلیکیشن Native iOS — از نظر فرمت داده، صفحه‌بندی، Latency و سایر فاکتورها — تنظیم می‌کند).

مطالعه‌ی موردی توزیع‌شده: Going, Going, Gone

پس از پرداختن به مونولیت در Silicon Sandwiches، فصل به سراغ کاتای دوم می‌رود که چالش‌های معماری بسیار متفاوتی دارد. برخلاف Silicon Sandwiches، در Going, Going, Gone (GGG) بخش‌های مختلف سیستم به ویژگی‌های معماری متفاوتی نیاز دارند — مثلاً Availability و Scalability بین نقش Auctioneer (حراج‌گزار) و Bidder (مزایده‌کننده) تفاوت اساسی دارد.

چرا معماری توزیع‌شده انتخاب شد

الزامات GGG سطوح بسیار جدی‌ای از Scale، Elasticity و Performance را مطرح می‌کنند و معمار باید سبکی را انتخاب کند که امکان شخصی‌سازی دقیق در سطح ریزدانه (Fine-Grained) را در معماری فراهم کند.

  • از میان معماری‌های توزیع‌شده‌ی نامزد، هم Event-Driven و هم Microservices با بسیاری از ویژگی‌های معماری مطابقت دارند.
  • اما Microservices بهتر از ویژگی‌های عملیاتی متفاوت پشتیبانی می‌کند، چون معماری‌های خالص Event-Driven معمولاً بخش‌ها را بر اساس این ویژگی‌ها جدا نمی‌کنند بلکه بر اساس سبک ارتباطی (Orchestrated در برابر Choreographed) تفکیک می‌شوند.
  • چالش اصلی رسیدن به Performance مطلوب در Microservices است، اما معمار می‌تواند با طراحی هدفمند (مثلاً استفاده از REST یا Messaging ناهمزمان) این نقطه‌ضعف را جبران کند.

سرویس‌های شناسایی‌شده در طراحی نهایی

طراحی نهایی GGG به هفت سرویس مجزا رسید که هرکدام مسئولیت مشخصی دارند:

سرویسمسئولیت
BidCaptureدریافت پیشنهادات آنلاین مزایده‌کنندگان و ارسال ناهمزمان آن‌ها به Bid Tracker؛ بدون نیاز به Persistence
BidStreamerاستریم پیشنهادات به مزایده‌کنندگان آنلاین، به‌صورت Read-Only و با کارایی بالا
BidTrackerیکپارچه‌سازی و ترتیب‌دهی پیشنهادات از دو منبع (Auctioneer و Bidder) با نزدیک‌ترین حالت به Real-Time
Auctioneer Captureدریافت پیشنهادات حراج‌گزار؛ به دلیل ویژگی‌های معماری متفاوت از BidCapture جدا شد
Auction Sessionمدیریت گردش‌کار (Workflow) هر حراج به‌صورت مجزا
Paymentارائه‌دهنده‌ی پرداخت شخص‌ثالث که پس از پایان Auction Session فعال می‌شود
Video Capture / Video Streamerضبط و استریم ویدیوی زنده‌ی حراج به مزایده‌کنندگان آنلاین

همزمان یا ناهمزمان؟ تصمیمی که به‌دقت توجیه شده

نکته‌ی کلیدی این طراحی، انتخاب هوشمندانه‌ی سبک ارتباطی برای هر ارتباط بین سرویسی است، نه یک قاعده‌ی یکسان برای همه.

معمار به‌دقت هم ارتباط Synchronous و هم Asynchronous را در این معماری شناسایی کرد؛ انتخاب Asynchronous در جاهایی که لازم بود، اساساً برای تطبیق با ویژگی‌های عملیاتی متفاوت بین سرویس‌ها بود. مثال روشن‌کننده این است: اگر سرویس Payment تنها بتواند هر ۵۰۰ میلی‌ثانیه یک پرداخت جدید را پردازش کند و تعداد زیادی از حراج‌ها هم‌زمان پایان یابند، ارتباط همزمان بین سرویس‌ها باعث Timeout و مشکلات Reliability می‌شد؛ استفاده از Message Queue به این بخش شکننده‌ی معماری، Reliability اضافه می‌کند.

تحلیل Quantum و نتیجه‌ی نهایی طراحی

در تحلیل نهایی، این طراحی به پنج Quantum مجزا رسید: Payment، Auctioneer، Bidder، Bidder Streams، و Bid Tracker، که تقریباً با سرویس‌ها مطابقت دارند. نویسندگان تأکید می‌کنند که این طراحی «تنها» یا حتی «بهترین» طراحی ممکن برای GGG نیست، بلکه صرفاً مجموعه‌ای از Trade-offهای کمترین بد (Least Worst) را ارائه می‌دهد.


پیاده‌سازی نهایی: معماری میکروسرویس برای GGG

با تحلیل Quantum که در فصل هشتم انجام شد، اکنون نویسندگان طراحی کامل و نهایی Going, Going, Gone را با معماری Microservices ترسیم می‌کنند. هر Component شناسایی‌شده در تحلیل قبلی، به یک سرویس مستقل تبدیل شده و Granularity کامپوننت و سرویس با هم منطبق شده‌اند.

سه رابط کاربری متمایز

طراحی GGG سه نوع رابط کاربری مجزا دارد که هرکدام نیازهای متفاوتی دارند:

  • Bidder: تعداد زیادی از مزایده‌کنندگان آنلاین که هم‌زمان در حراج شرکت می‌کنند.
  • Auctioneer: تنها یک نفر به ازای هر حراج، اما با نیاز به بالاترین سطح Availability و Reliability.
  • Streamer: سرویسی که مسئول استریم ویدیو و جریان پیشنهادات برای مزایده‌کنندگان است؛ چون این جریان صرفاً Read-Only است، امکان بهینه‌سازی‌هایی وجود دارد که در صورت نیاز به Update امکان‌پذیر نبود.

فهرست کامل سرویس‌ها با جزئیات فنی

هر سرویس در این معماری با توجیه دقیق فنی طراحی شده است:

  • BidCapture: ورودی‌های مزایده‌کنندگان آنلاین را دریافت و به‌صورت ناهمزمان (Asynchronous) به Bid Tracker ارسال می‌کند؛ چون صرفاً نقش یک کانال انتقال را دارد، به هیچ Persistence نیازی ندارد.
  • BidStreamer: پیشنهادات را با کارایی بالا و به‌صورت Read-Only به مزایده‌کنندگان آنلاین استریم می‌کند.
  • BidTracker: پیشنهادات را از دو منبع Auctioneer Capture و Bid Capture ردیابی می‌کند و این دو جریان اطلاعاتی کاملاً متفاوت را با نزدیک‌ترین حالت به Real-Time یکپارچه می‌سازد؛ نکته‌ی کلیدی این‌جاست که هر دو اتصال ورودی به این سرویس Asynchronous هستند و این امکان را می‌دهد که توسعه‌دهندگان از Message Queue به‌عنوان Buffer برای مدیریت نرخ‌های بسیار متفاوت جریان پیام استفاده کنند.
  • Auctioneer Capture: پیشنهادات حراج‌گزار را ثبت می‌کند؛ نتیجه‌ی تحلیل Quantum در فصل قبل، معمار را به این نتیجه رساند که این سرویس باید از Bid Capture جدا باشد چون ویژگی‌های معماری کاملاً متفاوتی دارند.
  • Auction Session: گردش‌کار (Workflow) هر حراج مجزا را مدیریت می‌کند.
  • Payment: ارائه‌دهنده‌ی پرداخت شخص‌ثالث که اطلاعات پرداخت را پس از اتمام Auction Session پردازش می‌کند.
  • Video Capture و Video Streamer: به‌ترتیب مسئول ضبط و استریم ویدیوی زنده‌ی حراج به مزایده‌کنندگان آنلاین هستند.

توجیه دقیق تصمیم Synchronous در برابر Asynchronous

مثال ملموس کتاب برای درک عمیق‌تر این تصمیم بسیار روشنگر است:

معمار به‌دقت هم ارتباطات Synchronous و هم Asynchronous را در این معماری شناسایی کرد، و انتخاب Asynchronous در جاهای حساس، اساساً به‌خاطر تطبیق با ویژگی‌های عملیاتی متفاوت بین سرویس‌ها بود. برای مثال، اگر سرویس Payment تنها بتواند هر ۵۰۰ میلی‌ثانیه یک پرداخت جدید را پردازش کند و تعداد زیادی از حراج‌ها هم‌زمان به پایان برسند، ارتباط Synchronous بین سرویس‌ها باعث Timeout و مشکلات Reliability می‌شد؛ استفاده از Message Queue به این بخش شکننده‌ی معماری، Reliability اضافه می‌کند.

نتیجه‌گیری نهایی فصل: پنج Quantum

تحلیل نهایی به پنج Quantum مجزا رسید: Payment، Auctioneer، Bidder، Bidder Streams، و Bid Tracker، که تقریباً با سرویس‌ها مطابقت دارند (نمونه‌های متعدد از هر سرویس با نماد پشته‌ای از Containerها در دیاگرام نشان داده شده‌اند).

نویسندگان با یک هشدار مهم فصل را می‌بندند: این طراحی، طراحی «صحیح» یا حتی «بهترین» ممکن برای GGG نیست، بلکه صرفاً مجموعه‌ای است که کمترین بدیِ Trade-offها را دارد. انتخاب Microservices همراه با استفاده هوشمند از Events و Messages، به معمار اجازه می‌دهد بیشترین بهره را از یک الگوی عمومی معماری بگیرد و در عین حال پایه‌ای برای توسعه و گسترش آینده بسازد.


پایان فصل ۱۸ و سوالات خودارزیابی

فصل ۱۸ با یک بخش سوالات خودارزیابی (Self-Assessment Questions) به پایان می‌رسد که هدف آن‌ها آزمایش درک عمیق خواننده از مفاهیم فصل است، نه صرفاً حفظیات.

چهار سوال کلیدی فصل

این سوالات دقیقاً محورهای اصلی فصل را بازتاب می‌دهند و ارزش دارد قبل از رفتن به فصل بعد به آن‌ها فکر کنید:

  • معماری داده (منطقی و فیزیکی) چگونه بر انتخاب سبک معماری اثر می‌گذارد؟
  • این اثرگذاری چگونه در انتخاب نهایی شما نمایان می‌شود؟
  • گام‌هایی که معمار برای تعیین سبک معماری، تفکیک داده (Data Partitioning)، و سبک‌های ارتباطی طی می‌کند را مشخص کنید.
  • چه عاملی معمار را به سمت یک معماری توزیع‌شده سوق می‌دهد؟

تحلیل انتقادی این سوالات

پاسخ به این سوالات در واقع خلاصه‌ی کل فصل است: عامل تعیین‌کننده برای معماری توزیع‌شده، نیاز به مجموعه‌های متفاوت از ویژگی‌های معماری (Architecture Characteristics) در بخش‌های مختلف سیستم است — همان چیزی که در قالب مفهوم Quantum در کیس Going, Going, Gone دیدیم؛ اگر یک Quantum واحد کافی باشد (مثل Silicon Sandwiches)، مونولیت گزینه‌ی منطقی‌تری است. گام‌های کلی تصمیم‌گیری معمار نیز دقیقاً سه محوری بودند که پیش‌تر دیدیم: تصمیم مونولیت در برابر توزیع‌شده، محل نگهداری داده، و سبک ارتباط (Synchronous در برابر Asynchronous).

با این بخش، فصل ۱۸ (Choosing the Appropriate Architecture Style) به‌طور کامل به پایان رسید. فصل بعدی، فصل ۱۹: Architecture Decisions است که به موضوعاتی مثل Anti-patternهای تصمیم‌گیری (نظیر Covering Your Assets و Email-Driven Architecture)، معیارهای Michael Nygard برای تشخیص تصمیمات معماری قابل‌توجه، و ساختار Architecture Decision Records (ADR) می‌پردازد.


فصل ۱۹: تصمیمات معماری (Architecture Decisions)

اکنون کتاب به بخش سوم منتقل می‌شود — Techniques and Soft Skills — و فصل ۱۹ با موضوع تصمیمات معماری آغاز می‌شود. یکی از انتظارات اصلی نقش معمار، اتخاذ تصمیمات معماری است؛ فرایند این کار شامل جمع‌آوری اطلاعات کافی، توجیه تصمیم، مستندسازی آن، و ارتباط مؤثر آن با ذی‌نفعان مناسب است.

سه Anti-Pattern پیش‌رونده در تصمیم‌گیری

نکته‌ی جالب این بخش، ارتباط زنجیره‌ای این سه Anti-Pattern با یکدیگر است: غلبه بر Anti-Pattern اول منجر به بروز دومی می‌شود، و غلبه بر دومی منجر به بروز سومی می‌گردد.

  • Covering Your Assets: وقتی معمار از ترس تصمیم اشتباه، اتخاذ تصمیم را به تعویق می‌اندازد یا از آن اجتناب می‌کند. راه‌حل، صبر تا Last Responsible Moment (نه بیشتر، تا در دام Analysis Paralysis نیفتیم) و همکاری مستمر با تیم توسعه برای اعتبارسنجی سریع تصمیم است.
  • Groundhog Day: وقتی هیچ‌کس دلیل یک تصمیم را نمی‌داند، پس آن تصمیم بی‌وقفه دوباره و دوباره مورد بحث قرار می‌گیرد. راه‌حل، ارائه‌ی توجیه کامل هم فنی و هم کسب‌وکاری برای هر تصمیم است؛ چهار توجیه رایج کسب‌وکاری شامل هزینه، Time to Market، رضایت کاربر، و موضع‌گیری استراتژیک هستند.
  • Email-Driven Architecture: وقتی افراد تصمیمی را که گرفته شده گم می‌کنند، فراموش می‌کنند یا حتی از آن بی‌خبرند. راه‌حل، عدم قرار دادن متن تصمیم در بدنه‌ی ایمیل (بلکه فقط لینک به یک منبع واحد مثل Wiki) و اطلاع‌رسانی فقط به افرادی که تصمیم مستقیماً روی آن‌ها اثر می‌گذارد.

معیار Architecturally Significant به روایت Michael Nygard

نکته‌ی مهم فنی: بسیاری از معماران تصور می‌کنند اگر تصمیمی شامل یک تکنولوژی خاص باشد، «تصمیم فنی» است نه «تصمیم معماری» — این تصور همیشه درست نیست.

Michael Nygard پنج عامل را برای تشخیص تصمیمات Architecturally Significant تعریف می‌کند:

  • Structure: تصمیماتی که الگو یا سبک معماری را تحت تأثیر قرار می‌دهند (مثلاً اشتراک‌گذاری داده بین چند Microservice که Bounded Context را تحت تأثیر قرار می‌دهد).
  • Nonfunctional Characteristics: اگر انتخاب یک تکنولوژی روی یک -ility مهم (مثل Performance) اثر بگذارد، این یک تصمیم معماری است.
  • Dependencies: نقاط Coupling بین کامپوننت‌ها/سرویس‌ها که روی Scalability، Modularity، Agility و… اثر می‌گذارند.
  • Interfaces: نحوه‌ی دسترسی و Orchestration سرویس‌ها (معمولاً از طریق Gateway یا API Proxy)، شامل تعریف Contractها و استراتژی نسخه‌بندی.
  • Construction Techniques: تصمیمات درباره‌ی پلتفرم، فریمورک، ابزار یا حتی فرایند که ذاتاً فنی هستند اما روی جنبه‌ای از معماری اثر می‌گذارند.

Architecture Decision Records (ADR)

یکی از مؤثرترین روش‌های مستندسازی تصمیمات معماری، ADR است که ابتدا توسط Michael Nygard در یک وبلاگ معرفی و بعدها در ThoughtWorks Technology Radar با برچسب “Adopt” مشخص شد. ابزار متن‌باز ADR-tools توسط Nat Pryce برای مدیریت این اسناد نوشته شده است.

ساختار پایه‌ی یک ADR از پنج بخش اصلی به‌همراه دو بخش پیشنهادی تشکیل شده:

بخشمحتوا
Titleعبارت کوتاه و شماره‌دار که تصمیم را توصیف می‌کند (مثلاً “42. Use of Asynchronous Messaging Between Order and Payment Services”)
StatusProposed، Accepted، یا Superseded
Contextنیروهایی که معمار را به این تصمیم سوق داده‌اند؛ در واقع نوعی مستندسازی خودِ معماری
Decisionخودِ تصمیم به‌همراه توجیه کامل، با لحن دستوری قاطع (مثلاً “we will use…” نه “I think…”)
Consequencesپیامدهای مثبت و منفی تصمیم؛ تحلیل Trade-off
Compliance (پیشنهادی)آیا این تصمیم به‌صورت خودکار (Fitness Function) یا دستی قابل پایش است؟
Notes (پیشنهادی)سایر یادداشت‌های تکمیلی

قاعده‌ی طلایی بخش Decision

نکته‌ی بسیار مهم و کاربردی این بخش، اهمیت نگارش تصمیم با فعل امری و قاطع است، چون بخش Decision بیش از هر بخش دیگر به معمار اجازه می‌دهد تأکید را روی چرایی (Why) بگذارد نه صرفاً چگونگی (How) — و درک چرایی یک تصمیم، از تکرار اشتباهات گذشته در بازطراحی‌های آینده جلوگیری می‌کند.


سه بخش کلیدی باقی‌مانده در ساختار ADR

پس از بررسی Title، Status، Context، Decision و Consequences در بخش قبل، سه بخش تکمیلی و بسیار کاربردی ADR باقی می‌ماند که نویسندگان به‌شدت توصیه به افزودن آن‌ها می‌کنند، هرچند بخشی از استاندارد پایه نیستند.

وضعیت RFC: راه‌حلی برای اجتناب از Analysis Paralysis

اگر معمار بخواهد پیش‌نویس یک ADR را برای دریافت نظر جمع بزرگ‌تری از ذی‌نفعان ارسال کند، پیشنهاد می‌شود یک وضعیت جدید به‌نام Request for Comments (RFC) با یک تاریخ Deadline مشخص ایجاد شود؛ این کار مانع بحث بی‌پایان و بدون تصمیم‌گیری نهایی (Analysis Paralysis) می‌شود. پس از رسیدن به Deadline، معمار نظرات را تحلیل می‌کند، تنظیمات لازم را انجام می‌دهد و وضعیت را به Proposed یا Accepted تغییر می‌دهد.

نکته‌ی مهم درباره‌ی Status، سه معیار پایه برای گفتگو درباره‌ی سطح اختیار تصمیم‌گیری معمار است: هزینه، تأثیر بین‌تیمی، و امنیت. اگر هزینه‌ی اجرای تصمیم از حد معینی (مثلاً بر اساس نرخ FTE) بگذرد یا روی تیم‌های دیگر/امنیت اثر بگذارد، تصمیم نباید توسط معمار به‌تنهایی تأیید شود.

Compliance: پیوند تصمیم به Fitness Function

بخش Compliance معمار را وادار می‌کند فکر کند که آیا رعایت این تصمیم به‌صورت دستی یا خودکار (از طریق Fitness Function) پایش خواهد شد.

مثال ملموس کتاب: تصمیم بگیرید که تمام اشیاء مشترک استفاده‌شده توسط Business Objects باید در لایه‌ی Shared Services قرار گیرند. این تصمیم را می‌توان با ابزارهایی مثل ArchUnit در جاوا یا NetArchTest در سی‌شارپ به‌صورت خودکار پایش کرد. کد نمونه‌ی Fitness Function برای این تصمیم:

1
2
3
4
5
classes()
  .that.areAnnotatedWith(SharedService.class)
  .should().resideInAPackage("..services..")
  .because("All shared services classes used by business objects in the business layer should reside in the services layer")
  .check(myClasses)

Notes: متادیتای ضروری حتی در Git

بخش Notes شامل متادیتای مهمی است که حتی وقتی ADR در یک سیستم Version Control مثل Git نگهداری می‌شود، باز هم مفید است:

  • نویسنده‌ی اصلی، تاریخ تأیید، تأییدکننده، تاریخ Supersede شدن، تاریخ آخرین ویرایش، ویرایش‌گر، و آخرین تغییر.

نگهداری ADRها: چرا نه همیشه در Git

نویسندگان یک هشدار عملی مهم می‌دهند: هرچند برخی معماران ترجیح می‌دهند ADRها را در همان مخزن Git کد نگه دارند (که مزیت Versioning دارد)، برای سازمان‌های بزرگ این کار توصیه نمی‌شود، چون همه‌ی ذی‌نفعانی که باید تصمیم را ببینند، لزوماً به آن مخزن Git دسترسی ندارند؛ همچنین این محل مناسبی برای تصمیمات با Context بیرون از یک اپلیکیشن خاص (مثل تصمیمات Integration یا Enterprise) نیست.

ساختار پوشه‌بندی پیشنهادی سه سطح دارد:

  • Application: شامل زیرپوشه‌ی Common (تصمیمات مشترک بین همه‌ی اپلیکیشن‌ها) و زیرپوشه‌های مخصوص هر اپلیکیشن.
  • Integration: تصمیمات مربوط به ارتباط بین اپلیکیشن‌ها، سیستم‌ها یا سرویس‌ها.
  • Enterprise: تصمیمات معماری سراسری که همه‌ی سیستم‌ها را تحت تأثیر قرار می‌دهند.

ADR به‌عنوان ابزار مستندسازی و استاندارد‌گذاری

دو کاربرد ارزشمند دیگر برای ADR وجود دارد که نویسندگان به آن‌ها تأکید ویژه دارند:

  • مستندسازی معماری: با وجود عدم وجود استاندارد جامع برای مستندسازی معماری (به‌جز تلاش‌هایی مثل C4 Model ساخته‌ی Simon Brown یا استاندارد ArchiMate)، بخش Context یک ADR به‌طور مؤثر بخشی از خودِ معماری را توصیف می‌کند، و بخش Decision بهترین شکل ممکن از مستندسازی «چرایی» یک تصمیم است.
  • استاندارد‌گذاری: بسیاری افراد از Standardها بیزارند چون به نظر می‌رسد بیشتر برای کنترل افراد وضع شده‌اند تا حل مشکل واقعی. استفاده از ADR برای استانداردها این وضعیت را تغییر می‌دهد: بخش Context دلیل وجود استاندارد را توضیح می‌دهد و بخش Decision نشان می‌دهد چرا استاندارد باید وجود داشته باشد؛ اگر معمار نتواند این توجیه را ارائه دهد، شاید آن استاندارد اصلاً نباید ساخته شود.

مثال کامل: ADR برای Going, Going, Gone

در سیستم حراج GGG، ده‌ها تصمیم معماری وجود دارد (استفاده از Microservices رویداد‌محور، تفکیک رابط کاربری Bidder و Auctioneer، استفاده از پروتکل RTP برای Video Capture، استفاده از یک لایه‌ی API واحد، و استفاده از پیام‌رسانی Publish-and-Subscribe) که همگی باید مستند و توجیه شوند. نمونه‌ی مشخص، ADR شماره ۷۶ با عنوان “Asynchronous PubSub Messaging Between Bidding Services” است که استفاده از پیام‌رسانی Pub/Sub بین سرویس‌های Bid Capture، Bid Streamer، و Bid Tracker را توجیه می‌کند.

با این توضیحات، فصل ۱۹ به پایان می‌رسد و به سوالات خودارزیابی خود می‌رسد:

  1. Anti-Pattern «Covering Your Assets» چیست؟
  2. چه تکنیک‌هایی برای اجتناب از Anti-Pattern «Email-Driven Architecture» وجود دارد؟
  3. پنج عامل Michael Nygard برای تشخیص «Architecturally Significant» چیست؟
  4. پنج بخش پایه‌ی یک ADR کدامند؟
  5. توجیه یک تصمیم معماری معمولاً در کدام بخش ADR قرار می‌گیرد؟
  6. در صورت نبود بخش Alternatives، فهرست گزینه‌های جایگزین در کدام بخش قرار می‌گیرد؟
  7. سه معیار پایه برای علامت‌گذاری وضعیت یک ADR به‌عنوان Proposed چیست؟

فصل ۲۰: تحلیل ریسک معماری

با پایان فصل ۱۹، اکنون به فصل ۲۰ می‌رسیم که به یکی از فعالیت‌های کلیدی معمار می‌پردازد: تحلیل مستمر ریسک معماری برای شناسایی نقاط ضعف و اقدام اصلاحی به‌موقع.

ماتریس ریسک: ابزاری برای کاهش سوگیری ذهنی

بزرگ‌ترین چالش در ارزیابی ریسک، تعیین Subjective سطح ریسک (کم، متوسط، زیاد) است که معمولاً باعث سردرگمی می‌شود. راه‌حل کتاب، یک ماتریس دو‌بعدی است که دو محور Impact (تأثیر) و Likelihood (احتمال وقوع) را در سه سطح Low (۱)، Medium (۲) و High (۳) ضرب می‌کند.

حاصل‌ضرب این دو عدد، امتیاز نهایی ریسک را تعیین می‌کند:

  • امتیاز ۱ و ۲: ریسک کم (سبز)
  • امتیاز ۳ و ۴: ریسک متوسط (زرد)
  • امتیاز ۶ تا ۹: ریسک بالا (قرمز)

نکته‌ی عملی مهم: همیشه ابتدا محور Impact و سپس محور Likelihood را در نظر بگیرید. مثال کتاب: اگر یک پایگاه‌داده‌ی مرکزی از کار بیفتد، Impact بالاست (۳)، اما اگر روی سرورهای Cluster شده با دسترس‌پذیری بالا قرار داشته باشد، Likelihood پایین است (۱)؛ نتیجه، ریسک متوسط (۳) خواهد بود، نه بالا.

گزارش Risk Assessment و نمایش روند تغییر

با تجمیع امتیازهای ماتریس بر اساس معیارهایی مثل Availability، Performance یا Data Integrity در برابر سرویس‌ها یا دامنه‌های مختلف، یک گزارش Risk Assessment ساخته می‌شود که نواحی پرریسک سیستم را برجسته می‌کند. برای نمایش روند تغییرات (بهتر یا بدتر شدن) به‌جای فلش‌های گیج‌کننده (که در آزمایش نویسندگان تقریباً ۵۰-۵۰ افراد آن را متضاد تفسیر کردند)، پیشنهاد می‌شود از علامت + / - یا فلش همراه با عدد هدف استفاده شود.

Risk Storming: چرا نباید یک معمار به‌تنهایی تصمیم بگیرد

هیچ معماری به‌تنهایی نمی‌تواند کل ریسک یک سیستم را شناسایی کند، چون هیچ معماری دانش کامل از تمام بخش‌های سیستم ندارد. Risk Storming یک تمرین مشارکتی است که با حضور معماران، توسعه‌دهندگان ارشد و Tech Lead روی یک بُعد مشخص از ریسک (مثل Performance، Availability، یا Unproven Technology) اجرا می‌شود.

این فرایند سه فعالیت اصلی دارد که ترتیب و ماهیت مشارکتی آن‌ها بسیار مهم است:

  • Identification (انفرادی): هر شرکت‌کننده به‌تنهایی و بدون تأثیرگذاری بر دیگران، ریسک را با ماتریس ارزیابی می‌کند و روی کاغذ Post-it یادداشت می‌نویسد.
  • Consensus (مشارکتی): همه‌ی یادداشت‌ها روی نمودار معماری قرار می‌گیرند و تیم درباره‌ی اختلاف‌نظرها بحث می‌کند تا به توافق برسد.
  • Mitigation (مشارکتی): تیم راه‌حل‌هایی برای کاهش ریسک شناسایی‌شده طراحی می‌کند، که معمولاً هزینه‌ی اضافی دارد و باید با ذی‌نفعان کسب‌وکار مذاکره شود.

نکته‌ی بسیار مهم فنی: برای تکنولوژی‌های ناشناخته یا اثبات‌نشده (Unproven)، همیشه باید بالاترین امتیاز ریسک (۹) اختصاص یابد، چون ماتریس ریسک معمول برای این بُعد کاربردی نیست.

مثال کامل: سیستم مشاوره تلفنی پرستاران

کتاب یک مثال کامل و بسیار آموزنده ارائه می‌دهد: سیستمی برای مشاوره‌ی پزشکی تلفنی که باید HIPAA-Compliant باشد و بار متغیر فصلی (مثل فصل سرماخوردگی) را مدیریت کند. سه دور Risk Storming روی این سیستم اجرا شد که هرکدام تغییرات معماری مهمی ایجاد کردند:

بُعد ریسکریسک شناسایی‌شدهراه‌حل کاهش ریسک
Availabilityپایگاه‌داده‌ی مرکزی (ریسک ۶)؛ موتور Diagnostics (ریسک ۹) 
Elasticityرابط موتور Diagnostics هنگام اوج بار (ریسک ۹) 
SecurityAPI Gateway مشترک بین همه‌ی کاربران (ریسک ۶) 

این مثال نشان می‌دهد چگونه Risk Storming می‌تواند مشکلاتی را کشف کند که یک معمار به‌تنهایی (که در این مثال ریسک Security را فقط ۲ ارزیابی کرده بود) هرگز متوجه آن‌ها نمی‌شد.


فصل ۲۱: دیاگرام‌کشی و ارائه معماری

پس از تکمیل مثال Risk Storming (که بخش Security آن نشان داد چگونه یک API Gateway مشترک ریسک بالای امنیتی داشت و راه‌حل، تفکیک آن به سه Gateway جداگانه برای Admin، Self-service و Nurse بود)، فصل ۲۱ به یکی از مهارت‌های نرم حیاتی معمار می‌پردازد: توانایی ترسیم و ارائه‌ی معماری. نکته‌ی کلیدی این فصل، Representational Consistency است — یعنی همیشه قبل از نمایش جزئیات یک بخش، رابطه‌ی آن بخش با کل توپولوژی معماری را نشان دهید تا مخاطب گیج نشود.

آنتی‌پترن Irrational Artifact Attachment

نکته‌ی روان‌شناختی بسیار جالب این بخش، رابطه‌ی مستقیم بین زمان صرف‌شده برای ساخت یک آرتیفکت و میزان وابستگی غیرمنطقی معمار به آن است.

اگر معماری با ابزاری مثل Visio دو ساعت وقت صرف ساخت یک دیاگرام زیبا کند، وابستگی غیرمنطقی او به آن دیاگرام تقریباً متناسب با همان دو ساعت خواهد بود. راه‌حل، استفاده از ابزارهای کم‌فناوری (Whiteboard، Tablet، کارت‌های Index) در مراحل اولیه‌ی طراحی است تا تیم بتواند بدون دلبستگی، ایده‌ها را آزادانه دور بریزد و اصلاح کند.

سه استاندارد دیاگرام‌کشی: UML، C4، ArchiMate

استانداردویژگی کلیدی
UMLترکیب سه فلسفه‌ی طراحی رقیب دهه ۸۰؛ امروزه فقط Class و Sequence Diagram آن پرکاربرد باقی مانده‌اند
C4ابداع Simon Brown با چهار سطح Context، Container، Component، Class؛ برای معماری‌های مونولیتیک بهتر عمل می‌کند تا معماری‌های توزیع‌شده مثل Microservices
ArchiMateاستاندارد The Open Group؛ زبان مدل‌سازی سبک‌وزن برای اکوسیستم‌های سازمانی

نکته‌ی مهم قراردادی در دیاگرام‌کشی: خطوط ممتد نشان‌دهنده‌ی ارتباط Synchronous و خطوط چین نشان‌دهنده‌ی ارتباط Asynchronous هستند — تقریباً تنها استاندارد فراگیر در این حوزه.

قواعد کلیدی دیاگرام‌کشی

  • Titles: همه‌ی عناصر دیاگرام باید عنوان‌گذاری شوند، حتی با چرخش متن برای صرفه‌جویی در فضا.
  • Lines: خطوط باید به‌وضوح دیده شوند و فلش‌ها جهت جریان اطلاعات را نشان دهند.
  • Color: رنگ باید برای تفکیک آرتیفکت‌های متفاوت (نه صرفاً زیبایی) استفاده شود.
  • Keys: در صورت هرگونه ابهام در شکل‌ها، حتماً یک راهنما (Key) اضافه کنید؛ هیچ‌چیز بدتر از دیاگرامی نیست که به تفسیر غلط منجر شود.

مهارت ارائه: مدیریت زمان و آنتی‌پترن Bullet-Riddled Corpse

تفاوت بنیادی بین سند و ارائه، کنترل زمان است: در سند، خواننده سرعت را کنترل می‌کند؛ در ارائه، ارائه‌دهنده این کنترل را دارد. ابزارهای ارائه دو روش برای مدیریت زمان دارند: Transitions (بین اسلایدها) و Animations (درون یک اسلاید).

مهم‌ترین آنتی‌پترن این بخش، Bullet-Riddled Corpse است — وقتی هر اسلاید عملاً یادداشت‌های سخنران است که برای همه نمایش داده می‌شود.

مشکل این آنتی‌پترن این است که ارائه‌دهنده دو کانال اطلاعاتی (کلامی و بصری) دارد، اما با نوشتن متن زیاد روی اسلاید و سپس تکرار همان کلمات، یک کانال را اشباع و دیگری را محروم می‌کند. راه‌حل، Incremental Builds است: به‌جای نمایش کل تصویر یکجا، با پوشاندن بخش‌هایی از آن با یک باکس سفید بی‌کادر و آشکارسازی تدریجی، تعلیق (Suspense) در ارائه حفظ می‌شود.

نکته‌ی عملی جالب پایانی: اگر ارائه‌دهنده بخواهد توجه کامل مخاطب را به خودش برگرداند، کافی است یک اسلاید خالی نمایش دهد — در این حالت، ارائه‌دهنده تنها چیز جالب در اتاق برای نگاه کردن می‌شود.


فصل ۲۲: مؤثرسازی تیم‌ها

فصل ۲۲ به یکی از مسئولیت‌های کمتر شناخته‌شده‌ی معمار می‌پردازد: علاوه بر طراحی معماری، معمار مسئول هدایت تیم توسعه در مسیر پیاده‌سازی صحیح آن معماری نیز هست. تجربه‌ی نویسندگان نشان می‌دهد معماری که تیم توسعه را نادیده بگیرد و در انزوا معماری طراحی کند، معماری‌ای تولید می‌کند که تیم در پیاده‌سازی آن دچار مشکل خواهد شد.

مرزهای تیم: جعبه‌ای که معمار می‌سازد

یکی از نقش‌های اصلی معمار، ایجاد و انتقال محدودیت‌ها یا همان «جعبه‌ای» است که توسعه‌دهندگان درون آن معماری را پیاده‌سازی می‌کنند. این مرزها می‌توانند بیش‌ازحد تنگ، بیش‌ازحد گشاد، یا مناسب باشند — و هر کدام تأثیر مستقیمی بر توانایی تیم در پیاده‌سازی صحیح معماری دارند.

سه شخصیت معماری

هر شخصیت معماری دقیقاً با یک نوع مرز مطابقت دارد:

  • Control Freak: سعی می‌کند هر جزئیات کوچک فرایند توسعه را کنترل کند؛ مثلاً ممنوعیت استفاده از کتابخانه‌های Open Source، محدودیت‌های سخت روی Naming Convention، یا حتی نوشتن Pseudocode برای تیم. این شخصیت مرزهای تنگ می‌سازد و هنر برنامه‌نویسی را از توسعه‌دهندگان می‌رباید. نکته‌ی مهم: تازه‌واردان به نقش معماری، به‌راحتی در این تله می‌افتند، چون وسوسه می‌شوند همان کاری را که به‌عنوان توسعه‌دهنده انجام می‌دادند (طراحی Class Diagram و Design Pattern) ادامه دهند، در حالی که این دیگر نقش آن‌ها نیست.
  • Armchair Architect: معماری که مدت‌هاست کدنویسی نکرده و جزئیات پیاده‌سازی را در طراحی نادیده می‌گیرد؛ اغلب از تیم‌های توسعه بی‌خبر یا غایب است. نکته‌ی طنازانه‌ی کتاب: کدنویسی را نمی‌توان جعل کرد (یا توسعه‌دهنده کد می‌نویسد یا نمی‌نویسد)، اما معماری را می‌توان جعل کرد، چون هیچ‌کس دقیقاً نمی‌داند یک معمار باید چه‌کاری انجام دهد! این شخصیت مرزهای بیش‌ازحد گشاد ایجاد می‌کند و تیم را مجبور می‌کند خودش نقش معمار را بازی کند.
  • Effective Architect: محدودیت‌های مناسب ایجاد می‌کند، ابزار و تکنولوژی درست را تضمین می‌کند، و موانع پیش‌روی تیم را برمی‌دارد.

چارچوب پنج‌عاملی برای تعیین میزان کنترل

با اقتباس از مفهوم Elastic Leadership روی اوشروف، نویسندگان پنج عامل مشخص را برای تعیین میزان کنترل معمار معرفی می‌کنند (در مقیاس ۲۰- تا ۲۰+، منفی به‌سمت Armchair و مثبت به‌سمت Control Freak):

عاملجهت افزایش کنترلجهت کاهش کنترل
Team Familiarityاعضای جدید و ناآشنا 
Team Sizeتیم بزرگ (بیش از ۱۲ نفر) 
Overall Experienceتوسعه‌دهندگان Junior 
Project Complexityپروژه‌ی پیچیده 
Project Durationپروژه‌ی طولانی (مثلاً ۲ سال) 

نکته‌ی ضد‌شهودی مهم درباره‌ی Duration: در پروژه‌های کوتاه، حس فوریت به‌صورت طبیعی در تیم وجود دارد و کنترل زیاد معمار فقط پروژه را به تعویق می‌اندازد؛ اما در پروژه‌های بلندمدت، توسعه‌دهندگان آرام‌تر عمل می‌کنند و نیاز به کنترل بیشتری برای حفظ ریتم کار وجود دارد.

سه نشانه‌ی هشدار برای تیم بیش‌ازحد بزرگ

  • Process Loss (قانون Brooks): هرچه افراد بیشتری به پروژه اضافه شود، زمان پروژه بیشتر می‌شود؛ نشانه‌ی آن Merge Conflictهای مکرر است.
  • Pluralistic Ignorance: وقتی افراد ظاهراً با یک هنجار موافقت می‌کنند اما در خفا آن را رد می‌کنند، چون می‌ترسند چیزی واضح را از دست داده باشند (شبیه داستان «لباس نو پادشاه»).
  • Diffusion of Responsibility: با افزایش اندازه‌ی تیم، ارتباطات ضعیف می‌شود و مسئولیت‌ها گم می‌شوند — دقیقاً مثل رانندگانی که در بزرگراه شلوغ، کنار خودروی خراب‌شده توقف نمی‌کنند چون فکر می‌کنند «حتماً کس دیگری کمک کرده».

قدرت چک‌لیست‌ها: از اتاق عمل تا کد نویسی

با استناد به کتاب The Checklist Manifesto اثر Atul Gawande (که نرخ عفونت بیمارستانی را با چک‌لیست‌های جراحی تا نزدیک صفر کاهش داد)، نویسندگان سه چک‌لیست کلیدی را برای تیم‌های توسعه توصیه می‌کنند:

  • Developer Code Completion Checklist: موارد ساده اما اغلب فراموش‌شده مثل Absorbed Exceptions یا Code Cleanup.
  • Unit and Functional Testing Checklist: موارد Edge-case مثل کاراکترهای خاص، مقادیر حداقل/حداکثر، یا فیلدهای گم‌شده.
  • Software Release Checklist: پرخطرترین و پرنوسان‌ترین چک‌لیست، شامل تغییرات Configuration و اسکریپت‌های Migration پایگاه‌داده.

نکته‌ی مهم درباره‌ی طراحی چک‌لیست: هر موردی که قابل Automation باشد باید از چک‌لیست حذف و به یک تست خودکار تبدیل شود؛ همچنین اگر فرایند یک توالی رویه‌ای با وابستگی بین مراحل دارد (نه یک فهرست مستقل)، آن یک چک‌لیست واقعی نیست. یک ترفند روان‌شناختی جالب برای رعایت چک‌لیست‌ها، Hawthorne Effect است: صرف اعلام اینکه چک‌لیست‌ها گاهی بررسی می‌شوند (حتی اگر واقعاً فقط گاهی چک شوند)، رفتار توسعه‌دهندگان را بهبود می‌دهد.


فصل ۲۳: مهارت‌های مذاکره و رهبری

فصل ۲۳ به یکی از سخت‌ترین مهارت‌های نرم معمار می‌پردازد: مذاکره. دلیل اهمیت این مهارت ساده است — تقریباً هر تصمیم معماری که می‌گیرید، توسط کسی به چالش کشیده خواهد شد؛ چه توسعه‌دهندگان، چه معماران دیگر، و چه ذی‌نفعان کسب‌وکار.

مذاکره با ذی‌نفعان کسب‌وکار

سناریوی کلاسیک کتاب: معاون ارشد شرکت اصرار دارد سیستم باید Five Nines (99.999%) دسترس‌پذیری داشته باشد، در حالی که معمار بر اساس تحقیق خود معتقد است Three Nines (99.9%) کافی است. چند تکنیک کلیدی برای این نوع مذاکره:

  • رمزگشایی از عبارات پرطمطراق: جملاتی مثل «باید صفر Downtime داشته باشیم» یا «همین دیروز به این قابلیت نیاز داشتم» بی‌معنا هستند اما اطلاعات ارزشمندی درباره‌ی نگرانی واقعی ذی‌نفع می‌دهند (اولی یعنی Availability مهم است، دومی یعنی Time to Market).
  • جمع‌آوری اطلاعات کمی پیش از مذاکره: تبدیل «۵ نُه» به عبارت ملموس‌تر مثل «۵ دقیقه و ۳۵ ثانیه Downtime در سال» یا معادل آن یعنی فقط ۱ ثانیه Downtime برنامه‌ریزی‌نشده در روز، مذاکره را از حالت انتزاعی به حالت قابل‌فهم می‌برد.
  • قاعده‌ی تفرقه‌بینداز-و-غالب‌شو (Divide and Conquer): با اقتباس از جمله‌ی سون تزو در هنر جنگ («اگر نیروهایش متحدند، آن‌ها را جدا کن»)، معمار باید بپرسد آیا واقعاً کل سیستم به ۵ نُه نیاز دارد یا فقط بخشی از آن؛ این کار دامنه‌ی مذاکره را کوچک می‌کند.
  • هزینه و زمان را برای آخر نگه دارید: شروع مذاکره با «این خیلی گران است» یا «وقت نداریم» معمولاً مذاکره را از ابتدا خراب می‌کند.

مذاکره با معماران دیگر و توسعه‌دهندگان

وقتی دو معمار درباره‌ی REST در برابر Messaging اختلاف دارند، بهترین تکنیک این است: نمایش عملی، بحث را شکست می‌دهد — به‌جای استدلال کلامی، یک مقایسه‌ی واقعی در محیطی مشابه Production اجرا کنید تا نتیجه خودش صحبت کند. همچنین باید از شخصی‌شدن مذاکره پرهیز کرد؛ رهبری آرام و استدلال شفاف همیشه در بحث پیروز می‌شود.

برای مذاکره با توسعه‌دهندگان، نکته‌ی کلیدی این است که به‌جای فرمان دادن، توجیه ارائه دهید. مثال کتاب نشان می‌دهد چگونه جمله‌ی «باید از طریق Business Layer عبور کنی» با پاسخ منفی مواجه می‌شود، اما جمله‌ی «چون کنترل تغییرات برای ما مهم‌ترین اولویت است، معماری Layer بسته ساختیم» گفت‌وگو را به سمت همکاری («پس چطور مسئله‌ی Performance را حل کنیم؟») می‌برد. تکنیک قدرتمند دیگر، گذاشتن توسعه‌دهنده در مسیر رسیدن به راه‌حل خودش است — اگر او هم شکست بخورد و هم موفق شود، در هر دو حالت معمار برنده است.

چهار حرف C معماری

نویسندگان هشدار می‌دهند که معماران و توسعه‌دهندگان مثل «پروانه به‌سوی شعله» به سمت پیچیدگی کشیده می‌شوند. تمایز مهمی که باید درک شود:

  • Essential Complexity: پیچیدگی ذاتی یک مسئله‌ی سخت واقعی (مثل پشتیبانی از Six Nines).
  • Accidental Complexity: پیچیدگی‌ای که خودِ معمار به یک مسئله ساده تحمیل می‌کند، گاهی برای اثبات ارزش خود یا امنیت شغلی.

راه‌حل پیشنهادی، ۴ حرف C معماری است:

Cمعنا
Communicationارتباط شفاف و مؤثر با تمام ذی‌نفعان
Collaborationهمکاری با توسعه‌دهندگان، ذی‌نفعان کسب‌وکار، و سایر معماران برای شکل‌دهی مشترک راه‌حل
Clarityوضوح در بیان ایده‌ها
Concisenessاختصار و پرهیز از پیچیدگی غیرضروری

Pragmatic اما Visionary

یک معمار باید هم‌زمان Visionary (برنامه‌ریزی برای آینده با تخیل و بصیرت) و Pragmatic (توجه به محدودیت‌های واقعی) باشد. عوامل Pragmatic شامل بودجه، محدودیت زمانی، سطح مهارت تیم، Trade-offهای تصمیم، و محدودیت‌های فنی راه‌حل پیشنهادی است. مثال کتاب: در برابر مسئله‌ی Elasticity، یک معمار Visionary ممکن است فوراً به سراغ یک Data Mesh پیچیده برود، اما معمار Pragmatic ابتدا می‌پرسد: آیا شرکت قبلاً از Data Mesh استفاده کرده؟ آیا واقعاً گلوگاه (Bottleneck) در پایگاه‌داده است، یا می‌توان با Cache آن را حل کرد؟

رهبری با نمونه، نه با عنوان

داستان کلاسیک کاپیتان و سرجوخه در این بخش گفته می‌شود: سربازان به‌جای فرمان کاپیتان بلندپایه، به نگاه تأییدی سرجوخه‌ی نزدیک‌تر به خودشان اعتماد می‌کنند — درس اینجا این است که رتبه و عنوان در رهبری واقعی اهمیت کمی دارد. چند تکنیک عملی رهبری:

  • از نام افراد استفاده کنید و تلفظ صحیح آن را تمرین کنید؛ این کار گفت‌وگو را شخصی‌تر می‌کند.
  • درخواست را به یک لطف تبدیل کنید: به‌جای «باید این کار را انجام دهی»، بگویید «در مضیقه‌ام، می‌توانی کمکم کنی؟» — طبیعت انسان کمک‌کردن را بیشتر از فرمان‌بردن می‌پذیرد.
  • دست دادن مناسب: یک دست‌دادن محکم ولی نه بیش‌ازحد، همراه با نگاه در چشم؛ از آغوش در محیط کاری خودداری کنید.
  • کنار تیم بنشینید: نشستن در یک اتاق جداگانه پیامی می‌فرستد که معمار «خاص» است و نباید مزاحمش شد.

مدیریت جلسات: تشخیص Imposed در برابر Called

نویسندگان تفکیک مهمی بین دو نوع جلسه ارائه می‌دهند: جلساتی که بر معمار تحمیل می‌شوند (دعوت‌شده) و جلساتی که معمار خودش فرا می‌خواند. برای نوع اول، همیشه از برگزارکننده بپرسید «چرا باید در این جلسه باشم؟» و Agenda را از قبل درخواست کنید تا حضور خود را ارزیابی کنید. برای نوع دوم، همیشه از خود بپرسید آیا این جلسه از کاری که تیم را از آن دور می‌کنید مهم‌تر است، و هرگز جلسه را در ساعاتی که توسعه‌دهندگان معمولاً در حالت Flow (تمرکز کامل ذهنی) هستند برنامه‌ریزی نکنید.

فصل ۲۳ با نقل قولی از تئودور روزولت به پایان می‌رسد: «مهم‌ترین عنصر در فرمول موفقیت، دانستن نحوه‌ی کنار آمدن با مردم است». فصل بعدی و پایانی کتاب (۲۴) به توسعه‌ی مسیر شغلی می‌پردازد؛ شامل قانون ۲۰ دقیقه، ساخت رادار شخصی تکنولوژی، و رادار تکنولوژی ThoughtWorks.


فصل ۲۴ (پایانی): توسعه‌ی مسیر شغلی

فصل پایانی کتاب به پرسشی بنیادین می‌پردازد: با وجود اینکه معماری شدن، هم یادگیری مداوم می‌خواهد و هم زمان بسیار محدود، چگونه یک معمار می‌تواند دانش فنی خود را به‌روز نگه دارد؟

قانون ۲۰ دقیقه

ایده‌ی اصلی این تکنیک ساده است: هر روز حداقل ۲۰ دقیقه به یادگیری چیزی جدید یا عمیق‌تر شدن در یک موضوع خاص اختصاص دهید، از منابعی مثل InfoQ، DZone Refcardz یا ThoughtWorks Technology Radar.

نکته‌ی عملی و بسیار مهم این بخش، زمان‌بندی اجرای این قانون است. نویسندگان تجربه کرده‌اند که تلاش برای اجرای این قانون در زمان ناهار یا عصر معمولاً شکست می‌خورد، چون ناهار کوتاه‌تر می‌شود و عصرها با برنامه‌های خانوادگی درگیر می‌شوند. توصیه‌ی قوی کتاب: این قانون را صبح، بلافاصله بعد از نوشیدن قهوه یا چای و پیش از چک‌کردن ایمیل اجرا کنید — چون به‌محض چک‌کردن ایمیل، حواس‌پرتی شروع می‌شود و روز از دست می‌رود.

ساخت رادار تکنولوژی شخصی

نیل فورد، یکی از نویسندگان، داستان تجربه‌ی خودش را با پلتفرم Clipper روایت می‌کند — تکنولوژی‌ای که یک‌شبه ناپدید شد و درسی ماندگار به‌جا گذاشت: هرگز پیشرفت تکنولوژی را نادیده نگیرید، چون حباب‌های تکنولوژیک (Technology Bubbles) از درون هرگز قابل تشخیص نیستند تا زمانی که خیلی دیر شده باشد.

راه‌حل، ساخت یک رادار تکنولوژی شخصی بر اساس مدل ThoughtWorks Technology Radar است که چهار حوزه (Quadrant) و چهار حلقه (Ring) دارد:

حلقهمعنا برای استفاده‌ی شخصی
Holdتکنولوژی‌ها یا عادت‌هایی که باید از آن‌ها دوری کرد (مثلاً دنبال‌کردن Gossipهای بی‌ارزش تیمی)
Assessتکنولوژی‌های امیدبخشی که هنوز فرصت بررسی عمیق آن‌ها را نداشته‌اید
Trialتکنولوژی‌هایی که در حال آزمایش عملی (Spike) روی آن‌ها هستید تا Trade-off آن‌ها را درک کنید
Adoptتکنولوژی‌ها و بهترین شیوه‌هایی که با اطمینان کامل از آن‌ها استفاده می‌کنید

نکته‌ی کلیدی: ارزش این تمرین بیشتر در فرایند فکرکردن و گفت‌وگو است تا در خودِ نمودار نهایی؛ رادار صرفاً بهانه‌ای برای اختصاص زمان به این تفکر است. معماران باید به سبد تکنولوژی خود مثل یک سبد مالی نگاه کنند: تنوع‌بخشی کلید اصلی است.

شبکه‌های اجتماعی و اهمیت روابط ضعیف

با استناد به کتاب Enterprise 2.0 اثر Andrew McAfee، کتاب سه نوع رابطه را معرفی می‌کند: Strong Links (خانواده و همکاران نزدیک)، Weak Links (آشنایان گاه‌به‌گاه)، و Potential Links (افرادی که هنوز ملاقات نکرده‌اید).

مشاهده‌ی جالب McAfee این است که شغل بعدی یک فرد معمولاً از یک Weak Link می‌آید، نه از یک Strong Link؛ چون افراد نزدیک، دقیقاً همان اطلاعاتی را دارند که شما دارید، در حالی که آشنایان دورتر دیدگاه‌های خارج از حباب شما را به همراه می‌آورند. به همین دلیل، معماران باید از شبکه‌های اجتماعی مثل Twitter برای دنبال‌کردن متخصصانی که به نظرشان احترام می‌گذارند استفاده کنند تا حلقه‌ی Assess رادار خودشان را تغذیه کنند.

سخن پایانی کتاب

کتاب با یک پرسش تأمل‌برانگیز از Ted Neward به پایان می‌رسد: «چطور می‌توان معماران بزرگ تربیت کرد، وقتی آن‌ها در طول کل حرفه‌ی خود کمتر از نیم‌دوجین بار فرصت طراحی معماری واقعی پیدا می‌کنند؟» پاسخ نویسندگان، تمرین مستمر از طریق Architecture Katas (تمرین‌های شبیه‌سازی‌شده‌ی طراحی معماری) است.

جمله‌ی پایانی و کلیدی نیل فورد که فلسفه‌ی کل کتاب را خلاصه می‌کند: «در معماری پاسخ درست یا غلطی وجود ندارد — فقط Trade-off وجود دارد.» این جمله دقیقاً همان اصل بنیادینی است که در طول کل کتاب، از انتخاب سبک معماری تا مذاکره با ذی‌نفعان، به‌کار رفته است.