بخش پنجم: معماری
از دید نویسنده کتاب معماری تمیز، معماری شامل تفکر درباره تصمیمات مهم و مهارتهای فنی است. همچنین معمار نرمافزار باید اول برنامه نویس خوبی باشد تا بتواند مشکلات اعضای تیم فنی را بهتر درک کند و تیم را به سمت بهرهوری بالاتر هدایت کند.
معماری نرمافزار شکلی است که سازنده یا معمار به آن میدهد و نرمافزار را به مولفهها و نحوه ارتباط آنها تبدیل میکند. هدف این شکل سادهسازی مراحل توسعه، استقرار، عملیات و نگهداری نرمافزار است. این کار را با باز گذاشتن تصمیمات تا جای ممکن (leave as many options open as possible for as long as possible) انجام میدهد. معماری خوب علاوه بر این مورد، هزینه را کاهش داده و نرخ بهرهوری برنامهنویس را بیشینه میکند.
در ادامه این نوشته به بررسی چهار مرحله ذکر شده از دید معماری میپردازیم. یکی از مراحل، توسعه است. ساختار تیمی متفاوت منجر به تصمیمات متفاوت میشود. در این صورت از معماری component-per-team میتوان استفاده کرد که از نظر استقرار، عملیات و نگهداری اصلا مناسب نیست. این معماری در تیمهای کوچک منجر به داشتن معماری بد میشود و در تیمهای بزرگ برای هر تیم در ساختار شرکت مثلا یک مولفه در نظر گرفته میشود.
مرحله دیگر استقرار است. هدف معماری در این راستا آسان سازی استقرار سیستم با یک حرکت است. مثلا در معماری میکروسرویس توسعه راحت ولی استقرار پیچیده و مشکل است. مرحله دیگر عملیاتی شدن سیستم است. در این مرحله وظیفه معماری آشکار کردن عملیات است. یعنی باید ویژگیها، رفتار و ... به موجودیتهای first-classقابل فهم برای توسعهدهنده تبدیل شوند.
آخرین مرحله نگهداری است. این مرحله از سایر مراحل پر هزینهتر است. این هزینه شامل spelunking (بررسی نرمافزارهای موجود برای پیدا کردن بهترین مکان و استراتژی اضافه کردن یک ویژگی جدید یا رفع نقص) و ریسک (ایجاد defectهای ناخواسته ناشی از تغییرات) است. در اینجا معماری خوب باید باعث کاهش هزینهها شود. راهحل شامل تبدیل نرمافزار به مولفههای مجزا و ایزوله کردن آنها با اینترفیسهای پایدار است.
در فصلهای قبلی کتاب به این مسئله اشاره شد که نرمافزار دارای دو ارزش رفتار و ساختار است و از دید نویسنده کتاب ساختار از اهمیت بیشتری برخوردار است و منجر به نرم و انعطاف پذیر بودن نرمافزار میشود. راهکاری که برای نرمتر بودن نرمافزار ارائه شده، باز گذاشتن گزینهها تا جای ممکن است.
اکنون باید بررسی کنیم چه گزینههایی را باید باز بگذاریم؟ نرمافزار شامل policy و جزئیات است. طبق تعریف کتاب، policy همان قوانین کسبوکار و procedureهاست که همان هسته اصلی سیستم است. در مقابل جزئیات قرار دارند که تاثیری روی policy ندارند. طبق این تعریف، هدف معماری ایجاد ساختاری برای نرمافزار است که در آن تمرکز روی policyهاست و تصمیم درباره جزئیات تا جای ممکن به تعویق میافتد. هر قدر این تصمیمات دیرتر گرفته بشوند، اطلاعات بیشتری کسب کردیم و مناسبتر خواهند بود.
همانطور که قبلا اشاره شد معماری خوب باید از عملیات و موارد کاربرد، نگهداری، توسعه و استقرار نرمافزار پشتیبانی کند. اولین دغدغه معمار و اولین اولویت معماری پشتیبانی از موارد کاربری است. در واقع باید رفتار تبدیل به المانها first-class در بالاترین سطح سیستم تبدیل شود تا هدف سیستم در سطح معماری مشخص شود. برای توسعه نیز طبق اصل Conway ساختار طراحی نرمافزار یک کپی از ساختار ارتباطات آن است. برای انجام این کار در بهترین حالت، سیستم را باید به تعدادی مولفه کاملا مستقل و مجزا تقسیم کرد. در مرحله استقرار نیز هدف این است که در لحظه بتوان نرمافزار را مستقر کرد و معماری خوب به دهها فایل تنظیمات برای این کار متکی نیست. راهحل در اینجا ایزوله کردن و تقسیمبندی است.
معماری خوب باید بتواند تمام این دغدغهها را متوازن کند. اما این کار به دلیل تغییر دغدغهها و ناپایدار بودن اهداف بسیار سخت است. برای همین باید از «باز گذاشتن تصمیمات» استفاده کرد تا بتوان به راحتی سیستم را تغییر داد.
طبق اصل SRP در SOLID و اصل CCP، معمار باید لایهها را از هم جدا کند. بنابراین سیستم به تعدادی لایه افقی تقسیم میشود که مثلا شامل UI، قوانین کسبوکار و پایگاه داده است. هر مورد کاربری نیز سیستم را به تعدادی لایه عمودی تقسیم میکند. بنابراین هر مورد کاربری شامل تمام لایههای افقی سیستم نیز خواهد بود.
حالتهای جداسازی مختلفی برای این کار وجود دارد که خود آن یک گزینه است. برای مثال میتوان این جداسازی را در سطح سرویس یا واحدهای اجرایی انجام داد که به مولفههای آن سرویس یا میکروسرویس میگویند. معماری متناظر با آن نیز معماری سرویسگرا است. حالت دیگر جداسازی در سطح استقرار است و سیستم به واحدهایی تقسیم میشود که قابلیت استقرار مجزا از هم را دارند. حالت سوم، جداسازی در سطح کد منبع است که منجر به ساختار یکپارچه (monolithic) میشود. در مراحل اولیه توسعه مشخص نیست که کدام حالت بهتر است. ممکن است در ابتدا یکی از این سه حالت را انتخاب کنیم ولی در ادامه که سیستم تکامل یابد حالت بهینه تغییر کند. معماری خوب در ابتدا مونولیتیک است وقتی که رشد کند واحدهایی که مستقل از هم استقرار یابند ایجاد میشوند و با تکامل بیشتر سرویسها از هم مجزا میشوند و به سمت سرویسگرایی پیش میرود. پس بنابراین باید این تصمیم را تا جای ممکن باز گذاشت.
در ادامه کتاب درباره مرزهای معماری صحبت شده است. هدف معماری کاهش نیروی انسانی برای ساخت و نگهداری سیستم است. آنچه باعث کاهش قدرت افراد تیم میشود، تصمیمات نابههنگامی است که میتوانست به تعویق بیفتد. این تصمیمات شامل مواردی هستند که تاثیری روی موارد کاربری یا نیازمندیهای کسبوکار ندارند. در کتاب دو مثال از این نوع تصمیمات آورده شده است. در این دو مثال تاثیر مخرب تصمیمی که باید به تعویق میافتاد ولی زود گرفته شده بود نشان داده شده است. در ادامه نیز نویسنده یک مثال از پروژه موفق Fitness زده است. در این مثالها انگار خط مرزی بین قوانین کسبوکار و جزئیات (مثل پایگاه داده) رسم شده است. کشیدن این مرز منجر به تاخیر انداختن برخی تصمیمات شده و از بروز برخی مشکلات جلوگیری میکند. خط مرزی بین موارد با اهمیت و آنهایی است که مهم نیستند.
در مثالی از کتاب نشان میدهد چگونه میتوان گفت که پایگاه داده جزو تصمیماتی است که باید به تعویق بیفتد. در واقع پایگاه داده چیزی است که قوانین کسبوکار میتوانند غیر مستقیم از آن استفاده کنند و برای کار با آن به یک واسط نیاز دارند. طبق شکل زیر و جهت وابستگی بین دو مولفه، پایگاه داده میتواند هز چیزی باشد و برای قوانین کسبوکار مهم نیست.
درباره ورودی و خروجیها نیز همین مسئله برقرار است. مثلا GUI باید به قوانین کسبوکار وابستگی داشته باشد و در نتیجه مرز جداسازی مشابه حالت فوق خواهد بود.
بخش بعدی از کتاب معماری تمیز، تحت عنوان معماری افزونهای (Plug-in Architecture) است. در تاریخچه توسعه نرمافزار ایجاد پلاگین برای معماری مقیاسپذیر و قابل نگهداری مطرح شده است. این معماری متشکل از یک هسته (core) و تعدادی پلاگین است. هسته این معماری، قوانین کسبوکار است و پلاگین گزینههای اختیاری یا مواردی هستند که چند انتخاب برای آنها وجود دارد. با فرض داشتن این نوع معماری اعمال تغییرات نیز عملی میشود.
استدلال وجود پلاگین نیز مشابه با پایگاه داده است. در اینجا قصد داریم برخی ازماژولها و مولفهها مثل قوانین کسبوکار از اغییرات بقیه در امان باشند. معماری افزونهای این امکان را فراهم میکند تا با ایجاد فایروال ار انتشار تغییرات به سایر بخشها جلوگیری کنیم. در واقع همان مرزی که در تصویر گرافیکی رسم کردیم، محوری برای تغییر است. مولفههایی که در یک طرف مرز قرار دارند به دلایل مختلفی میتوانند تغییر کنند. بنابراین میتوان گفت که همان اصل SRPدر اصول طراحی SOLID استفاده شده است و این اصل مشخص میکند مرز را در کجا رسم کنیم. به طور کلی در کشیدن مرز پس از شناسایی مولفهها باید حواسمان باشد که تمام پیکانهایی که در مرز قرار دارند در یک جهت و به سمت هسته باشند. این بخش نیز ناشی از اصل DIP در اصول طراحی SOLID است که جهت پیکانها از مولفههای سطح پایین به سمت سطح بالا باید باشد.
همانطور که اشاره شد، معماری مولفهها و خطوط جداکننده آنها را مشخص میکند. تعریف تقاطع خط مرزی (boundary crossing) در کتاب به صورت زیر است:
«یک تابع در طرفی از مرز که تابع دیگری را در آن ظرف مرز با پاس دادن دادهها فراخوانی میکند.»
برای اینکه بتوانیم تقاطع خط مرزی را به صورت مناسب ایجاد کنیم لازم است وابستگیها در سطح کد را مدیریت کنیم. از آنجایی که تغییر کوچکی در کد منجر به تغییر کد وابسته به آن شده و در نتیجه نیاز به کامپایل و استقرار مجدد هر دو مولفه دارد، باید مرزها را طوری تغیین کنیم که از تغییرات این چنینی محافظت کنند. در ادامه به بررسی ساختار برخی از مرزهای معماری پرداخته شده است.
سادهترین مرزها نمایش فیزیکی ندارند و معمولا به صورت یک قانون و نظم در جداسازی هستند. در واقع این مرزها ناشی از حالت جداسازی در سطح sourceهستند و منجر به ساختار مونولیتیک میشوند. این معماری به نوعی چندریختی (polymorphism) پویا جهت مدیریت وابستگیهای داخلی متکی است. سادهترین تقاطع مرزی نیز فراخوانی یک تابع از کاربر به سرویسهای نرمافزاراست. پس بنابراین ارتباطات از نوع فراخوانی تابع است که بسیار سریع و ارزان هستند. استقرار نیز شامل کامپایل و لینک کردن استاتیک است.
نوع دیگر مرزها با توجه به مولفههای استقرار است. سادهترین راه نمایش مرزها استفاده از کتابخانههای لینک پویا است. در واقع این مرز ناشی از حالت جداسازی در سطح استقرار است. در اینجا نیز مشابه با حالت مونولیتیک ارتباطات از طریق فراخوانی توابع است.
نوع دیگر مرزها که قویتر از بقیه است، فرایندهای محلی است. فرایند محلی شامل کدها یا مجوعهای از مولفههاست. ایجاد این فرایندها از طریق خط فرمان (command line) یا فراخوانیهای سیستمی است. ارتباط اینها از طریق سوکت یا هر راه ارتباطی دیگری در سیستم عامل است و در نتیجه اتباطات نسبت به مرزهای دیگر هزینهبر هستند.
قویترین مرز معماری، سرویس است که هر سرویس مجموعهای از فرایندهای محلی است. این نوع مرز به مکان فیزیکی وابستگی ندارد و فرض این است که در همه سرویسهای در شبکه قرار دارند. این فرض منجر میشود که در مقایسه با فراخوانی توابع بسیار کندتر باشد و تاخیر خیلی زیادی داشته باشد.
نرمافزار توضیحی از policyهاست. بخشی از توسعه معماری مربوط به جداسازی این policyها از یکدیگر و قرار دادن آنها در گروهها بر مبنای علت تغییراتشان است. نتیجه این جداسازی نیز یک گراف بدون حلقه و جهتدار (DAG) است. معماری خوب است که جهت وابستگیها در آن براساس سطح مولفهها و از پایین به بالا باشد.
سطح هر مولفه «فاصله میان ورودی و خروجی» آن است. هر قدر یک policy از ورودی و خروجی دورتر باشد سطح آن بالاتر است. این policyها با نرخ کمتری تغییر میکنند و دلایل تغییرشان مهمتر است. از طرفی دیگر policyکه مسئول مدیریت ورودی و خروجی است پایینترین سطح را دارد که با نرخ بیشتری تغییر میکنند و دلایل تغییرشان فوریت بیشتر و اهمیت کمتری دارد. این policyهای سطح پایین به صورت پلاگین با policyهای سطح بالا در ارتباط هستند.
همانطور که قبلتر اشاره شد معماری افزونهای شامل دو بخش هسته (قوانین کسبوکار) و پلاگینهاست. در ادامه توضیحاتی درباره قوانین کسبوکار داده شده است. این قوانین چه به صورت سیستمی و یا به صورت دستی اجرا شوند، منجر به تولید سرمایه یا ذخیره آن میشوند. به همین دلیل به آنها قوانین حیاتی کسبوکار (Critical Business Rules) گفته میشود. این قوانین در اجرا نیاز به دادههایی دارند که به آنها دادههای حیاتی کسبوکار گفته میشود. این دادهها و قوانین به هم متصلاند و بهترین کاندیدایی هستند که شی موجودیت (Entity) را ایجاد میکنند. این موجودیت کاملا مربوط به کسبوکار است و جزئیات دیگری ندارند. واسط این موجودیت نیز شامل توابعی است که قوانین کسبوکار اجرایی روی دادهها را پیادهسازی کردند.
مورد کاربری روشی است که یک سیستم خودکار استفاده میشود. در واقع مورد کاربری قوانین کسبوکار application-specific را مشخص میکنند. پس میتوان گفت که کورد کاربری به نوعی موجودیتها را کنترل میکند. مثلا مورد کاربری تعیین زمان و چگونگی فراخوانی قوانین، روش انتقال داده و در کل تعامل بین کاربر و موجودیت را بیان میکند. با توجه به این توضیحات، مورد کاربری یک شی است که موجودیت از آن اطلاعاتی ندارد. بنابراین طبق اصل DIPاز اصول طراحی SOLID، موجودیت سطح بالا و مورد کاربری سطح پایین است.
از آنجایی که قوانین کسبوکار سبب ایجاد نرمافزار میشوند، بنابراین باید از دغدغههای سطح پایین مانند UI در امان باشند. پس میتوان گفت قوانین کسبوکار باید دارای بیشترین قابلیت استفاده مجدد و مستقلترین بخش کد باشند.
فردی به نام Jacobson معماری را به صورت زیر تعریف میکند:
«معماری ساختارهایی است که از موارد کاربری نرمافزار پشتیبانی میکند»
بنابراین معماری درباره چارچوبها و ابزارها نیست و معماری خوب تصمیمات مربوط به اینها را به عویق میاندازد. پس تاکید روی جداسازی موارد کاربری از دغدغههای جانبی است. به عنوان مثال وب یک مکانیزم ارسال است و معماری نیز همین دید را نسبت به وب دارد. پس جزو جزئیات سیستم است و تاثیری روی ساختار ندارد بنابراین میتواند به تعویق بیفتد. چارچوبها نیز همینطور هستند زیرا معماری باید درباره سیستم باشد و نه ابزارها و چارچوبهایی است که استفاده میشود. علاوه بر این موارد معماری باید قابلیت تست نیز داشته باشد و باید بتوانیم موارد کاربری را تست کنیم.
یکی از ایدههای مطرح شده برای معماری، جداسازی است که نرمافزار را به تعدادی لایه تقسیمبندی میکند. سیستمهایی که با این معماری ساخته شوند، مشخصههای زیر را دارند:
1- فریمورکها به عنوان یک ابزار هستند و نه محدودیت.
2- تمامی بخشها باید قابلیت تست داشته باشند
3- واسط کاربری (UI) به صورت مستقل و بدون تغییر در قوانین کسبوکار باید بتواند تغییر کند.
4- مشابه مورد قبل برای پایگاه داده نیز برقرار است و قوانین کسبوکار ارتباطی با پایگاه داده ندارند.
5- همچنین قوانین کسبوکار چیزی از دنیای بیرون نمیدانند
در این شکل ساختار معماری تمیز نشان داده شده است که در مرکز policyها و در اطراف مکانیزمها قرار دارند. طبق این ساختار و قانون وابستگی، دایرهها داخلی از بیرونیها اطلاعی ندارند. در ادامه به توضیح مختصری از اجزا این ساختار پرداخته شده است:
- موجودیتها: موجودیت یک مجموعه از داده و متدهاست. در واقع در این لایه باید قوانین سطح بالای کسبوکار را قرار بدهیم که با تغییرات خارجی کمتر احتمال عوض شدن آنها وجود داشته باشد.
- موارد کاربری: این موارد شامل قوانین مختص به آن برنامه (app-specific) و پیاده سازی آنها است. در صورت تغییر کارکرد سیستم، باید موارد کاربری و در نتیجه این لایه تغییر کند و کدهای مرتبط با آن نیز بهروز شوند.
- واسط adapter: این لایه تبدیل دادهها را از فرمت مناسب برای موارد کاربری به فرمت مناسب برای فعالیتها خارجی مثل پایگاه داده یا وب تبدیل میکند.
- فریمورک و درایور: این لایه، خارجی ترین لایه و شامل ابزارهایی مانند پایگاه داده است. تمام جزئیات در اینجا قرار دارد، به خصوص مواردی که ممکن است خطرناک باشند.
آیا همین چهار لایه برای معماری کافی است؟ خیر، میتوانیم در صورت لزوم با توجه به دو نکته لایه اضافه کنیم: 1- وابستگیها از خارج به داخل هستند؛ و 2- هر قدر به مرکز نزدیک شویم، جزئیات کمتر میشود و سطح تجرید افزایش مییابد.
در ادامه همین فصل به توضیح مرزهای متقاطع (crossing boundary) پرداخته شده است. در شکل 2، در مرکز موارد کاربری قرار گرفته است و تمام وابستگیها به سمت داخل است. اما جریان کنترل متفاوت است. برای حل این مسئله از اصل DIPاستفاده میشود. برای مثال اگر موارد کاربری به فراخوانی presenter نیاز داشته باشند، یک اینترفیس در لایه موارد کاربری ایجاد میکنیم تا از طریق آن با لایه بیرونی ارتباط برقرار شود.
دادههایی که از این نوع مرز عبور میکنند دادههای ساده و ایزوله هستند و حتما فرمت آنها به حالتی که برای مقصد قابل فهم باشد خواهد بود.
در فصل بعدی دربارهpresenter و شی humble صحبت شده است. الگوی شی humble یک الگوی طراحی و راهی برای unit testerهاست تا رفتاری را که به سختی از سایر بخشها جدا میشود تست کنند. در این الگو رفتار را به دو ماژ<ل یا کلاس تقسیم میکنند:
1- بخش humble که بخشی است که به سختی تست میشود. برای مثال view (تست واحد برای GUIسخت است)
2- رفتار قابل تست یا presenter
از آنجایی که تست پذیری به عنوان یک ویژگی کیفی برای معماری در نظر گرفته میشود، استفاده از الگوی شی humble میتواند یک مرز منایب برای معماری باشد. یکی از این مرزها Presenter/View است.
برای مثال بین پایگاه داده و interactorهای موارد کاربری مرزی به نام gateway پیاگاه داده قرار گرفته است که شامل اینترفیسهای مختلف برای CRUDاست. این gateway که توسط لایه پایگاه داده پیادهسازی شده است در واقع یک شی humbleاست. مثالهای دیگر در کتاب نیز درباره Data mapper و service listenerاست.
پس به طور کلی میتوان گفت که در اغلب مرزهای معماری از الگوی شی humble استفاده شده است که منجر به افزایش testability میشود.
در ادامه به مرزهای جزئی پرداخته شده است. اگر بخواهیم از مرزهای کامل معماری استفاده کنیم، بسیار هزینهبر است و تلاش زیادی به خصوص در نگهداری نیاز دارد. اما معمار خوب باید به گونهای طراحی کند که در آینده اگر نیاز به چنین مرزهایی بود، بتوان به راحتی آنها را اضافه کرد. در جامعه Agile این تفکر نقضی برای YAGNI است که “You aren’t going to need it”. از آنجایی که ممکن است معماران به این مرزها نیاز نداشته باسند، مفهوم مرزهای جزئی مطرح شد. در ادامه به توضیح سه نوع مرز جزئی پرداخته شده است:
مرز جرئی اول: skip the last step - راه ایجاد این نوع مرزها، انجام تمام کارهای لازم برای ساخت مولفههای مستقل از هم از نظر استقرار و کامپایل است که در نهایت به سادگی در یک مولفه قرار میگیرند. انگار که یک فایل کامپایل و مستقر شده است.
مرز جزئی دوم: مرزهای یک بعدی - مرزهای تکامل یافته، برای جداسازی در دو جهت از اینترفیسهای مرزی استفاده میکنند که بسیار پر هزینه است. راه سادهتر برای این کار استفاده از الگوی Strategy است.
مرز جزئی سوم: استفاده از الگوی Facade - در این نوع مرز، به جای وابستگی مستقیم کلاینت به متدها و سرویسها، از یک واسط استفاده میشود و کلاینت به شکل غیر مستقیم به سرویس وابستگی دارد. در نتیجه در صورت بروز هر تغییری در یک سرویس، مجددا باید توسط کلاینت کامپایل شود.
اغلب سیستمهای ساده از سه مولفه UI، قوانین کسبوکار و پایگاه داده تشکیل شدند. اما سیستمهای پیچیدهتر مولفههای بیشتری نیاز دارند. برای مثال اگر یک بازی ساده را نظر بگیریم، UI پیامها را از بازکنان به بخش قوانین کسبوکار منتقل میکند و قوانین هم اطلاعات را در پایگاه داده ذخیره میکنند. اما مولفههای دیگری هم ممکن است وجود داشته باشد.
در کتاب مثالی از یک بازی به نام Hunt the Wumpus آورده شده است. در این مثال اگر بخواهند که بازی مستقل از زبان باشد، باید بخش متنی UI از قوانین کسبوکار جدا شود. اگر وابستگی کد به درستی مدیریت شود، میتوان هر تعداد کامپوننت UI را مجزا از قوانین ایجاد کرد. مشابه با جداسازی این مولفه، میتوان انواع ذخیره سازی دادهها را هم از قوانین جدا کرد.
در این صورت برنامه دارای دو جریان داده میشود. یکی از این جریانها داده را از کاربر گرفته و سپس در بخش زبان، با استفاده از یک APIمستقل از زمان از قوانین کسبوکار استفاده میکند و جریان دیگر هم اطلاعات را ذخیره میکند. باز هم ممکن است جریانهای داده دیگری وجود داشته باشد. به صورت کلی هر قدر که سیستم پیچیدهتر باشد، ساختار ممکن است به تعداد بیشتری از این جریانهای تقسیم شود.
حتی گاهی ممکن است خود قوانین کسبوکار را به مولفههای دیگری تقسیم کنیم. مثلا اگر قوانین شامل سیاستهای سطح بالا و پایین باشند که بهتر است از هم جدا شوند. تمام این تقسیمات به نوعی مرزهای معماری هستند. به عنوان معمار اگر بخواهیم این مرزها را کامل پیاده سازی کنیم، خیلی هزینه دارد و از طرفی اگر آنها را نادیده بگیریم، هزینه اضافه کردن آنها در آینده هم خیلی زیاد است.
راه حل این است که به عنوان معمار آینده را هم در نظر بگیریم، محل مرزها، هزینه و ریک هر کدام را تخمین بزنیم و در نهایت درباره پیاده سازی کامل، جزئی یا نادیده گرفتن تصمیم بگیریم. این کار را در همان ابتدای پروژه نمیتوان انجام داد و باید به صورت مداوم بررسیها انجام شود.
در ادامه به مولفه اصلی پرداخته شده است. در هر سیستم، حداقل یک مولفه وجود دارد که مولفههای دیگر را ایجاد، هماهنگ و نظارت میکند و به آن Main یا اصلی میگویند. این مولفه سطح پایین و بسیار کثیف است و در خارجیترین حلقه از معماری تمیز قرار میگیرد. پس مولفه Main نقطه ورودی اولیه به سیستم است و فقط سیستمعامل به آن وابسته است. وظیفه مولفه Main، ایجاد امکانات مورد نیاز (مانند جریان ورودی) و سپس واگذاری کنترل به بخشهای سطح بالای سیستم است. همچنین باید وابستگیها از طریق یک فریمورک (Dependency Injection Framework) در آن تزریق شوند.
به صورت کلی میتوانیم Main را به عنوان یک plugin برای برنامه در نظر بگیریم که تنظیمات اولیه را انجام میدهد و سپس کنترل را به مولفههای سطح بالاتر منتقل میکند. همچنین به خاطر افزونهای بودن این مولفه، میتوان انواع مختلفی از آن را برای هر پیکربندی در نظر گرفت.
در فصل بعدی به سرویسها و میکروسرویسها پرداخته شده است که امروزه محبوبیت بالایی دارند. علت این محبوبیت در ادامه آمده است. اما باید توجه داشت که این دلایل کاملا درست نیستند:
- سرویسها کاملا مجزا از هم هستند (decoupled)
- سرویسها از توسعه و استقرار مستقل پشتیبانی میکنند.
استفاده از سرویسها یک معماری نیست. زیرا آنچه معماری را مشخص میکند، مرزهایی است که policy سطح بالا را از جزئیات سطح پایین جدا میکند و همه سرویسها لزوما از نظر معماری مهم نیستند.
مزیت اول منجر میشود که سرویسها در فضاهای مجاز از هم اجرا شده و به اطلاعات هم دسترسی نداشته باشند. اما این تا حدی درست است. سرویسها از طریق منابع مشترک با هم در ارتباط هستند و در واقع به خاطر همین دادهها از هم مجزا نیستند.
مزیت دوم به این معنی است که هر تیم مسئول یک سرویس است. همچنین فرض شده که داشتن مولفههای مستقل از نظر استقرار و توسعه، مقیاسپذیر بوده و در سازمانها بزرگ هم امکانپذیر است. اما بخشی از این درست است. در سازمانها بزرگ سرویسها تنها راه ساخت سیستمهای مقیاسپذیر نیستند. همچنین این میزان استقلال بستگی به دادهها، کارکرد و سایر موارد مشترک دارد.
برای درک بهتر نادرستی دلایل محبوبیت سرویسها، مثالی از یک شرکت تاکسی اینترنتی آمده است که میخواهد کارکرد جدیدی اضافه کند تا گربهها را هم بتوانند از مبدا به مقصد ببرند. اما باید این را هم در نظر بگیرند که ممکن است رانندگان یا مسافران به گربه آلرژی داشته باشند. برای اضافه کردن این امکان باید همه سرویسها تغییر کنند. در واقع این نشان دهنده اتصال بین سرویسهاست و اینکه نمیتوانند به صورت مستقل از هم توسعه یابند. این ناشی از دغدغهها متداخل است و فقط مشکل سیستمهای سرویسگرا نیست.
چگونه این مشکل در معماری مبتنی بر مولفه حل میشود؟ استفاده از رویکرد شی گرایی به حل این مسئله کمک میکند. همچنین برای حل مشکل دغدغههای متداخل، باید سرویسها با با مولفههای داخلی طراحی شوند و از قانون وابستگی هم تبعیت کنند.
در ادامه به مرزهای تست پرداخته شده است. تستها هم بخشی از سیستم هستند و در معماری تاثیر دارند. تستها به طور طبیعی از قانون وابستگی تبعیت میکنند. تستها بسیار جزئی و concrete هستند و همیشه به کدها وابستهاند. به نوعی میتوان تستها را خارجیترین لایه معماری دانست که هیچ چیز به آنها وابسته نیست.
تستها به صورت مستقل قابل استقرار هستند. همچنین ایزولهترین بخش سیستم هستند و برای عملیاتی شدن سیستم ضروری نیستند و هدفشان پشتیبانی از توسعه سیستم است. اما این ایزوله کردن تستها اگر بیش از خد باشد توسعه دهنگان دچار گمراهی شده و تستها را خارج سیستم در نظر میگیرند و تستها به خوبی طراحی نمیشوند. در نتیجه در صورت تغییر سیستم، تستها تغییر نمیکنند، که این منجر به ایجاد مشکل «تستهای آسیب پذیر یا Fragile Tests Problem» میشود. تستهای آسیب پذیر اغلب تاثیرات نامطلوبی در انعطاف پذیری سیستم داشته باشد.
راه حل این مشکل طراحی به شکل testable است. اولین قانون طراحی نرمافزار این است که وابسته به چیزهای فرّار و volatile نباشیم. در واقع باید سیستم به گونهای ساخته شود که قوانین کسبوکار بدون نیاز به بخشهای volatile تست شوند.
راه حل رسیدن به این قابلیت، استفده از یک APIبرای تست است که بتوانیم تمام قوانین کسبوکار را تست کنیم. این APIباید شامل interactorها و adapterهای اینترفیس باشد که توسط رابط کاربری به کار بروند. هدف از APIجدا کردن تستها از برنامه کاربردی است.
Structural couplingیکی از قویترین شکلهای اتصال تست است. با استفاده از API ساختار برنامه از تستها مخفی میشود. از طرفی این API برای تست ممکن است منجر به نگرانیهای امنیتی شود. به همین دلیل توصیه میشود که این بخش در مولفه جداگانه نگهداری شود.
پس به صورت کلی تستها بخشی از سیستم هستند و باید به خوبی طراحی شوند. اگر با این دیدگاه تستها طراحی نشوند با دشواری در نگه داری رو به رو میشویم.
در ادامه به بحث معماری تو کار نهفته اشاره شده است. نرمافزار چیزی است که میتواند عمر مفید طولانی داشته باشد، در حالی که سخت افزار این طور نیست و به طور مداوم در حال بهبود و تکامل است. پس با اینکه نرمافزار فرسوده نمیشود اما به خاطر عدم میریت وابستگی ممکن است از بین برود.
فریمور به معنی نرمافزاری که در ROMقرار گرفته نیست و در واقع به جای ذخیره شدن ربطی ندارد. بلکه به دلیل آنچه که به آن وابسته است و میزان سختی تغییر آن، firmware گفته میشود. پس آنچه نیاز است firmware کمتر و نرمافزار بیشتر است. ما هم زمانی که کدهایمان به پلتفرم وابسته است، firmware مینویسیم. برای طولانی کردن عمر نرمافزار باید نوشتن firmware را متوقف کنیم و از معماری تو کار استفاده کنیم.
سه فعالیت ساخت نرمافزار از دید Beck در ادامه آمده است:
1- نرمافزار باید کار بکند وگرنه از بیزینس خارج میشود.
2- نرمافزار باید درست کار کند. یعنی متناسب با نیازها تغییر کند و بتوان آن را اصلاح کرد.
3- نرمافزار باید سریع کار کند. یعنی refactor برای بهبود پرفورمنس
پس اول باید چیزی که میسازیم کار کند و سپس آن را بهتر کنیم. مجبور کردن برنامه برای کارکردن تست app-titude است. برنامه نویسی فراتر از آن است که فقط یک برنامه کار کند.
بخش ششم از کتاب درباره جزئیات است. در ادامه به خلاصه کوتاهی از هر یک از جزئیات مطرح شده (پایگاه داده، وب، فریمورک) و مطالعه موردی در رابطه با فروش ویدئو پرداخته خواهد شد.
در فصل بعدی درباره نقش و اهمیت پایگاه داده در معماری سیستمهای نرمافزاری بحث شده است. از نظر معماری بانک اطلاعاتی بخش جزئی و کوچکی از معماری به شمار میرود و رابطه آن با معماری شبیه به رابطه دستگیره در و معماری خانه است. (همین قدر جزئی و کوچک) پس میتوان گفت که یک جزئیات کوچک در مقایسه با ساختار کلی سیستم نرمافزاری است.
در ادامه به بحث پایگاه داده رابطهای پرداخته شده است. مدل رابطهای یک فناوری برای ذخیرهسازی دادههاست و بنابراین میتوان گفت که یک برنامه کاربردی نباید در این زمینه دغدغهای داشته باشد. زیرا طبق توضیحاتنحوه کارکرد آنها، بانکهای اطلاعاتی یا پایگاههای داده مکانیزمی هستند که برای انتقال دادهها به عقب و جلو بین سطح دیسک و RAM میشوند.
در فصل بعدی به وب به عنوان یکی از جزئیات پرداخته شده است. در دهه 90 میلادی وب همه چیز را تغییر داد. در ادامه مثالی از شرکتی به نام Q آورده شده که یک سیستم مالی شخصی بسیار محبوب ایجاد کرد. این شرکت در نسخههای بعدی خود GUIرا تغییر داد تا مانند مرورگر شود و بعدها دوباهر این GUI شبیه مرورگر را از بین برده و از نسخه دسکتاپ استفاده کردند. GUIیکی از جزئیات است و وب نیز یک GUI است. پس وب یکی از جزئیات طراحی است و باید آنها را از منطق کسبوکار جدا کنیم.
در فصل بعدی به فریمورکها پرداخته شده است که جزئی از معماری هستند و خود معماری به شمار نمیروند. اغلب فریمورکها به صورت رایگان در اختیار افراد قرار میگیرند تا کمکی به جامعه شده باشد. رابطه بین ارائه دهندگان استفاده کنندگان از فریمورکها نامتقارن است. یعنی استفاده کننده باید نسبت به آن تعهد زیادی داشته باشند در حالی که ارائه دهنده هیچ تعهدی به آنها ندارند. همچنین استفاده از فریمورکها توسط استفاده کنندگان برای ارائه دهندگان که کنترل کاملی روی آن دارند، خطری ندارد. ارائه دهنده فریمورک میخواهد استفاده کننده را به آن وابسته کند زیرا چیزی که باعث اعتبار آنها میشود، داشتن گروه بزرگی از جامعه کاربران است.
استفاده از فریمورکها ریسکهایی مانند تمیز نبودن معماری دارد و حتی ممکن است در وسط راه فریمورکهای دیگری پیدا کنیم که بهتر باشند و بخواهیم از آنها استفاده کنیم. راهحل این است که فقط از فریمورکها استفاده کنیم و نباید به آنها وابسته باشیم. همچنین نباید بلافاصله با دیدن یک فریمورک از آن استفاده کنیم و به جامعه کاربران آن بپیوندیم. بلکه باید تا حد امکان تصمیمگیری در این زمینه را به تاخیر بیندازیم تا حتی شاید بدون نیاز به استفاده از برخی فریمورکها بتوانیم با هزینه کمتر به هدف خود برسیم.
در فصل بعدی به مطالعه موردی درباره یک وبسایت که فروش ویدئو پرداخته شده است. اولین گام درتعیین معماری اولیه این سیستم، شناسایی اکتورها و موارد کاربری هستند. در این مثال چهار اکتور (خریدار، ادمین، نویسنده و بازدید کننده) وجود دارد داریم و برای هر یک از آنها موارد کاربری را مشخص کردیم. در مرحلهبعدی معماری مولفهها مشخص میشود.
در شکل فوق جداسازی مولفهها در دو بعد انجام شده است. اولین بعد جداسازی اکتورها بر اساس مسئولیتها و دومین بعد وابستگی است.
یکی از روشها پکیج کردن به وسیله ساختار لایهای است. این روش اولین و سادهترین رویکرد طراحی است که در آن کدها بر اساس دیدگاه فنی به لایههای افقی تقسیم میشوند. وابستگی در این ساختار فقط به لایه پایین است. به اغلب این تقسیمبندی نرمافزارها را به سه قسمت تقسیم میکند که شامل وب، قوانین کسبوکار و ماندگاری (persistence) است.
روش دیگر پکیج بر اساس ویژگی است. در این رویکرد برشهای عمودی بر اساس ویژگیهای مرتبط و مفاهیم آن دامنه ایجاد میشود. پورتها و اداپتورها، معماری شش ضلعی و ... نیز در این روش به جداسازی کمک میکنند. به این معنی که کدهای مرتبط با دامنه در داخل و تعاملات با دنیای بیرونی در ناحیه خارجی قرار میگیرند. همچنین باید اصل وابستگی (از سمت خارجی به داخلی) نیز رعایت شود.
روش دیگر پکیج بر اساس کامپوننتهاست. این رویکرد ترکبیبی از کلیه روشهایی است که تا کنون در این کتاب بررسی شده است. در رویکرد پکیج بر اساس کامپوننت، منطق کسبوکار را با کدها با هم در یک بسته قرار میدهیم که از دید نویسنده همان مفهوم کامپوننت است.
در نهایت نیز به این موضوع اشاره شده که باید اندازه تیم، سطح مهارت آنها و پیچیدگی راهحل در رابطه با زمان و محدودیتهای پروژه در نظر گرفت.
این مطلب، بخشی از تمرینهای درس معماری نرمافزار در دانشگاه شهیدبهشتی است.