پیچیدگی در نرم افزار

تولید نرم افزار، یکی از خلاقانه ترین فعالیت های بشر در طول تاریخ است. برنامه نویس ها، به محدودیت هایی مثل قوانین فیزیک مقید نیستند. آن ها می توانند دنیاهای مجازی بی نظیری خلق کنند که هیچ وقت قبل از آن وجود نداشته است. برنامه نویسی نیاز به مهارت های جسمانی یا تناسب فیزیکی خاصی ندارد. تمام آن چیزی که لازم است، یک ذهن خلاق و توانایی سازماندهیِ افکار است.بنابراین، بزرگ ترین محدودیت در تولید نرم افزار، توانایی ما در فهم سیستم هایی است که در حال ساختن آنها هستیم. در طول زمان که نرم افزار تکامل می یابد و ویژگی های جدید به دست می آورد، وابستگی بین اجزاء آن، باعث ایجاد پیچیدگی می شود. رفته رفته، پیچیدگی ها روی هم انباشته می شود و برای برنامه نویس ها، تغییر و نگهداری سیستم سخت تر و سخت تر می شود.این رویه باعث می شود، توسعه ی نرم افزار آهسته تر پیش رود و منجر به تولید خطا می شود. این خطاها، باز هم روند تولید را آهسته تر می کنند و هزینه ها را بالا می برند.این که پیچیدگی، در طی پیدایش یک نرم افزار، به مرور بیشتر می شود، یک امر ناگزیر است. هر چقدر نرم افزار بزرگ تر باشد و تعداد افراد بیشتری روی آن کار کنند، مدیریت این پیچیدگی سخت تر است. این پیچیدگی را نمی توان از بین برد ولی می توان آن را مدیریت کرد. اگر می خواهیم تولید نرم افزار را راحت تر و ارزان تر انجام دهیم، باید راه هایی پیدا کنیم که سادگی را در نرم افزار بیشتر کنیم.بنابراین، هر چه قدر هم که ما تلاش کنیم، در طول زمان، پیچیدگی بیشتر می شود، ولی اگر سادگی را در طراحی خود رعایت کنیم، می توانیم سیستم های بزرگ تر و قدرتمند تری را به وجود آوریم، قبل از این که پیچیدگی غیر قابل کنترل شود.چرا پیچیدگی در نرم افزار مهم است؟نرم افزار، بر خلاف بسیاری از سازه های فیزیکی، مثل ساختمان ها، کشتی ها و پل ها، شکل پذیر است. یعنی در طول زمان زندگی یک سیستم نرم افزاری، بارها و بارها ممکن است در طراحی آن تجدید نظر کنیم.البته همیشه اینطور نبوده، بیشتر تاریخِ برنامه نویسی اینگونه گذشته است که، در شروعِ پروژه، تمرکز روی طراحی بود و در فازهای بعد روی پیاده سازی، تست و نگهداری. به این روش، روش آبشاری (waterfall) گفته می شود و برای سایر رشته های مهندسی مناسب است، اما در حوزه ی نرم افزار به ندرت خوب کار می کند. چرا که تجسم کردن و فهم همه ی ابعادِ یک سیستم نرم افزاری بزرگ، در ابتدای کار، غیر ممکن است و تا زمانی که نرم افزار عملیاتی و اجرا نشود، مشکلاتِ طراحی اولیه، خود را نشان نمی دهند.در نتیجه، در حال حاضر، بیشتر تیم های نرم افزاری، از رویکرد چابک (Agile) بهره می برند. در این روش، تمرکز اولیه، روی طراحی یک بخش کوچک از تمام عملکردهای سیستم است. این بخش کوچک طراحی، پیاده سازی و ارزیابی می شود. زمانی که مشکلات این طراحی اولیه، مشخص و برطرف شد، تعداد کمی ویژگی های دیگر پیاده سازی می شوند. در هر دوره ی زمانی (iteration)، مشکلات شناسایی شده، برطرف می شوند و ویژگی های جدید با توجه به تجربه های کارهای قبلی طراحی می شوند. از آنجا که ویژگی های جدید بر پایه ی تجربه ی ویژگی های قبلی و مشکلاتِ پیش آمده به وجود می آیند، مشکلات کمتری خواهند داشت.قطعا این رویکرد، برای سازه های فیزیکی مناسب نیست. مثلا در میانه ی ساختن یک پل، نمی توانیم تعداد پایه های آن را تغییر دهیم. ولی در نرم افزار، به علت انعطاف پذیریِ ذاتی آن، بهترین رویکردِ ممکن است.بنابراین، اگر قرار است در بازه ی زمانیِ ساختِ یک نرم افزار، به صورت مرتب، در طراحی آن تجدید نظر کنیم، کاهش پیچیدگی، یکی از موارد مهم است که باید در ذهن داشته باشیم، در غیر این صورت، ممکن است ناگهان با یک سیستم خیلی پیچیده مواجه شویم که نگهداری آن اگر غیرممکن نباشد به سختی امکان پذیر است.پیچیدگی چیست؟پیچیدگی، رفتار یک سیستم یا مدل را، که دارای اجزای متعددی است و این اجزا به چندین روش با هم ارتباط دارند، توصیف می کند. به عنوان مثال مغز انسان یک سیستم پیچیده است، چرا که از میلیون ها نورون تشکیل شده که با هم در ارتباط هستند. همچنین سیستم های حمل و نقل، سازمان های اقتصادی و در نهایت جهان به صورت یک کل، سیستم های پیچیده هستند.در سال 1987، Fred Brooks در مقاله ای با عنوان No Silver Bullet دو نوع پیچیدگی را در نرم افزار مطرح کرد:پیچیدگی ذاتی یا ضروری (Essential complexity)این پیچیدگی، در ذات مساله ای که می خواهیم آن را حل کنیم وجود دارد و قابل حذف شدن نیست. اگر یک قطعه از کد، باید شامل 3 عملکرد باشد، به اندازه ی این 3 عملکرد پیچیدگی خواهد داشت و نمی توانیم مثلا با حذف یکی از این عملکرد ها از پیچیدگی بکاهیم.پیچیدگی تصادفی (Accidental complexity)این نوع از پیچیدگی ناشی از مشکلاتی است که خود برنامه نویس ها باعث می شوند. این پیچیدگی را می توان کمتر یا حذف کرد.در این مقاله، Brooks معتقد است که مقدار زیادی از پیچیدگی های تصادفی نرم افزارها، با روی کار آمدن زبان های سطح بالای برنامه نویسی و معرفی طراحی شی گرا، حذف شده است.در کتاب A philosophy of software design پیچیدگی به این صورت تعریف شده:پیچیدگی، هر چیز مرتبط با یک سیستم نرم افزاری است، که درک و تغییر سیستم را سخت می کند.برای مثال، ممکن است فهم این که یک قطعه از کد چگونه کار می کند دشوار باشد. یا برای بهبود یک بخش کوچک از سیستم نیاز به تلاش بسیار باشد. یا شاید اصلاح یک خطا، بدون دستکاری سایر قسمت های سیستم امکان پذیر نباشد.از منظر دیگر می توان به عنوان هزینه و سود به این مساله نگاه کرد. در یک سیستم پیچیده، هزینه ی زیادی برای یک تغییر کوچک در سیستم پرداخت می شود. در یک سیستم ساده، تغییرات بزرگ با هزینه ی کم اعمال می شوند.پیچیدگی به اندازه ی سیستم ارتباط مستقیم ندارد. ممکن است یک سیستم بسیار کوچک، بسیار پیچیده طراحی شود و یا یک سیستم بزرگ به ساده ترین حالت ممکن. همچنین اگر در یک سیستم بزرگ، بخش های کوچکی که خیلی هم مورد استفاده نیستند پیچیدگی زیادی داشته باشند، نمی توان گفت کل سیستم پیچیده است. در واقع می توان گفت پیچیدگی کل سیستم، شامل مجموعِ پیچیدگی های بخش های مختلف سیستم است، که وزن آن بخش ها در این مجموع لحاظ شده است.نشانه های پیچیدگیپیچیدگی در نرم افزار خودش را به سه شکل نشان می دهد:تشدید تغییر (change amplification) اولین نشانه ی پیچیدگی این است که تغییراتِ خیلی ساده، نیاز به اصلاحات گسترده در کد دارد. به این امر rigidity گفته می شود به این معنا که نرم افزار سخت و صلب است و هر تغییری در آن به دشواری انجام می شود. مفهوم دیگری که به این مساله مرتبط است، شکنندگی (fragility) نرم افزار است. شکننده بودن نرم افزار به این معنی است که با تغییر یک قسمت از کد، بخش های دیگری که هیچ ارتباط منطقی با کدِ تغییر داده شده ندارند، دچار خطا می شوند. اگر نرم افزار شکنندگی بالایی داشته باشد، هر خطایی که اصلاح می شود باعث به وجود آمدن چندین خطای دیگر شده و به نظر می رسد که عدم اصلاح خطاها به صرفه تر است!بارِ شناختی (cognitive load)بارِ شناختی یعنی این که یک برنامه نویس، برای انجام دادن یک تسک، چقدر باید بداند. اگر نرم افزار طوری طراحی شده باشد، که برای انجام یک کار ساده، افراد ناچار شوند بخش های زیادی از نرم افزار را کشف کنند و نسبت به آن ها دانش به دست آورند، این نشانه ای از پیچیدگی بالاست.بار شناختی (Cognitive load) عبارتی است که در روانشناسی شناختی استفاده می شود و مفهوم آن، میزان استفاده از حافظه ی کاری (Working memory) در زمان پردازش اطلاعات است. حافظه ی کاری یک حافظه ی کوتاه مدت است، که می تواند 4 تا 5 آیتم را حداکثر برای 10 ثانیه نگه دارد.طراحی خوب در نرم افزار، با محدود کردن میزان مفاهیمی که باید در حافظه ی کاری خود نگه دارید، باعث می شود ایجاد تغییرات در کد ساده تر باشد. در مقابل آن طراحی های پیچیده هستند که تغییرات را بسیار دشوار می کنند. به عنوان نمونه ای از طراحی بد، اگر برای تغییر یک کلاس، باید 9 کلاس دیگر را که از آن استفاده می کنند را بفهمید، نگه داشتن این همه اطلاعات در حافظه ی کاری شما بسیار دشوار است و احتمال این که اشتباه کنید زیاد است.یک روش در طراحی که بار شناختی را کم می کند، کپسوله سازی (encapsulation) است. می توانیم با استفاده از abstraction و interface ها، پیچیدگی های پیاده سازی را مخفی کنیم. در نتیجه برای تغییر کدها، نیازی به دانستن این که client چکونه از این interface استفاده می کند، یا نیازی به دانستن جزئیات پیاده سازی این abstraction ها نداریم. پس مغز ما فقط اطلاعات مهم را پردازش می کند و از ظرفیتِ محدودِ حافظه ی کاری، به بهترین شکل استفاده می شود.یک اشتباه رایج این است که، پیچیدگی می تواند با توجه به تعداد خط های کد اندازه گیری شود و اگر یک پیاده سازی، از پیاده سازی دیگر کوتاه تر است (بر اساس تعداد خط) پس پیچیدگی کمتری دارد. اما این فرضیه، هزینه ی بارِ شناختی را در نظر نمی گیرد. گاهی اوقات برای یک قطعه کد، تمام سعی خود را می کنیم که تعداد خطوط کمتری بنویسیم، ولی در نهایت فهم آن قطعه کد سخت تر می شود، در واقع، قابلیت خوانایی کد (readability) کمتر می شود.برای نمونه این کد:const firstNumb = 100; let secondNumb; const secondNumb = firstNumb > 50 ? &quotNumber is greater than 50&quot : &quotNumber is less than 50&quotدر مقایسه با این کد:const firstNumb = 100; let secondNumb; if (firstNumb > 50) { secondNumb = &quotNumber is greater than 50&quot } else { secondNumb = &quotNumber is less than 50&quot }کد دوم، با وجود تعداد خط های بیشتر، قابلیت خوانایی بالاتری دارد و فهم آن بار شناختی کمتری دارد.ندانسته های ناشناخته (unknown unknowns)سومین نشانه ی پیچیدگی این است که قسمت هایی از کد که باید برای انجام یک تسک مشخص تغییر کنند، واضح نیست. در واقع تا زمانی که نرم افزار بعد از اعمال یک تغییر، دچار خطا نشود، هیچ کس متوجه این ناشناخته ها نمی شود.شاید بحث دانسته ها و ندانسته ها، در ابتدا جمله ی معروف سقراط را به یاد ما می آورد:دانم که ندانم!اما برای اولین بار، ابن یمین، در این شعر به این مساله پرداخت: آنکس که بداند و بداند که بداند / اسب شرف از گنبد گردون بجهاند (known knowns) آنکس که بداند و نداند که بداند / با کوزه ی آب است ولی تشنه بماند (unknown knowns)آنکس که نداند و بداند که نداند / لنگان خرک خویش به مقصد برساند (known unknowns)آنکس که نداند و نداند که نداند / در جهل مرکب ابدالدهر بماند (unknown unknowns)دقت کنید که این مورد، با مورد اول یعنی تشدید تغییر تفاوت دارد. در مورد اول، ما می دانیم که یک تغییر، باعث تغییر در چه بخش های دیگری می شود، تمام آن تغییرات (که تعدادشان ممکن است خیلی زیاد باشد) را اعمال می کنیم و سیستم به درستی کار می کند. ولی در این مورد یعنی ندانسته های ناشناخته، حتی نمی دانیم کدام بخش ها از این تغییر متاثر می شوند و تنها زمانی می فهمیم که خطایی رخ بدهد.علت های پیچیدگیحال که نشانه های پیچیدگی را شناختیم، ببینیم چه چیزی باعث آن می شود:وابستگی هادر اینجا منظور از وابستگی، هر نوع ارتباط بین یک کد با کد دیگر است، به این صورت که برای تغییر در یکی، دیگری هم باید تغییر کند. به عنوان مثال، signature یک متد، بین کدی که متد را تعریف کرده و کدی که آن را استفاده می کند، وابستگی به وجود می آورد و مثلا اگر یک پارامترِ اجباری به تعریف متد اضافه شود، تمام کدهایی که از آن استفاده می کنند باید تغییر کنند. همچنین اگر دو قطعه کد به صورت فرستنده و گیرنده عمل کنند، پروتکل ارتباطی بین آن ها یک وابستگی است. اگر فرستنده تصمیم به تغییر پروتکل بگیرد، گیرنده هم باید تغییر کند.قطعا وابستگی ها بخشی از نرم افزار هستند و نمی توانیم آن ها را از بین ببریم. هدف ما به عنوان طراح نرم افزار باید این باشد که تا حد ممکن وابستگی ها را کم کنیم و همچنین آن ها را ساده و واضح طراحی کنیم.وابستگی ها معمولا باعث "تشدید تغییر" و "بار شناختی" بالا می شوند.ابهامابهام زمانی اتفاق می افتد که اطلاعات مهم، واضح و روشن نباشد. مثلا ممکن است نام یک متغیر طوری تعریف شود که با خواندن آن هیچ اطلاعات مفیدی درباره ی آن متغیر به دست نیاوریم. ابهام به وابستگی هم ارتباط دارد، وقتی که وجود یک وابستگی، واضح نیست. مثلا اگر یک نوع پیام جدید به سیستم اضافه شود، ولی برنامه نویس هیچ جا به متن آن پیام دسترسی نداشته باشد.یک روشِ اشتباه برای کم کردن ابهام در نرم افزار، مستندسازی است. معمولا اگر در طراحی نرم افزار، نیاز به مستندسازی های پرهزینه و زیاد داریم، یعنی طراحی نرم افزار به درستی انجام نشده است، یک طراحیِ خوب، قطعا به مستندسازی کمتری نیاز خواهد داشت.ابهام در نرم افزار، باعث ایجاد "ندانسته های ناشناخته" و "بار شناختی" زیاد می شود.خلاصهپیچیدگی، چیزی نیست که با یک اشتباه بزرگ در سیستم ایجاد شود، بلکه نیازمند انباشته شدن مسائل کوچک زیادی روی هم است. یک وابستگی یا یک ابهام نمی تواند تاثیر زیادی روی قابلیت نگهداری سیستم داشته باشد. پیچیدگی وقتی زیاد می شود که صدها و هزاران وابستگی و ابهام کوچک در طول زمان در سیستم ایجاد شوند. به همین علت مدیریت پیچیدگی سخت می شود. هر برنامه نویس فرض می کند که این تکه کد کوچک که در حال نوشتن آن است، اگر هم دارای ابهام باشد یا وابستگی غیر ضروری به سیستم تحمیل کند، مشکلی ایجاد نخواهد کرد. ولی مجموع کار برنامه نویس های مختلف که با این رویکرد سیستم را توسعه می دهند، می تواند تبدیل به فاجعه شود.منابعA philosophy of software designNo Silver Bullet