امین برجیان
امین برجیان
خواندن ۵۱ دقیقه·۳ سال پیش

کتاب معماری تمیز

معرفی کتاب و عنوان آن

کتاب Clean Architecture: A Craftsman’s Guide to Software Structure and Design اثر Robert C. Martin می‌باشد. معمولا اکثر افراد نویسنده‌ی این کتاب را با نام Uncle Bob می‌شناسند. نام این کتاب معماری تمیز گذاشته شده است اما سوالی که ایجاد می‌شود این است که چرا نام کتاب این‌گونه تعیین شده است؟ نویسنده‌ی کتاب در بخش مقدمه‌ی کتاب این چنین شروع می‌کند:

من اولین خط برنامه‌نویسی خود را در سن 12 سالگی در سال 1964 نوشتم. امروز سال‌ها گذشته است و من هنوز در حال نوشتن کد هستم. در آن زمان من در مورد ساختار سامانه‌های نرم‌افزاری مطالبی را یاد گرفتم، مطالبی که به نظرم از نظر دیگران نیز ارزشمند بودند.
من این موارد را با ساختن سامانه‌های بسیاری چه کوچک و چه بزرگ یاد گرفتم. من هم سامانه‌های کوچک در حد سامانه‌های نهفته (Embedded) را طراحی کردم و هم سامانه‌های بزرگ مثل سامانه‌های پردازش دسته‌ای را طراحی کردم. من هم سامانه‌های پردازش لحظه‌ای (real-time) و هم سامانه‌های مبتنی بر وب را طراحی کردم. من سامانه‌های مبتنی بر ترمینال، دارای رابط کاربری، بازی‌های متنوع، سامانه‌های حساب‌داری، ارتباط راه دور و خیلی موارد دیگر را تولید کردم. من همچنین برنامه‌های تک پروسس، چند پروسسه، دارای پروسس‌های سبک و سنگین، پایگاه‌داده‌ای، دارای محاسبات هندسی و ... نیز تولید کردم. من نرم‌افزارهای زیادی در این سال‌ها تولید کردم و در تمام این سال‌ها این را مشاهده کردم:
تمام قواعد معماری یکسان هستند!

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

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

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

این همان جواب راز جمله‌ی اول است. اگر چه ابزارها تغییر کرده‌اند اما عدم تغییر مفاهیم کدنویسی دلیلی است که معماری بین سامانه‌های مختلف هنوز یکپارچه هستند. قواعد معماری در واقعیت قواعد کنار هم گذاشتن این بلاک‌های کد و نحوه‌ی اجرای آن برای رسیدن به هدف هستند. از آن‌جایی که این بلاک‌های کد خود هنوز تغییر نکرده‌اند، قواعد مربوط به نحوه‌ی در کنار هم قرار دادن و ترتیب اجرای آن‌ها نیز تغییر نکرده است.(مطلبی که درک آن برای برنامه‌نویسان امروزه سخت است اگر چه که یک واقعیت است!)

به همین دلیل آنکل باب مطالب این کتاب را این‌گونه توصیف می‌کند:

در طول زمان یک چیز تغییر کرده است: در زمان‌های قدیم، ما نمی‌دانستیم قوانین معماری چیست. در نتیجه، ما آن‌ها را بارها و بارها شکستیم. اکنون، با نیم قرن تجربه پشت سرمان، ما به آن قوانین پی برده‌ایم. این قواعد (قوانین بی‌زمان و تغییرناپذیر) است که این کتاب درباره‌ی آن‌ها توضیح می‌دهد.

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

تمامی عکس‌های استفاده‌شده، همان عکس‌های کتاب است تا با حال و هوای عکس کتاب نیز آشنا شوید.

بخش یک (آشنایی)

تفاوت طراحی و معماری

در طول سال ها سردرگمی‌های زیادی در مورد طراحی و معماری وجود داشته است. طراحی چیست؟ معماری چیست؟ چه تفاوت‌هایی بین این دو وجود دارد؟

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

هدف معماری نرم‌افزار به حداقل رساندن نیروی انسانی موردنیاز برای ساخت و نگهداری سیستم موردنیاز است.

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

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

تنها راه سریع پیش رفتن، خوب پیش رفتن است.

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

ازرش دو مقدار (رفتار و ساختار)

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

رفتار سامانه

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

معماری (ساختار)

دومین ارزش نرم‌افزار مربوط به کلمه "نرم‌افزار" است، کلمه‌ای مرکب که از «soft» و «ware» تشکیل شده است. کلمه «ware» به معنای «محصول» است. کلمه «نرم» همان جایی است که ارزش دوم نهفته است. نرم‌افزار برای «نرم» بودن اختراع شد و در نظر گرفته شده بود که راهی برای تغییر آسان رفتار ماشین‌ها باشد. اگر می‌خواستیم رفتار ماشین‌ها به سختی تغییر کند، آن را سخت افزار می‌نامیدیم.

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

ارزش بیش‌تر؟ عملکرد یا معماری؟

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

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

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

ماتریس Eisenhower

ما دو نوع مشکل داریم، مشکل فوری (urgent) و مهم (important). موارد فوری مهم نیستند و موارد مهم هرگز فوری نیستند.

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

  1. فوری و مهم
  2. غیر فوری و مهم
  3. فوری و غیر مهم
  4. غیر فوری و غیر مهم

توجه داشته باشید که معماری کد (موارد مهم) در دو موقعیت بالای این لیست هستند، در حالی که رفتار کد (موارد فوری) در جایگاه‌های اول و سوم هستند. اشتباهی که مدیران و توسعه‌دهندگان کسب و کار اغلب مرتکب می‌شوند این است که موارد را در جایگاه 3 به جایگاه 1 ارتقا می‌دهند. به عبارت دیگر، آن‌ها نمی‌توانند آن ویژگی‌هایی را که فوری هستند اما مهم نیستند از ویژگی‌هایی که واقعا فوری و مهم هستند جدا کنند.

