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

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

2.1 تعریف پیچیدگی

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

در یک سیستم ساده، بهبودهای بزرگتر را می‌توان با تلاش کمتر پیاده‌سازی کرد. پیچیدگی چیزی است که یک توسعه‌دهنده در یک نقطه خاص زمانی زمانی که سعی دارد به هدف خاصی برسد، تجربه می‌کند. پیچیدگی لزوماً به اندازه یا عملکرد کلی سیستم مرتبط نیست. مردم اغلب از واژه “پیچیده” برای توصیف سیستم‌های بزرگ با ویژگی‌های پیچیده استفاده می‌کنند، اما اگر چنین سیستمی کار کردن با آن آسان باشد، برای اهداف این کتاب پیچیده نیست. البته تقریباً همه سیستم‌های بزرگ و پیچیده نرم‌افزاری در واقع کار کردن با آنها دشوار است، بنابراین آنها نیز با تعریف من از پیچیدگی تطابق دارند، اما این لزوماً نباید اینگونه باشد. همچنین ممکن است یک سیستم کوچک و ساده نیز بسیار پیچیده باشد. پیچیدگی بر اساس فعالیت‌هایی که بیشترین فراوانی را دارند، تعیین می‌شود. اگر یک سیستم چند قسمت پیچیده داشته باشد، اما آن قسمت‌ها تقریباً هیچ‌وقت نیاز به تغییر نداشته باشند، تأثیر زیادی بر پیچیدگی کلی سیستم نخواهند داشت. پیچیدگی کلی یک سیستم (C) توسط پیچیدگی هر قسمت p (cp) تعیین می‌شود که وزن آن با توجه به درصد زمانی که توسعه‌دهندگان صرف کار بر روی آن قسمت (tp) می‌کنند، محاسبه می‌شود.

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

2.2 علائم پیچیدگی

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

افزایش تغییرات

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

بار شناختی

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

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

Figure 2.1

Figure 2.1

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

ناشناخته‌های ناشناخته

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

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

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

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

3.2 علل پیچیدگی

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

در مثال وب‌سایت شکل 2.1(a)، رنگ پس‌زمینه وابستگی‌هایی بین تمام صفحات ایجاد می‌کند. تمام صفحات باید همان پس‌زمینه را داشته باشند، بنابراین اگر رنگ پس‌زمینه برای یک صفحه تغییر یابد، باید برای همه صفحات تغییر یابد. مثال دیگری از وابستگی‌ها در پروتکل‌های شبکه رخ می‌دهد. معمولاً کد جداگانه‌ای برای فرستنده و گیرنده پروتکل وجود دارد، اما هر دو باید به پروتکل پایبند باشند؛ تغییر کد فرستنده تقریباً همیشه نیاز به تغییرات متناظر در گیرنده دارد و بالعکس. امضای یک روش وابستگی‌ای بین پیاده‌سازی آن روش و کدی که آن را فراخوانی می‌کند ایجاد می‌کند: اگر یک پارامتر جدید به یک روش اضافه شود، تمام فراخوانی‌های آن روش باید اصلاح شوند تا آن پارامتر جدید را مشخص کنند. وابستگی‌ها بخش اصلی نرم‌افزار هستند و نمی‌توان آنها را کاملاً حذف کرد. در واقع، ما وابستگی‌ها را به‌طور عمدی در فرآیند طراحی نرم‌افزار وارد می‌کنیم. هر بار که یک کلاس جدید می‌نویسید، وابستگی‌هایی حول API آن کلاس ایجاد می‌کنید.

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

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

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

2.4 پیچیدگی افزایشی است

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

2.5 نتیجه‌گیری

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