تولید نرمافزار، یکی از خلاقانهترین فعالیتهای بشر در طول تاریخ است. برنامهنویسها، به محدودیتهایی مثل قوانین فیزیک مقید نیستند. آنها میتوانند دنیاهای مجازی بینظیری خلق کنند که هیچ وقت قبل از آن وجود نداشته است اما وجود پیچیدگی در نرمافزار را نمیتوان نادیده گرفت. برنامهنویسی نیاز به مهارتهای جسمانی یا تناسب فیزیکی خاصی ندارد. تمام آن چیزی که لازم است، یک ذهن خلاق و توانایی سازماندهی افکار است.
بنابراین، بزرگترین محدودیت در تولید نرمافزار، توانایی ما در فهم سیستمهایی است که در حال ساختن آنها هستیم. در طول زمان نرمافزار تکامل مییابد و ویژگیهای جدیدی به دست میآورد، وابستگی بین اجزاء آن، باعث ایجاد پیچیدگی است. رفته رفته، پیچیدگیها روی هم انباشته شده و برای برنامهنویسها، تغییر و نگهداری سیستم سختتر و سختتر میشود.
این رویه باعث میشود، فرایند توسعهی نرمافزارها آهستهتر پیش رود و منجر به تولید خطا میشود. این خطاها، باز هم روند تولید را آهستهتر میکنند و هزینهها را افرایش میدهند.
این که پیچیدگی، در طی پیدایش یک نرمافزار، به مرور بیشتر میشود، یک امر گریز ناپذیر است. هر چقدر نرمافزار بزرگتر باشد و تعداد افراد بیشتری روی آن کار کنند، مدیریت این پیچیدگی سختتر است. این پیچیدگی را نمیتوان از بین برد ولی میتوان آن را مدیریت کرد. اگر میخواهیم تولید نرمافزار را راحتتر و ارزانتر انجام دهیم، باید راههایی پیدا کنیم که سادگی را در نرمافزار بیشتر کنیم.
بنابراین، هر چه قدر هم که ما تلاش کنیم، با گذشت زمان، پیچیدگی بیشتر میشود، ولی اگر سادگی را در طراحی خود رعایت کنیم، میتوانیم سیستمهای بزرگتر و قدرتمندتری را به وجود آوریم، قبل از این که پیچیدگی غیر قابل کنترل شود.
چرا پیچیدگی در نرمافزار مهم است؟
نرمافزار، بر خلاف بسیاری از سازههای فیزیکی، مثل ساختمانها، کشتیها و پلها، شکلپذیر است. یعنی در طول زمان زندگی یک سیستم نرمافزاری، بارها و بارها ممکن است در طراحی آن تجدید نظر کنیم.
البته همیشه این طور نبوده است، بیشتر تاریخِ برنامهنویسی این گونه گذشته است که در شروعِ پروژه، تمرکز روی طراحی بود و در فازهای بعد روی پیادهسازی، تست و نگهداری. به این روش، روش آبشاری (waterfall) گفته میشود و برای سایر رشتههای مهندسی مناسب است اما در حوزه نرمافزار به ندرت خوب کار میکند. چرا که تجسم کردن و فهم همه ابعاد یک سیستم نرمافزاری بزرگ در ابتدای کار غیرممکن است و تا زمانی که نرمافزار عملیاتی و اجرا نشود، مشکلات طراحی اولیه، خود را نشان نمیدهند.
در نتیجه، در حال حاضر، بیشتر تیمهای نرمافزاری، از رویکرد چابک (Agile) بهره میبرند. در این روش، تمرکز اولیه روی طراحی یک بخش کوچک از تمام عملکردهای سیستم است. این بخش کوچک طراحی، پیادهسازی و ارزیابی میشود. زمانی که مشکلات این طراحی اولیه مشخص و برطرف شد، تعداد کمی از ویژگیهای دیگر پیادهسازی میشوند. در هر دوره زمانی (iteration)، مشکلات شناسایی و بر طرف میشوند. ویژگیهای جدید با توجه به تجربه کارهای قبلی طراحی میشوند. از آن جا که ویژگیهای جدید بر پایهی تجربۀ ویژگیهای قبلی و مشکلات پیش آمده به وجود میآیند، مشکلات کمتری خواهند داشت.
قطعا این رویکرد، برای سازههای فیزیکی مناسب نیست. مثلا در میانه ساختن یک پل، نمیتوانیم تعداد پایههای آن را تغییر دهیم. ولی در نرمافزار، به علت انعطافپذیریِ ذاتی آن، بهترین رویکرد ممکن است.
بنابراین، اگر قرار است در بازه زمانی ساخت یک نرمافزار، به صورت مرتب، در طراحی آن تجدید نظر کنیم، کاهش پیچیدگی، یکی از موارد مهم است که باید در ذهن داشته باشیم، در غیر این صورت، ممکن است ناگهان با یک سیستم خیلی پیچیده مواجه شویم که نگهداری آن اگر غیرممکن نباشد به سختی امکانپذیر است.
پیچیدگی در نرمافزار چیست؟
پیچیدگی، رفتار یک سیستم یا مدل را، که دارای اجزای متعددی است و این اجزا به چندین روش با هم ارتباط دارند، توصیف میکند. به عنوان مثال مغز انسان یک سیستم پیچیده است، چرا که از میلیونها نورون تشکیل شده و با هم در ارتباط هستند. همچنین سیستمهای حملونقل، سازمانهای اقتصادی و در نهایت جهان به صورت یک کل، سیستم پیچیدهای است.
در سال ۱۹۸۷، Fred Brooks در مقالهای با عنوان No Silver Bullet دو نوع پیچیدگی را در نرمافزار مطرح کرد:
- پیچیدگی ذاتی یا ضروری (Essential complexity):
این پیچیدگی، در ذات مسالهای که میخواهیم آن را حل کنیم وجود دارد و قابل حذف شدن نیست. اگر یک قطعه از کد، باید شامل ۳ عملکرد باشد، به اندازه این ۳ عملکرد پیچیدگی خواهد داشت و نمیتوانیم مثلا با حذف یکی از این عملکردها از پیچیدگی بکاهیم.
- پیچیدگی تصادفی (Accidental complexity)
این نوع از پیچیدگی ناشی از مشکلاتی است که خود برنامهنویسها باعث آن میشوند. این پیچیدگی را میتوان کمتر یا حذف کرد.
در این مقاله، Brooks معتقد است که مقدار زیادی از پیچیدگیهای تصادفی نرمافزارها، با روی کار آمدن زبانهای سطح بالای برنامهنویسی و معرفی طراحی شیگرا، حذف شده است.
در کتاب A philosophy of software design پیچیدگی به این صورت تعریف شده است:
«پیچیدگی، هر چیز مرتبط با یک سیستم نرمافزاری است، که درک و تغییر سیستم را سخت میکند.»
برای مثال، ممکن است فهم این که یک قطعه از کد چگونه کار میکند، دشوار باشد یا برای بهبود یک بخش کوچک از سیستم نیاز به تلاش بسیاری باشد یا شاید اصلاح یک خطا، بدون دستکاری سایر قسمتهای سیستم امکانپذیر نباشد.از منظر دیگر میتوان به عنوان هزینه و سود به این مساله نگاه کرد. در یک سیستم پیچیده، هزینه زیادی برای یک تغییر کوچک در سیستم پرداخت میشود. در یک سیستم ساده، تغییرات بزرگ با هزینه کم اعمال میشوند.
پیچیدگی به اندازه سیستم ارتباط مستقیم ندارد. ممکن است یک سیستم بسیار کوچک، بسیار پیچیده طراحی شود یا یک سیستم بزرگ به سادهترین حالت ممکن. همچنین اگر در یک سیستم بزرگ، بخشهای کوچکی که خیلی هم مورد استفاده نیستند پیچیدگی زیادی داشته باشند، نمیتوان گفت کل سیستم پیچیده است. در واقع میتوان گفت پیچیدگی کل سیستم، شامل مجموع پیچیدگیهای بخشهای مختلف سیستم است، که وزن آن بخشها در این مجموع لحاظ شده است.
نشانههای پیچیدگی در نرمافزار چیست؟
پیچیدگی در نرمافزار، خود را به سه شکل نشان میدهد:
- تشدید تغییر (change amplification)
اولین نشانه پیچیدگی این است که تغییراتِ خیلی ساده، نیاز به اصلاحات گسترده در کد دارد. به این امر rigidity گفته میشود به این معنا که نرمافزار سخت و صلب است و هر تغییری در آن به دشواری انجام میشود. مفهوم دیگری که به این مساله مرتبط است، شکنندگی (fragility) نرمافزار است. شکن��ده بودن نرمافزار به این معنی است که با تغییر یک قسمت از کد، بخشهای دیگری که هیچ ارتباط منطقی با کد تغییر داده شده ندارند، دچار خطا میشوند. اگر نرمافزار شکنندگی بالایی داشته باشد، هر خطایی که اصلاح میشود باعث به وجود آمدن چندین خطای دیگر شده و به نظر میرسد که اصلاح نکردن خطاها به صرفهتر است! - بار شناختی (cognitive load)
بار شناختی یعنی این که یک برنامه نویس، برای انجام دادن یک تسک، چقدر باید بداند. اگر نرمافزار طوری طراحی شده باشد که برای انجام یک کار ساده، افراد ناچار شوند بخشهای زیادی از نرمافزار را کشف کنند و نسبت به آنها دانش به دست آورند، این نشانهای از پیچیدگی بالا است.
بار شناختی (Cognitive load) عبارتی است که در روانشناسی شناختی استفاده میشود و مفهوم آن، میزان استفاده از حافظهی کاری (Working memory) در زمان پردازش اطلاعات است. حافظه کاری یک حافظهی کوتاهمدت است، که میتواند ۴ تا ۵ آیتم را حداکثر برای ۱۰ثانیه نگه دارد.
طراحی خوب در نرمافزار، با محدود کردن میزان مفاهیمی که باید در حافظهی کاری خود نگه دارید، باعث میشود ایجاد تغییرات در کد سادهتر باشد. در مقابل آن طراحیهای پیچیده است که تغییرات را بسیار دشوار میکنند.
به عنوان نمونه ای از طراحی بد، اگر برای تغییر یک کلاس باید ۹ کلاس دیگر را که از آن استفاده می کنند را بفهمید، نگه داشتن این همه اطلاعات در حافظه کاری شما بسیار دشوار و احتمال این که اشتباه کنید زیاد است.
یک روش در طراحی که بار شناختی را کم میکند، کپسولهسازی (encapsulation) است. میتوانیم با استفاده از abstraction و interface ها، پیچیدگیهای پیادهسازی را مخفی کنیم. در نتیجه برای تغییر کدها، نیازی به دانستن این که client چگونه از این interface استفاده میکند یا نیازی به دانستن جزئیات پیادهسازی این abstraction ها نداریم. پس مغز ما فقط اطلاعات مهم را پردازش میکند و از ظرفیت محدود حافظه کاری، به بهترین شکل استفاده میشود.
یک اشتباه رایج این است که پیچیدگی میتواند با توجه به تعداد خطهای کد اندازهگیری شود و اگر یک پیادهسازی، از پیادهسازی دیگر کوتاهتر است (بر اساس تعداد خط) پس پیچیدگی کمتری دارد اما این فرضیه، هزینهی بار شناختی را در نظر نمیگیرد. گاهی اوقات برای یک قطعه کد، تمام سعی خود را میکنیم که تعداد خطوط کمتری بنویسیم، ولی در نهایت فهم آن قطعه کد سختتر میشود، در واقع، قابلیت خوانایی کد (readability) کمتر میشود.
برای نمونه این کد:
const firstNumb = 100;
let secondNumb;
const secondNumb = firstNumb > 50 ? “Number is greater than 50” : “Number is less than 50”
در مقایسه با این کد:
const firstNumb = 100;
let secondNumb;
if (firstNumb > 50) {
secondNumb = “Number is greater than 50”
} else {
secondNumb = “Number is less than 50”
}
کد دوم، با وجود تعداد خطهای بیشتر، قابلیت خوانایی بالاتری داشته و فهم آن بار شناختی کمتری دارد.
- ندانستههای ناشناخته (unknown unknowns)
سومین نشانه پیچیدگی این است که قسمتهایی از کد که باید برای انجام یک تسک مشخص تغییر کنند، واضح نیست. در واقع تا زمانی که نرمافزار بعد از اعمال یک تغییر، دچار خطا نشود، هیچ کس متوجه این ناشناختهها نخواهد شد.
شاید بحث دانستهها و ندانستهها، در ابتدا جمله معروف سقراط را به یاد ما میآورد:
دانم که ندانم!
اما برای اولین بار، ابن یمین، در این شعر به این مساله پرداخت:
آنکس که بداند و بداند که بداند / اسب شرف از گنبد گردون بجهاند (known knowns)
آنکس که بداند و نداند که بداند / با کوزه آب است ولی تشنه بماند (unknown knowns)
آنکس که نداند و بداند که نداند / لنگان خرک خویش به مقصد برساند (known unknowns)
آنکس که نداند و نداند که نداند / در جهل مرکب ابدالدهر بماند (unknown unknowns)
دقت کنید که این مورد، با مورد اول یعنی تشدید تغییر تفاوت دارد. در مورد اول، ما میدانیم که یک تغییر، باعث تغییر در چه بخشهای دیگری میشود، تمام آن تغییرات (که تعدادشان ممکن است خیلی زیاد باشد) را اعمال میکنیم و سیستم به درستی کار میکند ولی در این مورد یعنی ندانستههای ناشناخته، حتی نمیدانیم کدام بخشها از این تغییر متاثر میشوند و تنها زمانی میفهمیم که خطایی رخ دهد.
علتهای به وجود آمدن پیچیدگی چیست؟
حال که نشانههای پیچیدگی را شناختیم، ببینیم علت به وجود آمدن آن چیست:
- وابستگیها
در این جا منظور از وابستگی، هر نوع ارتباط بین یک کد با کد دیگر است، به این صورت که برای تغییر در یکی، دیگری هم باید تغییر کند. به عنوان مثال، signature یک متد بین کدی است که متد را تعریف کرده و برای کدی که آن را استفاده میکند، وابستگی به وجود میآورد و مثلا اگر یک پارامتر اجباری به تعریف متد اضافه شود، تمام کدهایی که از آن استفاده میکنند باید تغییر کنند. همچنین اگر دو قطعه کد به صورت فرستنده و گیرنده عمل کنند، پروتکل ارتباطی بین آنها یک وابستگی است. اگر فرستنده تصمیم به تغییر پروتکل بگیرد، گیرنده هم باید تغییر کند.
قطعا وابستگیها بخشی از نرم افزار هستند و نمیتوانیم آنها را از بین ببریم. هدف ما به عنوان طراح نرمافزار باید این باشد که تا حد ممکن وابستگیها را کم و همچنین آنها را ساده و واضح طراحی کنیم.
وابستگیها معمولا باعث «تشدید تغییر» و «بار شناختی» بالا میشوند. - ابهام
ابهام زمانی اتفاق میافتد که اطلاعات مهم، واضح و روشن نباشد. مثلا ممکن است نام یک متغیر طوری تعریف شود که با خواندن آن هیچ اطلاعات مفیدی درباره آن متغیر به دست نیاورده باشیم.
ابهام به وابستگی هم ارتباط دارد، وقتی که وجود یک وابستگی، واضح نیست. مثلا اگر یک نوع پیام جدید به سیستم اضافه شود ولی برنامه نویس هیچ جا به متن آن پیام دسترسی نداشته باشد.
یک روشِ اشتباه برای کم کردن ابهام در نرمافزار، مستندسازی است. معمولا اگر در طراحی نرمافزار، نیاز به مستندسازیهای پرهزینه و زیاد داریم، یعنی طراحی نرمافزار به درستی انجام نشده است، یک طراحی خوب، قطعا به مستندسازی کمتری نیاز خواهد داشت.
ابهام در نرمافزار، باعث ایجاد «ندانستههای ناشناخته» و «بار شناختی» زیاد میشود.
جمعبندی
پیچیدگی، چیزی نیست که با یک اشتباه بزرگ در سیستم ایجاد شود بلکه نیازمند انباشته شدن مسائل کوچک زیادی روی هم است. یک وابستگی یا یک ابهام نمیتواند تاثیر زیادی روی قابلیت نگهداری سیستم داشته باشد. پیچیدگی وقتی زیاد میشود که صدها و هزاران وابستگی و ابهام کوچک در طول زمان در سیستم ایجاد شوند. به همین دلیل مدیریت پیچیدگی سخت میشود. هر برنامهنویس فرض میکند که این تکه کد کوچک که در حال نوشتن آن است، اگر هم دارای ابهام باشد یا وابستگی غیرضروری به سیستم تحمیل کند، مشکلی ایجاد نخواهد کرد. ولی مجموع کار برنامهنویسهای مختلف که با این رویکرد سیستم را توسعه میدهند، میتواند تبدیل به فاجعه شود.
دیدگاهتان را بنویسید