معضل توسعه‌دهندگان نرم افزار این است که مدیران تجاری برای ارزیابی اهمیت معماری توانایی کامل ندارند. این همان کاری است که توسعه‌دهندگان نرم افزار برای انجام آن استخدام شدند. بنابراین این مسئولیت تیم توسعه نرم‌افزار است که اهمیت معماری را بر فوریت ویژگی‌ها تایید کند.

تیم توسعه باید برای آنچه که معتقد است برای شرکت بهترین است مبارزه کند، تیم مدیریت، تیم بازاریابی و تیم فروش و تیم عملیات نیز همینطور.

به عنوان یک توسعه دهنده نرم افزار، شما یک ذینفع هستید. شما سهمی در نرم افزار دارید که باید از آن محافظت کنید. این بخشی از نقش شما و بخشی از وظیفه شماست و این بخش بزرگی از دلیل استخدام شما است.

بخش دوم (پارادایم‌های برنامه نویسی)

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

برنامه‌نویسی ساختاریافته

اولین پارادایم پذیرفته شده (اما نه اولین موردی که اختراع شد) برنامه‌نویسی ساختاریافته بود. Dijkstra نشان‌داد که استفاده از پرش‌های مهار نشده (گزاره های goto) برای ساختار برنامه مضر است. او این پرش‌ها را با ساختارهای آشناتر if/then/else و do/while/until جایگزین کرد. می‌توانیم پارادایم برنامه‌نویسی ساخت‌یافته را به صورت زیر خلاصه کنیم:

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

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

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

برنامه‌نویسی شی‌گرا

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

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

اما شی‌گرا دقیقا چیست؟ ترکیبی از داده‌ها و عملکرد جواب برخی از افراد به این سوال است اما این یک پاسخ صرفا رضایت بخش است و دقیق نیست چون فرق زیادی بین obj.f با f(obj) نیست. یکی دیگر از پاسخ‌های رایج به این سوال، راهی برای مدل‌سازی دنیای واقعی است. این یک پاسخ گریزان است زیرا مدل‌سازی از دنیای واقعی واقعا به چه معناست و چرا این چیزی است که ما می‌خواهیم انجام دهیم؟ شاید این عبارت به این معنی باشد که شی‌گرایی درک نرم افزار را آسان‌تر می‌کند زیرا رابطه نزدیک‌تری با دنیای واقعی دارد اما حتی این عبارت نیز ضعیف است و اصلا مشخص نمی‌کند که بالاخره شی‌گرایی چیست.

برخی از افراد برای توضیح ماهیت شی‌گرایی از سه کلمه جادویی استفاده می‌کنند: کپسوله‌سازی (encapsulation)، وراثت، و چندشکلی. به عبارتی زبانی را شی‌گرا می‌دانند که ترکیب مناسبی از این سه چیز است یا حداقل از این سه چیز پشتیبانی کند. اما اگر به صورت دقیق‌تر زبان‌های گذشته مثل C را مشاهده کنیم و بررسی کنیم، می‌بینیم که هر کدام از نیازهای بالا را می‌توانستیم به شکل دیگری برآورده کنیم (با فرمت دیگری) و در نتیجه در زبان‌های قدیمی نیز این ویژگی‌ها وجود داشته است. صرفا در زبان‌های شی‌گرا این مفاهیم ساده‌تر شده و نامگذاری شده‌اند!

نکته این است که بخش زیادی از این ویژگی‌ها در زبان‌های قدیمی توسط اشاره‌گرها (pointers) فراهم می‌شود. مشکل استفاده صریح از اشاره‌گر به توابع برای ایجاد رفتار چندشکلی یا وراثت این است که نشانگرهای توابع خطرناک هستند. چنین استفاده‌ای توسط مجموعه‌ای از قراردادهای دستی هدایت می‌شود. شما باید به یاد داشته باشید که برای مقداردهی اولیه آن اشاره‌گرها از قرارداد پیروی کنید. شما باید به یاد داشته باشید که برای فراخوانی تمام توابع خود از طریق آن اشاره‌گرها، از قرارداد پیروی کنید. اگر هر برنامه‌نویسی نتواند این قراردادها را به خاطر بسپارد، ردیابی و حذف باگ ایجاد شده به طرز عجیبی دشوار است. زبان‌های شی‌گرا این قراردادها و در نتیجه این خطرات را حذف می‌کنند. این واقعیت قدرت عظیمی را ارائه می‌دهد که برنامه‌نویسان قدیمی C فقط می‌توانستند رویای آن را داشته باشند. بر این اساس می‌توان نتیجه گرفت که شی‌گرایی نظم و انضباط را بر انتقال غیرمستقیم کنترل (Pointers) تحمیل می‌کند.

این واقعیت که زبان‌های شی‌گرا چند شکلی را به صورت امن و راحت ارائه می‌کنند به این معنی است که هر نوع وابستگی کد منبع، مهم نیست که کجا باشد، می‌تواند معکوس شود. (برای این مورد می‌توانید در مورد Dependency Inversion مطالعه کنید) این به معمار اجازه می‌دهد تا یک معماری پلاگین‌مانند ایجاد کند، که در آن ماژول‌هایی که شامل سیاست‌های سطح بالا هستند مستقل از ماژول‌هایی هستند که حاوی جزئیات سطح پایین هستند. جزئیات سطح پایین به ماژول‌های افزونه منتقل می‌شوند که می‌توانند مستقل از ماژول‌هایی که حاوی سیاست‌های سطح بالا هستند، مستقر و توسعه یابند. این همان قدرت واقعی پارادیام شی‌گرایی می‌باشد.

برنامه‌نویسی تابعی (Functional)

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

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

