فصل پانزدهم در مورد اینکه معماری چیست و معمار نرمافزار چه کسی است و هدف از طراحی یک معماری خوب چیست، صحبت میکند. دلیل طراحی معماری نرمافزار، آسان کردن موارد توسعه (development)، استقرار (deployment)، عملکرد (operation) و نگهداری (maintenance) نرمافزار است. یک معماری خوب از طریق تقسیم کردن برنامه به چندین component مجزا، عمل توسعه توسط چند تیم را آسانتر میکند؛ از طریق کشیدن نمودارها، بررسی ارتباطات بین componentها و ساخت استراتژی استقرار، مشکلات عمل استقرار را قبل از وقوع برطرف میکند؛ از طریق ایزوله کردن componentها با استفاده از اینترفیسها و تجرید، نگهداری و اضافه کردن ویژگیهای جدید را آسانتر میکند؛ تنها موردی که نیاز کمتری به معماری خوب دارد و بدون آن هم میتوانیم به آن برسیم، عملکرد سیستم است. تقریباً تمام مشکلات عملکردی را میتوانیم با اضافه کردن سختافزار بیشتر، برطرف کنیم. معماری خوب در این مورد میتواند با نمایش بهتر عملکرد سیستم به توسعهدهندگان، به ما کمک کند. نکته دیگر این فصل این است که بهتر است تصمیمگیری در مورد جزئیات و قسمتهای concrete برنامه را تا جایی که میتوانیم عقب بندازیم و به جای آن، روی business ruleها و policyهای سطح بالا تمرکز کنیم؛ این امر، انعطافپذیری در توسعه را بسیار بالا میبرد.
فصل شانزدهم مطالب گفته شده در فصل قبل را تکرار میکند و به آنها اضافه میکند. این فصل میگوید که اولین چیزی که یک معماری خوب باید از آن پشتیبانی کند، use case است. از آنجایی که معماری تاثیر کمتری روی رفتار و عملکرد سیستم دارد، کاری که یک معماری خوب باید در مورد use caseها انجام دهد این است که هدف و قصد از توسعه آن نرمافزار را مشخص کند تا توسعهدهندگان بتوانند بهتر سیستم را درک کرده و آن را توسعه دهند. یک معماری خوب باید از نیازمندیهای عملکردی و مقیاسپذیری سیستم هم پشتیبانی کند؛ یعنی اگر response time پایین یا throughput بالایی نیاز است، معماری به طریقی باید آن را فراهم کند (مثلاً از طریق پخش بخشهای پردازشی روی سرورهای مختلف و اجرای همزمان آنها). معماری خوب همچنین باید از طریق بخشبندی سیستم به componentهای جدا و مستقل، کارها و وظایف تیمهای توسعه را از یکدیگر جدا کند و همچنین با این کار، استقرار (deployment) را هم آسانتر کند. یکی از مواردی که بسیار توصیه میشود، جداسازی componentها در سطوح مختلف مانند سطح کد، سطح استقرار و سطح سرویس است که در صورت تغییر نیازمندیها و use caseها، اعمال تغییرات را آسانتر میکند. این جداسازی از طریق اعمال دو اصل SRP و CCP، قابل انجام است. این که در کدام سطح باید جداسازی را انجام دهیم (به آن decoupling mode میگویند)، یکی از گزینههایی است که باید باز بماند و بعداً با توجه به نیازها، در مورد آن تصمیمگیری شود. این فصل به این نکته هم اشاره دارد که توسعهدهندگان باید ترس از duplication (تکرار کد) را کنار بگذارند و سعی نکنند تکرارهای تصادفی را حذف کنند.
فصل هفدهم در مورد اهمیت جدا کردن بخشهای مختلف نرمافزار و کاهش coupling (وابستگی بخشها به یکدیگر) صحبت میکند. این فصل همچنین بیان میکند که تصمیماتی که در ابتدای پروژه گرفته میشوند، بخصوص آنهایی که ربطی به نیازمندیهای کسبوکار ندارند، میتوانند در ادامه کار باعث ایجاد مشکلاتی در توسعه نرمافزار شوند. این فصل در ابتدا چند کمپانی رو مثال میزند که تصمیمات معمارانه عجولانهای گرفتند و بعداً دچار مشکل شدند و سپس، در مورد FitNesse حرف میزند که پروژه عمو باب با پسرش بود و در آن چون مرزهای بین componentها به خوبی مشخص شده بودند، انعطافپذیری در توسعه بالاتر رفته بود و میتوانستند بسیاری از تصمیمها را به عقب بیاندازند. در کل این فصل پیشنهاد میکند که در معماری نرمافزارمان، با استفاده از اصولی مانند SRP، DIP و SAP، مرزهایی را مشخص کنیم تا وابستگیها حداقل شوند و maintainability راحتتر شود.
فصل هجدهم در مورد مرزهای متفاوتی که در معماری یک سیستم نرمافزاری بکار میروند و ویژگیهای هر یک از این مرزها صحبت میکند. این فصل در انتها هم اشاره میکند که بیشتر سیستمها با توجه نیازمندیهایشان، ممکن است از ترکیبی از این استراتژیها استفاده میکنند. در ابتدا، source-level decoupling mode را داریم که یک مرز ساده است و فضای بحث، درون یک پردازنده و یک فضای آدرس است. در این روش جداسازی منظم تابعها و داده را از یکدیگر داریم. این روش بیشتر در برنامههای یکپارچه و monolithic مطرح است و جهت وابستگیها، از سطوح پایینتر به سمت سطوح بالاتر است. مفهوم بعدی، وابستگی زمان اجرا یا همان runtime dependency است که صدا زدن سرویسهای سطح پایین توسط clientهای سطح بالا است و در آن، وابستگیها در جهت مخالف جریان کنترل از مرز عبور میکنند. مورد بعدی، deployment-level decoupling mode است که همان dllها، jar فایلها، war فایلها و ... هستند که استراتژی مدیریتشان همانند monolithها است (چون تابعهای هردو درون یک پردازنده و فضای آدرس هستند). مرز بعدی، local process است که شامل پردازههایی (همان processها) است که در فضای آدرس جداگانهای اجرا میشوند اما ممکن است componentهایی را به اشتراک بگذارند. ارتباط از طریق socketها یا سیستمعامل برقرار میشود و استراتژی جداسازی بخشها از یکدیگر، مشابه monolithها و binary componentها است. قویترین مرز، مرز سرویس است که در آن سرویسها، پردازههایی هستند که در شبکه دایر و در حال اجرا هستند. در این روش به دلیل بالا بودن تاخیر (چون به صورت فیزیکی از هم جدا هستند و با شبکه به هم وصلند)، ارتباطات کندتر از فراخوانی تابعها است و کد سرویسهای سطح بالا، نباید اطلاعی از سرویسهای سطح پایین داشته باشند.
فصل نوزدهم در مورد این حرف میزند که سیستمهای نرمافزاری در اصل یکسری policy یا سیاستها هستند که ورودی و خروجیها را کنترل میکنند. در یک معماری خوب، این سیاستها با توجه به دلیل و زمان تغییرشان، در componentها مختلف پخش میشوند. معماران نرمافزار این componentها را تبدیل به گراف جهتدار بدون دور (DAG) میکنند که در آنها گرهها componentهایی را که در یک سطح هستند نشان میدهند و یالهای جهتدار، وابستگیهای source code یا زمان کامپایل بین componentها را نشان میدهند. سطح یک component با توجه به میزان فاصلهاش از ورودی و خروجیهای سیستم، مشخص میشود. نکته بعدی این است که جهت وابستگیهای source code ممکن است با جهت جریان داده یکسان نباشد که کار درستی است و هدف، وابسته کردن وابستگیهای source code به سطوح componentها است. مطلب دیگر که بیان میشود این است که تغییرات در سیاستهای سطوح پایینتر، بیشتر و غیرضروریتر است و برای همین بهتر است که اثر آنها روی سیاستهای سطوح بالاتر را کم کنیم و از آنها به عنوان pluginهایی استفاده کنیم که به راحتی و بدون اثرگذاری روی سیاستهای سطوح بالاتر، قابل تغییرند.
فصل بیستم در مورد اهمیت درک و دستهبندی business ruleها یا همان قوانین کسب و کار، صحبت میکند. قوانین کسب و کار، قوانینی هستند که یا پول کسب و کار را زیاد میکنند و یا جلوی از دست رفتن پول آنها را میگیرند. دو نوع کلی دارند: ۱. قوانین کسبو کار حیاتی یا Critical ۲. قوانین کسبوکار Application-Specific یا همان use caseها. قوانین کسبوکار حیاتی، آن دسته از قوانین هستند که چه در سیستم کامپیوتری پیادهسازی شوند یا اینکه به صورت دستی اعمال شوند، باعث سود آن کسبوکار میشوند. این قوانین به دادههای حیاتی کسبوکار (Critical Business Data) برای عملکردشان نیاز دارند و درون Entityها قرار داده میشوند. قوانین کسب و کار Application-Specific، قوانینی هستند که مربوط به استفاده کاربران از سیستم کامپیوتری هستند و محدودیتهایی را روی عملکرد آن سیستم میگذارند. این قوانین که همان use caseها هستند، مشخص میکنند که قوانین کسبوکار حیاتی که درون Entityها هستند چطور فراخوانی شوند و به نوعی آنها را کنترل میکنند. Use caseها وابسته به Entityها هستند (که سطحشان بالاتر است) اما Entityها مستقل هستند و اطلاعی از use caseها ندارند (استفاده مفید از DIP) که باعث میشود بتوانیم از Entityها در جاهای دیگر هم استفاده کنیم اما از use caseها نمیتوانیم؛ Use caseها مخصوص همان برنامهای هستند که براش نوشته شدهاند. دادههای ورودی و خروجی use caseها ساده و از یکدیگر مستقل هستند و referenceای به هیچ Entityای ندارند.
فصل بیست و یکم در مورد اهمیت ساخت برنامهها به طوری که با use caseها مطابقت داشته باشند، صحبت میکند. کتاب در ابتدا مثالی از نقشههای ساختمانها میزند و میگوید همانطور که نقشه یک کتابخانه، مشخص میکند که محصول نهایی یک کتابخانه است، معماری یک سیستم نرمافزاری هم باید اهداف و use caseهای آن سیستم نرمافزاری را مشخص کند و مبتنی بر use caseهای آن باشد. این فصل همچنین در مورد این حرف میزند که frameworkها، ابزار هستند و نباید به آنها بچسبیم و بگذاریم معماری ما را تعریف کنند. یک معماری خوب، frameworkها و ابزار توسعه را از use caseها جدا میکند و تصمیم در مورد آنها را عقب میاندازد. مثلا اگر برنامه در حوزه وب است، معماری نباید از آن خبر داشته باشد و اهمیتی به آن یا تغییر آن بدهد. این قضیه انجام unit test روی use caseها را نیز آسانتر میکند (مستقل از frameworkها و ابزار). در کل، باید use caseها را در اولویت قرار داد و در مورد frameworkها و ابزار، انعطافپذیر بود.
فصل بیست و دوم در مورد ویژگی مشترک بین یکسری معماری معروف صحبت میکند؛ مانند معماریHexagonal ، DCI و BCE که با وجود تفاوتها، بخشهای مختلف را از یکدیگر جدا کردهاند و لایههایی جداگانه مخصوص قوانین کسبوکار و رابطهای کاربری و سیستمی (User interface and system interface) دارند. این فصل علاوه بر اینکه مجدد به این مطلب اشاره دارد که باید به frameworkها به عنوان یک ابزار نگاه کرد و از آنها مستقل بود، در مورد استقلال از پایگاه داده و رابط کاربری هم حرف میزند و از مزایای آن مانند تستپذیری مستقل و آسانتر قوانین کسبوکار، صحبت میکند. در کل قوانین کسبوکار باید از رابطهای خارجی (مانند UI) مستقل و از نحوه ارائه خود به بیرون، بیاطلاع باشند. علاوه بر مفاهیم فصلهای قبل (entity و use case)، در مورد interface adaptersها هم صحبت میکند که تبدیل فرمتهای دادهای را بین use caseها و بخشهای خارجی مانند پایگاه داده، انجام میدهد. این فصل همچنین روی استفاده از dynamic polymorphism و ساختار دادههای ساده برای ارتباط بین مرز لایههای مختلف تاکید دارد. در کل این فصل بر استقلال بخشهای مختلف سیستم از یکدیگر، برای بالابردن انعطافپذیری و تستپذیری سیستم، تاکید دارد.
فصل بیست و سوم در مورد الگوی Humble Object حرف میزند که به جداسازی رفتارهایی که تستشان سخت است از آنهایی که تستشان آسان است، کمک میکند و تستپذیری را بالا میبرد. این کار از طریق تقسیم رفتارهای سیستم به دو ماژول یا کلاس انجام میشود؛ رفتارهایی که تستشان سخت است با یک فرم سادهای درون ماژول humble قرار میگیرند و باقی رفتارهای قابل تست در ماژول دیگر قرار میگیرند. این الگو با جداسازی بخشهای قابل تست و غیرقابل تست برنامه از یکدیگر، درتعیین مرزهای معماری به ما کمک میکند. یکی از مواردی که در آن از الگوی Humble Object استفاده شده است، تقسیم GUI به دو کلاس view و presenter است که در آن، view همان humble و presenter بخش قابل تست است. View وظیفه رندر کردن المانها و نمایش بخش گرافیکی برنامه را برعهده دارد و presenter دادهها را از برنامه میگیرد و با فرمت مناسب، به view تحویل میدهد تا نمایش دهد. این فصل در این مورد نیز حرف میزند که به جای واژه ORM، باید از واژه data mapper استفاده کنیم که داده را از جداول دیتابیسهای رابطهای میگیرد و درون ساختمان دادههای ساده قرار میدهد و اینکه متعلق به لایه پایگاه داده است که با استفاده از الگوی Humble Object، مرزی بین آن و gateway interfaceها که اعمالی که در لایه پایگاهداده پیادهسازی شدهاند، از طریق آنها فراخوانی میشوند، ایجاد شده است. اصل Humble Object همچنین میتواند بین سرویسهای یک سیستم مرز ایجاد کند و در کل، اصلی کارآمد است که تستپذیری سیستم را بالا میبرد و در تعیین مرزهای سیستم، نقش مؤثری دارد.
فصل بیست و چهارم در مورد مرزهای معماری در طراحی نرمافزار و رویکردهای مدیریت آنها حرف میزند. مرزهای معماری برای جداسازی componentهای مختلف سیستم مورد استفاده قرار میگیرد اما تنظیم اولیه و نگهداری آنها، هزینه بر و پیچیده است. این مرزها از طریق ساخت اینترفیسها، ساختماندادههای ورودی و خروجی و مدیریت وابستگیها انجام میشود. طراحی مقدماتی یعنی اینکه مرزهای معماری را قبل از آنکه به آنها نیاز پیدا کنیم، بسازیم که بعضی افراد که از اصل YAGNI یا همان You Aren’t Going to Need It طرفداری میکنند، با این موضوع مخالف هستند. یکی از راههای کاهش هزینههای ایجاد مرز، این است که مرزهای کامل را مشخص نکنیم و از مرزهای partial استفاده کنیم که همان طراحی مقدماتی مرزهای کامل را دارند، اما کامپایل و تبدیل به یک unit میشوند که مشکلات مدیریت نسخه را کاهش میدهد. استفاده از الگوی facade هم میتواند به تعیین مرزها کمک کند. در کل، مرزهای معماری برای مدیریت پیچیدگی نرمافزار ضروری هستند اما پیادهسازی آنها میتواند فرق کند و از پیادهسازی کامل داشته باشیم تا یک پیادهسازی ابتدایی که قرار است جایگزین شود.
فصل بیست و پنجم میگوید که سیستمهای نرمافزاری، معمولاً سه component اساسی دارند: UI، قوانین کسبوکار و پایگاه داده. این فصل مثالی از یک بازی میزند و در ابتدا مشخص میکند چطور میتوان از طریق جداسازی UI و زبان، بازی را به زبانهای دیگر هم ترجمه کرد. در ادامه در مورد فواید جداسازی قوانین بازی از فضای ذخیرهسازی بازیها صحبت میکند. در ادامه میگوید که مرزهای معماری فقط نباید UI، game ruleها و data storageها باشند و ترجمههای زبان، مکانیزمهای ارتباطی و ... هم میتوانند مرزهای معماری باشند. هدف این فصل این است که بگوید مرزهای معماری در تمام جاهای سیستم پخش هستند و وظیفه معمار نرمافزار این است که زمانی که به آنها نیاز شد، آنها را شناسایی، پیادهسازی و مدیریت کند.
فصل بیست و ششم در مورد Main component حرف میزند که کثیفترین component سیستم است و به این موضوع اشاره دارد که این component، پایینترین سطح را دارد، ورودی سیستم از آنجا است و جز سیستم عامل، هیچ component دیگری به آن وابسته نیست. این component وظیفه ساخت و نظارت بر تمام componentهای دیگر سیستم را دارد. این فصل میگوید که Main جایی است که وابستگیها باید توسط Dependency Injection framework تزریق شوند. وقتی این تزریق انجام شود، Main این وابستگیها را بدون نیاز به framework، در سیستم پخش میکند. Main بیرونیترین دایره معماری تمیز است و شرایط و تنظیمات اولیه را برای سیستم سطح بالا آماده میکند و کنترل را به آن میدهد. متن همچنین به نکته اشاره میکند که داشتن چندین Main plugin که هرکدام تنظیمات خاصی دارند و برای محیطهای خاصی نوشته شدهاند (محیط Dev، Test یا Production)، مشکل پیکربندی و تنظیم سیستم را حل میکند.
فصل بیست و هفتم در مورد محبوبیت معماریهای مبتنی بر سرویس و micro-service صحبت میکند. دلیل این محبوبیت، جدا بودن سرویسها از یکدیگر و همچنین جدا بودن فرایندهای توسعه و استقرار در این معماری و امکان تخصیص این وظایف به تیمهای مختلف است. برخلاف تفکر عام، سرویسها معماری را تعریف نمیکنند و معماری توسط مرزهایی که سیاستهای سطح بالا را از جزئیات سطح پایین جدا میکنند و از dependency rule پیروی میکنند، تعریف میشود. کتاب همچنین بیان میکند که نیاز نیست که تمام سرویسها از نظر معماری قابل توجه و خاص باشند و امکان دارد بعضی از سرویسها، از این نظر خاص نباشند و فقط برای جدا کردن رفتار برنامه بکار بروند. نکته بعدی این است که با اینکه سرویسها ممکن است جدا به نظر بیایند، اما ممکن است بخاطر منابع مشترک درون یک پردازنده یا در یک شبکه، به هم وابسته باشند. در این معماری، اضافه کردن ویژگی جدید که چند سرویس را در بر بگیرد، میتواند اذیتکننده باشد. یکی از راه حلهای بیان شده در این فصل، استفاده از کلاسها abstract و طراحی مبتنی بر component است که از OCP هم پیروی میکند. برای حل مشکل دربر گرفتن چند سرویس، باید درون آنها componentهایی در نظر گرفته شود که از dependency rule پیروی میکنند و سپس با استفاده از مرزهای معماری، این componentها باید جدا شوند. در کل، سرویسها معماری سیستم را مشخص نمیکنند و معماری سیستم با مرزهای مشخص شده و وابستگیهایی که از آن مرزها عبور میکنند، مشخص میشود.
فصل بیست و هشتم در مورد این میگوید که باید انواع مختلف تست (مانند unit test، integration test و ...) را عضوی جدایی ناپذیر از معماری بدانیم و با آنها مثل یکی از componentهای اساسی سیستم (و معادل هم)، رفتار کنیم. تستها، بیرونیترین دایره در معماری سیستم هستند و از Rule Dependency پیروی میکنند. تستها به طور مستقل قابل استقرار هستند و معمولا در سیستمهای تست مستقر (deploy) میشوند. تستها componentهای ایزولهای هستند که بیشتر از عمل توسعه پشتیبانی میکنند تا از عملکرد سیستم. یکی از چالشها، fragile testها هستند که وقتی اتفاق میافتند که تستها بسیار به سیستم متصل و وابسته باشند که باعث میشوند یک تغییر کوچک در componentهای سیستم، باعث ناکارآمد شدن تستها شود. برای رفع این مشکل باید وابستگی به عناصری که زیاد تغییر میکنند مانند GUI را کم کرده و تست را بیشتر روی business ruleها انجام دهیم که تغییرات کمتری دارند. در کل بهتر است که تستها از ساختار برنامه جدا شوند و از testing apiها برای کاهش هزینه و مشکلات درگیری با محدودیتهای امنیتی استفاده شود.
فصل بیست و نهم در مورد اهمیت فرق گذاشتن بین نرمافزار و firmware در سیستمهای embedded صحبت میکند. خیلی اوقات به اشتباه از firmware در جاهایی استفاده میشود که باید از نرمافزار استفاده میشد که باعث ایجاد وابستگیهایی روی سختافزار و سخت شدن تغییرات میشود. کتاب روی معماریهای embedded تمیزتر با استفاده از hardware abstraction layerها (HAL) و operating system abstraction layerها (OSAL) برای جداسازی نرمافزار از وابستگیهای سختافزاری، تاکید دارد که باعث نگهداری (maintainability) و تستپذیری بهتر نرمافزار میشود. عمو باب روی معماری تمیز سیستمهای embedded تاکید دارد.