چرا این نکته به عنوان یکی از ملاحظات معماری مهم است؟ چرا یک معمار باید نگران تغییرپذیری متغیرها باشد؟ همه شرایط مسابقه (race condition)، شرایط بن بست و مشکلات به روزرسانی همزمان به دلیل متغیرهای قابل تغییر هستند. اگر هیچ متغیری به‌روزرسانی نشود، نمی‌توانید شرایط مسابقه یا مشکل به‌روزرسانی همزمان داشته باشید. شما نمی‌توانید بدون قفل‌های قابل تغییر (locks) بن بست داشته باشید. به عبارت دیگر، تمام مشکلاتی که در برنامه‌های همزمان با آن‌ها مواجه می‌شویم، اگر متغیرهای قابل تغییر وجود نداشته باشند، نمی‌توانند اتفاق بیفتند. اما آیا تغییرناپذیری عملی است؟ اگر فضای ذخیره‌سازی بی‌نهایت و سرعت پردازنده بی‌نهایت باشد، پاسخ این سوال مثبت است. با نداشتن آن منابع بی‌نهایت، تغییر‌ناپذیری می‌تواند عملی باشد، اگر مصالحه‌های خاصی صورت گیرد. مواردی مثل جداسازی بخش‌های تغییرناپذیر و تغییرپذیر تا حد ممکن از یکدیگر، استفاده از روش‌های منبع‌کردن رویداد (event sourcing) و ... می‌تواند راه‌حل‌های میانه برای این موضوع باشد.


هر یک از پارادایم‌ها قابلیت‌هایی را از برنامه‌نویس حذف می‌کند. هیچ یک از آن‌ها قابلیت‌های جدیدی را اضافه نمی‌کند. هر کدام نوعی قوانین اضافی را تحمیل می‌کنند که در ذات خود منفی است. پارادایم‌ها به ما می‌گویند چه کاری را نباید انجام دهیم، تا اینکه به ما بگویند چه کار کنیم. این پارادایم ها چه ربطی به معماری دارد؟ ما از چندشکلی به عنوان مکانیزمی برای عبور از مرزهای معماری استفاده می‌کنیم. ما از برنامه نویسی کاربردی برای تحمیل نظم و انضباط در مکان و دسترسی به داده‌ها استفاده می‌کنیم. از برنامه‌نویسی ساخت‌یافته به عنوان پایه الگوریتمی ماژول‌های خود استفاده می‌کنیم. این سه تا حد خوبی با دغدغه بزرگ معماری همسو می‌شوند: عملکرد، جداسازی اجزا (separation of components) و مدیریت داده. در واقعیت در نگاه ساده پارادیام‌ها ما را محدود می‌کنند اما در عمل تمامی این قوانین برای این هستند که نظم برنامه بهتر شود و در بلندمدت به ما کمک خواهند کرد.

بخش سوم (اصول طراحی)

در این بخش uncle bob به بحث اصول SOLID و ارتباط آن‌ها با معماری می‌پردازد. اصول SOLID مشخص می‌کنند که چگونه توابع و ساختارهای داده را در کلاس‌ها مرتب کنیم و چگونه آن کلاس‌ها باید به هم متصل شوند. استفاده از کلمه «کلاس» به این معنی نیست که این اصول فقط برای زبان‌ها شی‌گرا قابل اجرا هستند. یک کلاس به سادگی یک گروه‌بندی توابع و داده است. هر سیستم نرم‌افزاری چنین گروه‌بندی‌هایی دارد، مستقل از این که کلاس نامیده شوند یا نه. اصول SOLID برای آن گروه‌بندی‌ها اعمال می‌شود.

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

اصل مسئولیت واحد (Single Responsibility Principle)

سیستم‌های نرم‌افزاری برای جلب رضایت کاربران و ذینفعان تغییر می‌کنند. آن کاربران و ذینفعان «دلیل تغییر» هستند. متأسفانه، کلمات «کاربر» و «ذینفع» واقعا کلمات مناسبی برای استفاده در اینجا نیستند. احتمالا بیش از یک کاربر یا ذینفع وجود خواهد داشت که خواهان تغییر سیستم به همان روش هستند. در عوض، ما واقعا به یک گروه (یک یا چند نفر) اشاره می‌کنیم که به این تغییر نیاز دارند. ما از آن گروه به عنوان یک بازیگر (actor) یاد خواهیم کرد. بنابراین SRP به این صورت است:

یک ماژول باید در برابر یک و تنها یک بازیگر مسئول باشد.

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

اصل باز و بسته (Open-Closed Principle)

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

یک نرم‌افزار تولیدشده باید برای گسترش باز باشد اما برای تغییرات بسته شود.

به عبارت دیگر، رفتار یک نرم‌افزار باید بدون نیاز به تغییر آن محصول قابل گسترش باشد. این اساسی‌ترین دلیلی است که ما معماری نرم‌افزار را مطالعه می‌کنیم. بدیهی است که اگر نیازمندی‌های ساده که از جنس اضافه‌شدن یک ویژگی جدید هستند نیاز به تغییرات گسترده‌ای در نرم افزار و بخش‌های از قبل نوشته‌شده هستند، معماران آن سیستم نرم‌افزاری دچار شکستی چشمگیر شده‌اند. اکثر افراد طراحی نرم‌افزار OCP را به عنوان اصلی می‌شناسند که آن‌ها را در طراحی کلاس‌ها و ماژول‌ها راهنمایی می‌کند، اما وقتی سطح اجزای معماری را در نظر می‌گیریم، این اصل اهمیت بیشتری پیدا می کند.

اصل OCP یکی از نیروهای محرک پشت معماری سیستم‌ها است. هدف این است که سیستم را به راحتی گسترش دهیم بدون اینکه تأثیر زیادی از تغییر ایجاد شود. این هدف با پارتیشن‌بندی سیستم به اجزا و مرتب‌کردن آن اجزا در یک سلسله مراتب وابستگی انجام می‌شود که از اجزای سطح بالاتر در برابر تغییرات در اجزای سطح پایین محافظت می‌کند.

اصل جایگزینی لیسکوف (Liskov Substitution Principle)

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

آنچه در این‌جا می‌خواهیم چیزی شبیه ویژگی جایگزینی زیر است: اگر برای هر شی o1 از نوع S یک شی o2 از نوع T وجود داشته باشد به طوری که برای همه برنامه‌های P که بر حسب T تعریف شده‌اند، رفتار P هنگامی که o1 جایگزین می‌شود بدون تغییر است. برای o2 پس S یک زیرنوع از T است.

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

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

این طراحی با LSP مطابقت دارد زیرا رفتار برنامه صورتحساب (Billing) به هیچ وجه به هیچ کدام از دو نوع مجوز بستگی ندارد. هر دو نوع مجوز (تجاری و شخصی) قابل جایگزینی برای نوع مجوز (License) هستند.

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

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

Rectangle r = ... r.setW(5); r.setH(2); assert(r.area() == 10);

اگر کد بخشی که مشخص نشده است یک مربع تولید می‌کرد، آن‌گاه ادعا (assert) شکست می‌خورد. تنها راه دفاع در برابر این نوع نقض LSP افزودن مکانیسم‌هایی به کاربر (مانند عبارت if) است که تشخیص می‌دهد که آیا مستطیل در واقع مربع است یا خیر. از آن‌جایی که رفتار کاربر به انواعی که استفاده می‌کند بستگی دارد، آن انواع قابل جایگزینی نیستند. (یعنی در عمل ارث‌بری این دو کلاس از همدیگر اشتباه بوده و باید دو کلاس جدا می‌شدند)

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

  • رابطی (interface) به سبک جاوا که توسط چندین کلاس پیاده‌سازی شده است.
  • چندین کلاس Ruby یا Go که امضاهای متد (method signature) یکسانی دارند.
  • مجموعه‌ای از خدمات که همه از طریق یک رابط REST یکسان پاسخ می‌دهند.

در تمام موقعیت‌های بالا و موارد دیگر، LSP قابل استفاده است. یک نقض ساده قابلیت جایگزینی، می‌تواند باعث شود که معماری سیستم با مقدار قابل توجهی از مکانیسم‌های اضافی آلوده شود.

اصل جداسازی رابط‌ها (Interface Segregation Principle)

این اصل به طراحان نرم‌افزار توصیه می‌کند که از وابستگی به چیزهایی که استفاده نمی‌کنند اجتناب کنند. وابستگی به ماژول‌هایی که حاوی بیش از نیاز شما هستند مضر است. این بدیهی است که برای وابستگی‌های کد (source code) می‌توانند کامپایل‌های غیرضروری و جابجایی مجدد را ایجاد کند. اما در سطح معماری نیز این اصل ضروری و مهم است و عدم رعایت آن منجر به همین مشکلات می‌شود.

به عنوان مثال، یک معمار را در نظر بگیرید که روی یک سیستم (S) کار می‌کند، او می‌خواهد یک چارچوب خاص (F) را ا در سیستم بگنجاند. حال فرض کنید که نویسندگان F آن را به یک پایگاه داده خاص (D)، متصل کرده‌اند. بنابراین S به F بستگی دارد که به D بستگی دارد. حال فرض کنید که D دارای ویژگی‌هایی است که F از آن‌ها استفاده نمی‌کند و بنابراین S به آن‌ها اهمیت نمی‌دهد. تغییرات در آن ویژگی‌ها (یعنی تغییر در D) ممکن است به‌خوبی باعث جابجایی مجدد F و بنابراین، استقرار مجدد S شود. حتی بدتر از آن، خرابی یکی از ویژگی‌های درون D ممکن است باعث خرابی در F و S شود. مشکل از آن‌جایی ایجاد شد که سامانه S به کلیه‌ی ویژگی‌های F نیاز نداشت اما تصمیم گرفت به آن وابسته شود. درسی که در اینجا وجود دارد این است که اگر چمدانی را حمل می‌کنید که در آن اشیایی است که به آن نیاز ندارید، می تواند مشکلاتی را برای شما ایجاد کند که انتظارش را نداشتید.

اصل وارونگی وابستگی‌ها (Dependency Inversion Principle)

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

اصل وارونگی وابستگی (DIP) بیان می‌کند که انعطاف‌پذیرترین سیستم‌ها، آن‌هایی هستند که در آن وابستگی‌های کد فقط به انتزاع‌ها (abstractions) اشاره می‌کنند، نه به موارد پیاده‌سازی (concretions).

واضح است که در نظر گرفتن این ایده در حالت کلی و به صورت صد درصد غیر ممکن است، زیرا سیستم‌های نرم‌افزاری باید به بسیاری از امکانات ملموس وابسته باشند. به عنوان مثال، کلاس String در جاوا عینی است و این بی معنی است که سعی کنیم آن را انتزاعی کنیم. نمی‌توان و نباید از وابستگی کد به String اجتناب کرد. کلاس String بسیار پایدار است و تغییرات آن بسیار نادر است و به شدت کنترل می‌شود. برنامه‌نویسان و معماران لازم نیست نگران تغییرات مکرر و هولناک String باشند.به این دلایل، وقتی صحبت از DIP به میان می‌آید، ما تمایل داریم که پس زمینه پایدار سیستم عامل و امکانات platform را نادیده بگیریم. ما آن وابستگی‌های ملموس را تحمل می‌کنیم زیرا می‌دانیم که می‌توانیم برای تغییر نکردن به آن‌ها تکیه کنیم.

در عوض عناصری که ما تعریف می‌کنیم و سامانه را با آن توسعه می‌دهیم دارای یکسری عناصر فرار (volatile) هستند. ما دنبال این هستیم که از وابستگی به این موارد تا حد امکان اجتناب کنیم. این‌ها ماژول‌هایی هستند که ما به طور فعال در حال توسعه آن‌ها هستیم و در حال تغییر مکرر هستند.

هر تغییر در یک رابط انتزاعی (interface) با تغییری در پیاده‌سازی‌های عینی آن (implementation) همراه است. برعکس، تغییرات در پیاده‌سازی‌های همیشه و یا حتی معمولا به تغییراتی در رابط‌هایی که پیاده‌سازی می‌کنند نیاز ندارند. بنابراین واسط‌ها نسبت به پیاده‌سازی‌ها کمتر تغییر می‌کنند. در واقع، طراحان و معماران نرم‌افزار خوب سخت تلاش می‌کنند تا نوسان تغییرات رابط‌ها را کاهش دهند. آن‌ها سعی می‌کنند راه‌هایی را برای اضافه‌کردن عملکرد به پیاده‌سازی‌ها استفاده کنند به گونه‌ای که تغییراتی در رابط‌ها ایجاد نشود.

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

  • به کلاس‌های پیاده‌سازی فرار (volatile concrete) ارجاع ندهید. به جای آن به رابط‌های انتزاعی مراجعه کنید. این قانون در همه زبان‌ها، اعمال می‌شود. معمولا عمل به این قاعده محدودیت‌های شدیدی را برای ایجاد اشیا ایجاد می‌کند و به همین دلیل معمولا همراه Abstract Factories اعمال می‌شود.
  • عملکرد کلاس‌های پیاده‌سازی (concrete) را با ارث‌بری جایگزین (override) نکنید. توابع این کلاس‌ها اغلب به وابستگی کد منبع نیاز دارند. وقتی آن توابع را جایگزین می‌کنید، آن وابستگی‌ها را حذف نمی‌شوند (dependencies)، در واقع آن‌ها را به ارث می‌برید. برای مدیریت آن وابستگی‌ها، باید تابع موردنظر را انتزاعی کنید و چندین پیاده‌سازی ایجاد کنید که هر کدام آن تابع را مطابق با نیاز شما پیاده‌سازی کنند.

بخش چهارم (اصول کامپوننت‌ها)

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

تعریف کامپوننت

بیش از شروع به صحبت‌کردن در مورد کامپوننت‌ها لازم است ابتدا تعریف کامپوننت‌ها را بفهمیم. کامپوننت یا اجزاء واحدهای استقرار هستند. آن‌ها کوچکترین موجوداتی هستند که می‌توانند به عنوان بخشی از یک سیستم مستقر شوند. در جاوا، آن‌ها فایل‌های jar هستند. در Ruby، آن‌ها فایل‌های gem هستند. در Net، آن‌ها DLL هستند. در زبان‌های کامپایل شده، آن‌ها انبوهی از فایل‌های باینری هستند. در زبان‌های تفسیر شده، آن‌ها مجموعه‌ای از فایل‌های کد هستند. در همه زبان‌ها، کامپوننت‌ها اجزایی هستند که استقرار می‌یابند.

کامپوننت‌ها را می‌توان به یک فایل اجرایی متصل کرد یا می‌توان آن‌ها را با هم در یک آرشیو واحد، مانند یک فایل war جمع کرد یا می‌توان آن‌ها را به‌صورت مستقل به‌عنوان افزونه‌هایی مانند فایل‌های jar یا dll یا exe مستقر کرد. صرف نظر از نحوه استقرار آن‌ها در نهایت، اجزای خوب طراحی شده همیشه این توانایی را حفظ می‌کنند که به طور مستقل قابل استقرار و بنابراین به طور مستقل قابل توسعه باشند.

انسجام کامپوننت‌ها (component cohesion)

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

  • اصل هم‌ارزی استفاده مجدد/انتشار (Reuse/Release Equivalence Principle) (REP)
  • اصل بسته‌بندی مفاهیم مرتبط (Common Closure Principle) (CCP)
  • اصل استفاده مجدد از مشترک‌ها (Common Reuse Principle) (CRP)

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

اصل هم‌ارزی استفاده مجدد/انتشار (Reuse/Release Equivalence Principle)

در دهه‌های گذشته شاهد ظهور مجموعه‌ای از ابزارهای مدیریت ماژول مانند Maven یا Leiningen و RVM بوده‌ایم. اهمیت این ابزارها در طول زمان افزایش یافته است و تعداد زیادی از اجزای قابل استفاده مجدد و کتابخانه‌ها ایجاد شده است. ما اکنون در عصر استفاده مجدد از نرم‌افزار زندگی می کنیم. اصل هم ارزی استفاده مجدد/انتشار (REP) یک اصل است که حداقل در ظاهر بدیهی به نظر می‌رسد:

افرادی که می‌خواهند از یک کتابخانه نرم‌افزاری استفاده مجدد کنند، نمی‌توانند و نمی‌خواهند این کار را انجام دهند، مگر اینکه این مؤلفه‌ها از طریق فرآیند انتشار (release) ردیابی شوند و شماره انتشار به آن‌ها داده شود.

وجود اعداد انتشار (release version) به این دلیل است که بدون وجود آن‌ها هیچ راهی برای اطمینان از سازگاری همه کتابخانه‌های استفاده‌شده با یکدیگر وجود نخواهد داشت. به عبارتی این نشان‌دهنده این واقعیت است که توسعه‌دهندگان نرم‌افزار باید بدانند چه زمانی نسخه‌های جدید عرضه می‌شوند و این نسخه‌های جدید چه تغییراتی را به همراه خواهند داشت.

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

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

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

اصل بسته‌بندی مفاهیم مرتبط (Common Closure Principle)

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

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

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

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

اصل استفاده مجدد از مشترک‌ها (Common Reuse Principle)

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

کاربران یک کامپوننت را مجبور نکنید به چیزهایی که نیاز ندارند وابسته شوند.

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

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

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

طبق توضیحات بالا، CRP نسخه عمومی‌تر قاعده ISP است که در بخش‌های قبلی مشاهده کردیم. ISP بیان می‌کند که به کلاس‌هایی وابسته نباشیم که بخش‌هایی دارند که ما استفاده نمی‌کنیم. CRP به ما توصیه می‌کند به کامپوننت‌هایی که کلاس‌هایی دارند که ما استفاده نمی‌کنیم وابسته نباشیم. به چیزهایی که نیاز ندارید وابسته نباشید.


نمودار تنش برای انسجام کامپوننت‌ها

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

به عنوان مثال طبق نمودار بالا معماری كه فقط بر روی REP و CRP تمركز می‌كند، متوجه می‌شود كه هنگام ايجاد تغييرات ساده، اجزای زيادی تحت تاثير قرار می‌گیرند. در مقابل، معماری كه به شدت بر CCP و REP تمركز می‌كند، باعث می‌شود كه تعداد زيادی نسخه‌های غیرضروری تولید شود.

یک معمار خوب تلاش می‌کند که دغدغه‌های فعلی تیم توسعه را برآورده بکند، اما همچنین آگاه است که این نگرانی‌ها در طول زمان تغییر خواهند کرد. به عنوان مثال، در اوایل توسعه یک پروژه، CCP بسیار مهمتر از REP است، زیرا توسعه‌پذیری مهمتر از استفاده مجدد است.

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

جفت‌شدن کامپوننت‌ها (component coupling)

در این بخش از کتاب عمو باب شروع به صحبت‌کردن در مورد ارتباط بین کامپوننت‌ها و نحوه‌ی تعامل آن‌ها می‌پردازد. در این بخش نیز 3 اصل مهم در رابطه با ارتباط میان کامپوننت‌ها اشاره می‌شود:

  • اصل وابستگی‌های غیر چرخشی (Acyclic Dependencies Principle)
  • اصل وابستگی‌های پایدار (Stable Dependencies Principle)
  • اصل انتزاعات پایدار (Stable Abstractions Principle)

اصل وابستگی‌های غیر چرخشی (Acyclic Dependencies Principle)

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

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

در طول چند دهه گذشته، دو راه حل برای این مشکل تکامل یافته است. پیشنهاد اولا معمولا «تولید هفتگی» است و دومی اصل وابستگی‌های غیر چرخشی (ADP) است.

تولید هفتگی (Weekly Build)

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

این رویکرد دارای مزیت شگفت‌انگیزی است که به توسعه‌دهندگان اجازه می‌دهد تا چهار روز از پنج روز هفته را در یک دنیای منزوی زندگی کنند. بدیهی است که ضرر آن هزینه‌ای خواهد بود که در روز ادغام این تغییرات باید پرداخت.

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

با کاهش تعداد روزهای باقی‌مانده برای توسعه‌دهندگان در مقابل زمان عظیم ادغام تغییرات، کارایی تیم کاهش می‌یابد. در نهایت این وضعیت به قدری ناامیدکننده می‌شود که توسعه‌دهندگان یا مدیران پروژه اعلام می‌کنند که برنامه باید به یک تولید هر دو هفته‌ای تغییر دهند. این برای مدتی کافی است، اما زمان ادغام با اندازه پروژه به رشد خود ادامه می‌دهد و به احتمال زیاد این کافی نخواهد بود!

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

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

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

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

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

حذف وابستگی‌های چرخه‌ای

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

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

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

اصل وابستگی‌های پایدار (Stable Dependencies Principle)

طرح‌ها و معماری‌ها نمی‌توانند کاملا ثابت باشند. اگر قرار است طرح و معماری حفظ شود، مقداری نوسان لازم است. (چون نیازمندی‌ها از سمت مشتری تغییر می‌کند) با انطباق با اصل CCP که در بخش‌های قبلی دیدیم، باید تلاش کنیم که اجزایی ایجاد کنیم که نسبت به انواع خاصی از تغییرات حساس هستند، اما نسبت به سایر تغییرات مصون هستند. برخی از این کامپوننت‌ها به گونه‌ای طراحی می‌شوند که فرار (volatile) باشند. ما انتظار داریم آن‌ها تغییر کنند و هر مؤلفه‌ای که انتظار داریم بی‌ثبات باشد، نباید به مؤلفه‌ای وابسته باشد که تغییر آن دشوار است. در غیر این صورت، تغییر آن بخش غیرثابت نیز دشوار خواهد بود.

ماژولی را که شما طراحی کرده‌اید تا به راحتی قابل تغییر باشد، می‌تواند توسط شخص دیگری مورد استفاده قرار بگیرد و تغییر آن را دشوار کند. هیچ خطی از کد منبع در ماژول شما نیاز به تغییر ندارد، اما تغییر ماژولی که به آن وابسته شدید، ناگهان چالش برانگیزتر می‌شود. با انطباق با اصل وابستگی‌های پایدار (SDP)، اطمینان حاصل می‌کنیم که ماژول‌هایی که برای تغییر آسان در نظر گرفته شده‌اند توسط ماژول‌هایی که تغییر آن‌ها سخت‌تر است استفاده نشوند.

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

این موضوع چه ارتباطی با نرم‌افزار دارد؟ بسیاری از عوامل ممکن است تغییر یک کامپوننت نرم‌افزار را سخت کند به عنوان مثال، اندازه، پیچیدگی آن و ... عمو باب همه آن عوامل را نادیده می‌گیرید و در عوض روی مورد خاصی متمرکز می‌شود. اگر کامپوننت‌های فراوانی به یک کامپوننت وابسته شوند، در عمل تغییر آن کامپوننت بسیار سخت خواهد شد. یک کامپوننت‌ با وابستگی‌های ورودی زیاد بسیار پایدار است، زیرا برای هرگونه تغییری باید تمامی کامپوننت‌‌های وابسته به آن را در نظر بگیریم که به کار زیادی نیاز دارد.

نمودار در شکل زیر را نشان می‌دهد که X یک کامپوننت‌‌ پایدار است. سه کامپوننت‌‌ به X بستگی دارند، بنابراین سه دلیل خوب برای تغییر نکردن آن وجود دارد. ما می‌گوییم که X مسئول آن سه کامپوننت‌‌ است. برعکس، X به هیچ چیز دیگری وابسته نیست بنابراین هیچ چیز تأثیر خارجی برای تغییر آن وجود ندارد و به همین دلیل ما می‌گوییم مستقل است.

شکل زیر نشان می‌دهد که Y کامپوننت‌‌ بسیار ناپایدار است. هیچ کامپوننت‌‌ دیگری به Y وابسته نیست، بنابراین می‌گوییم که Y غیرمسئول در برابر دیگران است. Y همچنین دارای سه کامپوننت‌‌ است که به آن‌ها بستگی دارد، بنابراین تغییرات ممکن است از سه منبع خارجی باشد و به همین دلیل می‌گوییم Y وابسته است.

متریک‌های پایداری

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

  • مفهوم Fan-in (وابستگی‌های ورودی): این متریک تعداد کلاس‌های خارج از این کامپوننت را که به کلاس‌های داخل کامپوننت بستگی دارد، مشخص می‌کند.
  • مفهوم Fan-out (وابستگی‌های خروجی): این متریک تعداد کلاس‌های داخل این کامپوننت را که به کلاس‌های خارج از کامپوننت بستگی دارد، مشخص می‌کند.
  • مفهوم I (ناپایداری): این مفهوم را بر اساس دو مفهوم بالا تعریف می‌کنیم. این مفهوم را می‌توان با فرمول زیر مدل نمود. این معیار دارای محدوده صفر تا یک است. I = 0 یک کامپوننت حداکثر پایداری را نشان می‌دهد. I = 1 یک کامپوننت حداکثر ناپایدار را نشان می‌دهد.
I = Fan-out / (Fan-in + Fan-out)

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

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

در جهت پایداری وابسته شوید.

اصل انتزاعات پایدار (Stable Abstractions Principle)

برخی از بخش‌های موجود در یک سامانه‌ی نرم‌افزاری نباید اغلب تغییر کنند. این موارد در واقعیت همان معماری سطح بالا و تصمیمات مربوط به سیاست‌ها هستند. ما نمی‌خواهیم این تصمیمات تجاری و معماری بی‌ثبات باشد. بنابراین نرم‌افزاری که سیاست‌های سطح بالای سیستم را در بر می‌گیرد باید در اجزای پایدار قرار گیرد (0 = I). مؤلفه‌های ناپایدار (I = 1) باید فقط شامل بخش‌هایی باشند که بی‌ثبات هستند، بخشی که می‌خواهیم بتوانیم سریع و آسان آن‌ها را تغییر دهیم. با این حال، اگر سیاست‌های سطح بالا در کامپوننت‌های پایدار قرار داده شوند، تغییر کد منبعی که آن سیاست‌ها را نشان می‌دهد دشوار خواهد بود. این می‌تواند معماری کلی را غیرقابل انعطاف کند.

چگونه یک کامپوننت که حداکثر پایدار است (I = 0) می‌تواند به اندازه کافی انعطاف‌پذیر باشد تا در برابر تغییراتی که از سمت مشتری ایجاد می‌شود مقاومت کند؟ پاسخ در اصل OCP یافت می‌شود در بخش‌های قبلی توضیح دادیم. این اصل به ما می‌گوید که مطلوب است که کلاس‌هایی ایجاد کنیم که به اندازه کافی انعطاف‌پذیر باشند تا بدون نیاز به اصلاح، گسترش یابند. کلاس‌های انتزاعی با این اصل مطابقت دارند.

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

قاعده SDP می گوید که وابستگی‌ها باید در جهت ثبات حرکت کنند و SAP می‌گوید که ثبات به معنای انتزاع است. بنابراین وابستگی‌ها در جهت انتزاع حرکت می‌کنند.

اندازه‌گیری انتزاع

متریک A معیاری برای انتزاع‌بودن یک کامپوننت است. مقدار آن صرفا نسبت رابط‌ها و کلاس‌های انتزاعی در یک کامپوننت به تعداد کل کلاس‌های کامپوننت است.

  • متریک Nc: تعداد کلاس‌های کامپوننت.
  • متریک Na: تعداد کلاس‌ها و رابط‌های انتزاعی در کامپوننت.
  • متریک انتزاع: نسبت بین Na به Nc می‌باشد. متریک A از 0 تا 1 متغیر است. مقدار 0 به این معنی است که کامپوننت هیچ کلاس انتزاعی ندارد. مقدار 1 به این معنی است که کامپوننت چیزی جز کلاس‌های انتزاعی ندارد.

دنباله‌ی اصلی (Main Sequence)

در ادامه خوب است که رابطه بین ناپایداری (I) و انتزاع (A) را تعریف کنیم و مورد بررسی قرار دهیم. برای انجام این کار، یک نمودار با A در محور عمودی و I در محور افقی ایجاد می‌کنیم. اگر دو نوع کامپوننت «خوب» را در این نمودار رسم کنیم، کامپوننت‌هایی را خواهیم یافت که حداکثر پایداری و انتزاعی را در بالا سمت چپ در (0، 1) دارند. اجزایی که حداکثر ناپایدار و عینی هستند در پایین سمت راست در (1، 0) قرار دارند.

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

از آن‌جایی که نمی‌توانیم قاعده‌ای را اعمال کنیم که همه مؤلفه‌ها در (0، 1) یا (1، 0) قرار گیرند، باید فرض کنیم که یک مکان از نقاط در نمودار A/I وجود دارد که موقعیت‌های معقولی را برای کامپوننت‌‌ها تعریف می‌کند. این بخش معمولا همین خط میانه‌ی نمودار هست که از آن به عنوان دنباله‌ی اصلی یاد می‌شود. با این حال وقتی این ناحیه‌ی میانی را در نظر می‌گیریم یکسری ناحیه‌ی دیگر هم در نمودار ظاهر می‌شوند. این ناحیه‌ها، نواحی هستند که تا حد امکان باید از قرارگیری مولفه‌ها در آن جلوگیری شود. در ادامه این نواحی را موردبررسی قرار می‌دهیم.

ناحیه‌ی درد (Zone Of Pain)

مولفه‌ای را در ناحیه‌ی اطراف (0، 0) در نظر بگیرید. این یک کامپوننت بسیار پایدار و در عین حال عینی (غیرانتزاعی) است. چنین کامپوننتی مطلوب نیست زیرا تغییر آن سخت است. نمی‌توان آن را گسترش داد زیرا انتزاعی نیست و تغییر آن به دلیل وابستگی‌های بسیار دیگران به آن دشوار است. بنابراین ما معمولاً انتظار نداریم که کامپوننت‌های خوب طراحی شده را در نزدیکی (0، 0) نبینیم. ناحیه اطراف (0، 0) ناحیه طرد شده به نام ناحیه‌ی درد است.

با این حال برخی از موجودیت‌های نرم‌افزاری در واقع در منطقه درد قرار می‌گیرند. یک مثال می‌تواند یک ساختار داده‌ها در پایگاه داده باشد. ساختارهای داده‌ی پایگاه داده بسیار بی‌ثبات، ملموس و به شدت وابسته هستند. این خود از دلایلی است که چرا مدیریت رابط بین برنامه‌های کاربردی شی‌گرا و پایگاه‌های داده بسیار دشوار است و به‌روزرسانی‌های ساختارهای داده عموما دردناک هستند. نمونه دیگری از نرم‌افزارهایی که در این ناحیه قرار دارند، کتابخانه خود ابزارها مثل String است. اجزای ثبات‌دار در ناحیه (0، 0) بی ضرر هستند زیرا احتمال تغییر آن‌ها وجود ندارد. هرچه یک کامپوننت در ناحیه درد احتمال تغییرش بیش‌تر باشد، «دردناک‌تر» است.

ناحیه‌ی بی‌فایدگی (Zone of Uselessness)

یک کامپوننت نزدیک (1، 1) را در نظر بگیرید. این مکان نامطلوب است زیرا حداکثر انتزاعی است، اما هیچ وابسته‌ای ندارد. چنین اجزایی بی‌فایده هستند. بنابراین این منطقه را منطقه بی‌فایده می‌نامند. موجودات نرم‌افزاری که در این منطقه زندگی می‌کنند اغلب کلاس‌های انتزاعی باقی‌مانده‌ای هستند که هیچ‌کس آن‌ها را پیاده‌سازی و استفاده نکرده است. بدیهی است که وجود چنین موجودات بی‌فایده‌ای نامطلوب است.

اجتناب از مناطق محروم

به نظر واضح است که فرارترین اجزای ما باید تا حد امکان از هر دو منطقه محرومیت دور نگه داشته شوند. مکان نقاطی که از هر ناحیه حداکثر فاصله دارند، خطی است که (1،0) و (0،1) را به هم متصل می‌کند. عمو باب این خط را دنباله‌ای اصلی (Main Sequence) می‌نامد. هر کامپوننتی که روی دنباله‌ی اصلی قرار می‌گیرد برای پایداری آن بیش از حد انتزاعی نیست و برای انتزاعی بودن، بیش از حد ناپایدار نیست. به عبارتی این کامپوننت نه بی‌فایده است و نه به خصوص دردناک. تا حدی که انتزاعی است به آن وابستگی‌هایی وجود دارد و به اندازه‌ای که عینی است به دیگران بستگی دارد. مطلوب‌ترین موقعیت برای یک کامپوننت در یکی از دو نقطه انتهایی دنباله‌ی اصلی است. معماران خوب تلاش می‌کنند تا اکثر کامپوننت‌های خود را در آن نقاط پایانی قرار دهند.

فاصله از دنباله‌ی اصلی (Main Sequence)

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

متریک فاصله (D) با رابطه‌ی |A+I-1| محاسبه می‌شود میزان نزدیکی به این خط را می‌سنجد. محدوده این متریک مقدار صفر تا یک است. مقدار صفر‌ نشان می‌دهد که کامپوننت مستقیما روی دنباله‌ی اصلی قرار دارد. مقدار یک نشان می دهد که کامپوننت تا حد امکان از دنباله‌ی اصلی دور است.

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

تجزیه و تحلیل آماری یک طرح نیز امکان‌پذیر است. میانگین و واریانس تمام معیارهای D مربوط به تمام کامپوننت‌های یک طرح قابل محاسبه می‌باشد. ما انتظار داریم که طراحی خوب دارای میانگین نزدیک به یک و واریانس نزدیک به صفر باشد. واریانس را می‌توان برای ایجاد «محدودیت های کنترلی» به منظور شناسایی اجزایی که در مقایسه با بقیه «استثنایی» هستند استفاده کرد.


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

معماری

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

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

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

این تسهیلات برای این است که تا آن‌جا که ممکن است گزینه‌های زیادی را باز بگذاریم تا زمانی که ممکن است.

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

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

به صورت خلاصه هر کدام از ابعاد معماری را در زیر بیان می‌کنیم.

توسعه

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

استقرار

برای مؤثر بودن، یک سیستم نرم‌افزاری باید قابل استقرار باشد. هر چه هزینه استقرار بیشتر باشد، سیستم کمتر مفید است. بنابراین، هدف یک معماری نرم‌افزاری باید ساختن سیستمی باشد که بتوان به راحتی با یک اقدام واحد آن را مستقر کرد. متأسفانه، استراتژی استقرار به ندرت در طول توسعه اولیه در نظر گرفته می‌شود. این منجر به معماری‌هایی می‌شود که ممکن است توسعه سیستم را آسان کند، اما استقرار آن را بسیار دشوار کند.

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

عملکرد

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

با این حال خوب است که معماری یک سیستم عملکرد سیستم را به راحتی برای توسعه‌دهندگان آشکار کند. این امر درک سیستم را ساده می‌کند و بنابراین به توسعه و نگهداری کمک زیادی می‌کند.

نگهداری

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


روشی که نرم‌افزار را نرم نگه می‌دارید این است که تا جایی که ممکن است گزینه‌های زیادی را باز بگذارید. گزینه‌هایی که باید باز بگذاریم چیست؟ آن‌ها جزئیاتی هستند که اهمیتی ندارند. تمام سیستم‌های نرم‌افزاری را می‌توان به دو عنصر اصلی تقسیم کرد: سیاست‌ها و جزئیات.

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

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

ان شا الله که مطالب بیان‌شده برای شما مفید باشد به گونه‌ای که شما را برای خواندن دیگر مطالب کتاب ترغیب نموده باشد. متن این کتاب روان بوده و تا حد خوبی مستقل از پیش‌زمینه‌ی فراوان در برنامه‌نویسی قابل فهمیدن و یادگیری می‌باشد. به امید این که مطالعه‌ی کتاب‌های مفید افزایش پیدا کند!

این مطلب، بخشی از تمرین‌های درس معماری نرم‌افزار در دانشگاه شهیدبهشتی است.


معماری_نرم_افزار_بهشتیمعماری تمیز
شاید از این پست‌ها خوشتان بیاید