کد تمیز کدی است که خواندن، فهمیدن و تغییر آن راحت است. درست است که نوشتن کد تمیز بیشتر یک هنر است تا یک علم، اما صنعت مهندسی نرمافزار بر اصول متعددی توافق کرده است که اگر رعایت شوند، به نوشتن کد تمیزتر کمک میکنند. در این فصل، با ۱۷ اصل برای نوشتن کد تمیز آشنا میشویم.
دقت کنید که این مطلب ترجمهی خط به خط کتاب نیست و فقط قسمتهای مهم آن آورده شده است.
شاید دربارهی تفاوت بین کد ساده (simple) و تمیز (clean) کنجکاو باشید. این دو مفهوم بسیار به هم مرتبط هستند چون کد تمیز ساده است و کد ساده تمیز است. اما ممکن است با کد پیچیدهای مواجه شوید که تمیز است. سادگی در مورد این است که از پیچیدگی دوری کنیم. تمیزی یک قدم فراتر از این میرود و به مدیریت پیچیدگی اجتناب ناپذیر نیز میپردازد. برای مثال، این کار را با استفادهی موثر از کامنتها و استانداردها انجام دهیم.ب
در فصلهای قبلی خواندیم که دشمن شماره یک هر پروژهی نرمافزاری، پیچیدگی است. کد تمیز، هم برای خود آیندهتان و هم برای دیگر برنامهنویسها، قابل فهمتر است. احتمال اینکه افراد در کد تمیز مشارکت داشته باشند و به آن اضافه کنند، بیشتر است. کد تمیز از هزینههای یک پروژه نیز کم میکند. همانطور که Robert C. Martin در کتاب Clean Code به این نکته اشاره میکند که بسیاری از برنامهنویسها بیشتر زمان خود را صرف خواندن کدهای قدیمی میکنند تا بتوانند کد جدید بنویسند. اگر کد قدیمی را راحت بتوان خواند، پس سرعت فرآیند نوشتن کد جدید بیشتر خواهد بود
نسبت زمانی که کد میخوانیم به زمانی که کد مینویسیم بیش از ۱۰ به ۱ است. ما مدام در حال خواندن کد قدیمی هستیم تا کد جدید بنویسیم. پس اگر راحت بتوان کد را خواند راحت هم میتوان کد نوشت.
اگر این نسبت را در نظر بگیریم، شکل زیر به دست میآید. محور x نشان دهندهی تعداد خط کدی است که در یک پروژهی نرمافزاری نوشته شده است. محور y نشان دهندهی زمان مورد نیاز برای نوشتن یک خط اضافه است. در کل، هر چقدر کد بیشتری در یک پروژه نوشته باشید، به زمان بیشتری برای اضافه کردن به آن کد احتیاج خواهید داشت. این هم برای کد تمیز و هم برای کد کثیف صادق است.
فرض کنید n خط کد نوشتهاید و n+1 امین خط را به کد اضافه میکنید. اضافه کردن این خط احتمالا تمام خطهایی را که قبلا نوشتهاید، تحت تاثیر قرار خواهد داد. برای مثال ممکن است کمی از پرفورمنس بکاهد که یعنی پرفورمنس تمام پروژه را کم میکند. ممکن است از یک متغیر که در جای دیگری معرفی شده استفاده کند. میتواند یک باگ با احتمال c اضافه کند که برای پیدا کردن آن باید تمام پروژه را جستجو کنید. این یعنی زمان تخمینی و هزینهی شما به ازای هر خط کد برابر c * T(n) است که T تابع افزایشی پیوسته با ورودی افزایشی n است.
این شکل همچنین تفاوت بین نوشتن کد تمیز و کثیف را نشان میدهد. کد کثیف در کوتاه مدت و در پروژههای کوچک، زمان کمتری میگیرد؛ به هر حال اگر فایدهای نداشت که کسی کد کثیف نمینوشت! اگر تمام قابلیتهای پروژهی خود را در ۱۰۰ خط جمع کنید، نیازی به صرف برای فکر کردن و بازسازی پروژهی خود ندارید. مشکلها زمانی شروع میشوند که کد بیشتری اضافه میکنید. وقتی تک فایل پروژهی شما بین ۱۰۰ تا ۱۰۰۰ خط کد دارد، به نسبت زمانی که در ماژولها، کلاسها و فایلهای مختلف به صورت منطقی ساختار یافته است، کمتر کارآمد خواهد بود.
به عنوان یک قاعدهی سرانگشتی: همیشه کد تمیز و بافکر بنویسید. هزینههای اضافی برای دوباره فکر کردن، دوباره ساختار بندی کردن و ریفکتور کردن در هر پروژهی غیر پیش پا افتادهای چند برابر خواهد شد.
هزینهها ممکن است خیلی زیاد باشد: در سال ۱۹۶۲ ناسا قصد داشت یک فضاپیما به زهره ارسال کند اما یک باگ کوچک، یعنی نبود کاراکتر - در کد، باعث شد مهندسها یک دستور خودتخریبی ارسال کنند و در نتیجه یک فضاپیمای ۱۸ میلیون دلاری نابود شود. اگر کد تمیزتر بود، مهندسها میتوانستند قبل از ارسال متوجه آن باگ شوند.
اگر بر روی یک پروژهی غیر پیش پا افتاده کار کنید، در نهایت با چند فایل، ماژول و کتابخانه مواجه خواهید شد که با هم کار میکنند تا یک برنامه کار کند. معماری نرمافزاری شما نحوهی ارتباط اجزای نرمافزار شما را تعریف میکند. تصمیمهای خوب معماری منجر به جهشهای عظیمی در عملکرد، نگهداری و قابلیت استفاده میشود. برای ساختن یک معماری خوب باید یک قدم عقب بیایید و به تصویر بزرگ فکر کنید. به فیچرهایی فکر کنید که در قدم اول به آنها نیاز داریم. در فصل قبل دربارهی MVP خواندیم و فهمیدیم که چطور باید بر روی فیچرهای ضروری پروژه تمرکز کرد. اگر این کار را انجام دهید، از انجام کارهای اضافه جلوگیری میکنید و کد تمیزتری مینویسید. در اینجا فرض میکنیم که شما برنامهی اول خود را که چند ماژول، فایل و کلاس دارد ساختید. چطور میتوانید تفکر تصویر بزرگ را به آن اعمال کنید تا بتوانید کمی نظم اضافه کنید؟ با در نظر گرفتن سوالهای زیر، میتوانید چند ایده برای تمیزتر کردن کدتان به دست بیاورید:
تفکر تصویر بزرگ روشی کارآمد از نظر زمان است که پیچیدگی برنامه را به طرز قابل توجهی کاهش میدهد. بعضی اوقات سخت است که این تغییرات را در مراحل بعدی اعمال کنید. به طور خاص، این طرز تفکر برای برنامههایی که میلیونها خط کد دارند سخت است مثل سیستم عامل ویندوز. اما به این راحتی نیز نمیتوانید این سوالها را نادیده بگیرید چون هر بهینهسازی کوچکی که اعمال کنید نمیتواند تاثیر تصمیمهای طراحی غلط را جبران کند.
ابداع دوبارهی چرخ به ندرت ارزشمند است. برنامهنویسی یک صنعت با عمر چند دههای است. بهترین برنامهنویسها در جهان برای ما یک میراث با ارزش فراهم کردهاند: یک پایگاه داده از میلیونها الگوریتم و توابع که به خوبی تست و بهینه شدهاند. دسترسی به این دانش جمعی میلیونها برنامهنویس، به سادگی یک خط import کردن است. هیچ دلیلی وجود ندارد که این کار را انجام ندهید.
استفاده از کدهای کتابخانهها احتمالا کارایی کد شما را بهتر میکند. توابعی که هزاران برنامهنویس از آنها استفاده کردهاند، بسیار بهینهتر از توابع شما هستند. علاوه بر این، فهمیدن توابع کتابخانهای راحتتر است و فضای کمتری در پروژهی شما میگیرند.
ممکن است فکر کنید که هدف اصلی یک سورس کد این است که به ماشین بگوییم که چه کار باید انجام دهد و چگونه آن را انجام دهد. نه دقیقا. تنها هدف یک زبان برنامهنویسی این است که به انسانها کمک کنند که کد بنویسند. کامپایلرها وظیفهی سنگین ترجمهی کد سطح بالا به کد سطح پایین را که ماشین میفهمد برعهده دارند. بله، در نهایت ماشین کد شما را اجرا خواهد کرد. اما کد را انسانها نوشتهاند و احتمالا قبل از اینکه دیپلوی شود، قرار است از چند مرحله از قضاوت انسانها بگذرد. نکتهی مهم: برای انسانها کد مینویسید، نه ماشینها.
همیشه فرض کنید که دیگران قرار است کد شما را بخوانند. فرض کنید به یک پروژهی جدید منتقل شدهاید و شخص دیگری در جای قبلی شما قرار گرفته است. راههای زیادی برای راحتتر کردن کار او و کم کردن تنشها وجود دارد. اول از همه، از نامهای متغیر بامعنا استفاده کنید تا خواننده راحت بفهمد که یک خط کد چه هدفی دارد و قرار است چه کاری انجام دهد. در اینجا یک مثال از یک کد با نامهای متغیر بد میبینید.
به سختی میتوان فهمید این کد چه کار میکند.
کد زیر همان کار بالا را انجام میدهد، صرفا از نامهای با معنا استفاده میکند.اینجا راحتتر میتوان فهمید چه اتفاقی میافتد.
هدف کد تمیز این است که انسان بهتر بتواند کد را بخواند. همانطور که Martin Fowler، متخصص مهندسی نرمافزار و نویسندهی کتاب معروف Refactoring میگوید:
هر کسی میتواند کدی بنویسد که کامیپوتر بفهمد. برنامهنویسهای خوب کدی مینویسند که انسانها بتوانند بفهمند.
برنامهنویسهای باتجربه بر روی مجموعهای از قراردادهای نامگذاری برای توابع، آرگومانهای تابع، آبجکتها، متدها و متغیرها توافق کردهاند. همه از رعایت اینها سود میبرند: کد خواناتر، قابل فهمتر و کمتر به هم ریخته میشود. اگر این قرارداد ها را زیر پا بگذارید، خوانندههای کد شما احتمالا فرض میکنند که این کد را یک برنامهنویس بدون تجربه نوشته است و ممکن است به کد شما اهمیتی ندهند.
این قراردادها ممکن است از زبان به زبان فرق کند. برای مثال جاوا از camelCaseNaming استفاده میکند و پایتون از underscore_naming. اگر از camelCase در پایتون استفاده کنید، خواننده را گیج خواهید کرد. اصلا دلتان نمیخواهد که نامگذاریهای غیرمعمول شما حواس خوانندهی کد را منحرف کند. شما میخواهید که خواننده بر روی کاری که کد شما انجام میدهد متمرکز شود نه بر روی استایل کدنویسی شما. طبق اصل کمترین غافلگیری (principle of least surprise): هیچ ارزشی در غافلگیر کردن دیگر برنامهنویسها با استفاده از نامگذاری غیرمعمول متغیرها وجود ندارد.
در اینجا چند مورد از قوانین نامگذاری را میخوانیم.
از نامهای توصیفی استفاده کنید: فرض کنید در پایتون یک تابع نوشتهاید که دلار را به یورو تبدیل میکند. به جای نام f(x) از usd_to_euro(amount) استفاده کنید.
از نامهای بدون ابهام استفاده کنید: شاید فکر کنید که نام dollar_to_euro(amount) یک نام خوب برای این تابع است. اگر چه بهتر از f(x) است اما بدتر از usd_to_euro(amount) است چون به ابهام اضافه میکند. منظور شما از دلار، دلار آمریکا، کانادا یا استرالیا است؟ اگر در آمریکا باشید ممکن است جواب بدیهی باشد اما یک برنامهنویس استرالیایی نمیداند که این کد در آمریکا نوشته شده است و ممکن است انتظار یک خروجی دیگر داشته باشد. این سردرگمیها را به حداقل برسانید!
از نامهای قابل تلفظ استفاده کنید: بیشتر برنامهنویسها ناخودآگاه کد را با تلفظ کردن در ذهنشان میخوانند. اگر نام یک متغیر غیر قابل تلفظ باشد، رمزگشایی آن توجه را به خود جلب میکند. برای مثال، متغیر cstmr_lst شاید توصیفی و بدون ابهام باشد اما قابل تلفظ نیست. نامگذاری customer_list ارزش جای اضافه در کدتان را دارد.
از متغیرهای ثابت دارای نام استفاده کنید نه اعداد جادویی: در کدتان شاید از عدد جادویی ۰.۹ چند بار به عنوان ضریب برای تبدیل مبلغ از دلار آمریکا به یورو کرده باشید. اما خوانندهی کد شما، از جمله خود شما در آینده، باید به مقصود این عدد فکر کند. این عدد خود توصیف نیست. یک روش بهتر این است که این عدد در یک متغیر که با حروف بزرگ نوشته شده است، ذخیره شود. استفاده از حروف بزرگ نشان میدهد که این متغیر ثابت است و تغییر پیدا نمیکند، مثلا CONVERTSION_RATE = 0.9 و سپس تبدیل کد به income_euro = CONVERTSION_RATE * income_usd.
فقط چند قانون نامگذاری وجود دارد. بهترین راه برای یادگیری این قراردادها، خواندن کدهایی است که متخصصها نوشتهاند. جستجوی قراردادهای نامگذاری مثل Python naming convention یک نقطهی شروع خوب است. همچنین میتوانید آموزشهای برنامهنویسی بخوانید، به StackOverflow بپیوندید و کدهای پروژههای اپن سورس را در GitHub بخوانید.
هر زبان برنامهنویسی دستهای از قوانین ضمنی یا صریح دربارهی چگونه کد تمیز نوشتن دارد. برای مثال، راهنمای رسمی پایتون که PEP8 نام دارد، در این لینک قابل دسترسی است. مانند هر راهنمای استایل کد، PEP8 شیوهی صحیح layout و indentation، نحوهی شکستن خطها، بیشترین تعداد کاراکتر در یک خط، استفادهی درست از کامنت، نامگذاری صحیح کلاسها، تابعها و متغیرها را تعریف میکند.
در شکل زیر، شیوهی غلط نوشتن را میبینیم. آرگومانها همراستا نیستند، چند کلمه به درستی در یک تابع قرار نگرفتهاند، لیست آرگومانها به درستی توسط فاصله جدا نشدهاند و indentation به جای ۴ فاصله از ۲ یا ۳ فاصله استفاده کرده است.
تمام خوانندههای کد شما انتظار دارند که به استانداردهای پذیرفته شده وفادار باشید. هر چیز دیگری منجر به آشفتگی دیگران میشود.
خواندن راهنماهای استایل کد میتواند کار خستهکنندهای باشد. بهعنوان یک روش کمتر خسته کننده برای یادگیری قراردادها و استانداردها، از linter ها و IDE هایی استفاده کنید که به شما میگویند که چگونه و کجای کد اشتباه کردهاید.
همانطور که قبلا گفته شد، وقتی برای انسانها کد مینویسیم، باید از کامنتها استفاده کنیم تا کمک کنیم خواننده بفهمد. کد زیر را در نظر بگیرید.
کد بالا یک متن کوتاه از رومئو و ژولیت شکسپیر را با استفاده از regular expressionها تحلیل میکند. اگر با regular expression ها آشنا نباشید، احتمالا به سختی متوجه خواهید شد که کد چه کار میکند. حتی متغیرهای با معنا نیز کمکی نمیکنند. حال همین کد با کامنت را در نظر بگیرید.
همچنین میتوانید از کامنتها قبل از یک بلاک کد استفاده کنید تا خلاصهی کاری را که انجام میدهد، توضیح دهید.
همچنین میتوانید از کامنت برای هشدار دادن دربارهی عواقب نامطلوب به برنامهنویسها هشدار دهید.
تمام کامنتها به بهتر فهمیدن خواننده کمک نمیکنند. در بعضی مواقع کامنتها از شفافیت کم و خواننده را گیج میکنند. برای نوشتن کد تمیز، نه تنها باید از کامنتهای با ارزش استفاده کنید بلکه باید از کامنتهای غیرضروری نیز دوری کنید.
هنگامی که من یک محقق علوم کامیپوتر بودم، یکی از دانشجوهای با استعداد من برای یک شغل در گوگل اقدام کرد. او گفت که headhunter های گوگل از استایل کدنویسی او انتقاد کردند چون کامنتهای غیرضروری زیادی نوشته بود. اعتبارسنجی کامنتها یک راه دیگر برای متخصصین کدنویسی است که میتوانند بفهمند شما تازهکار، متوسط یا حرفهای هستید. مشکلات کد مانند رعایت نکردن استانداردها، تنبلی در کامنتنویسی یا نوشتن کد به شکلی که قواعد یک زبان برنامهنویسی را رعایت نمیکند، با نام code smell شناخته میشوند که منجر به مشکلات متعدد در کد میشود و حرفهای ها آنها را از چند کیلومتری تشخیص میدهند.
از کجا باید فهمید که چه کامنتهایی خوب نیستند؟ در بیشتر مواقع، اگر کامنت اضافی باشد، غیرضروری است. برای مثال، از متغیرهای بامعنا استفاده کردهاید و کد به خوبی خود را توضیح میدهد و احتیاجی به کامنت ندارد. مثلا کد زیر
کاملا واضح است که این کد چه کار میکند. حال شکل زیر کامنتهای اضافی را نشان میدهد. تمام کامنتهای این شکل اضافه هستند.
در بیشتر مواقع باید از common sense برای تشخیص ضروری بودن یک کامنت استفاده کنید اما در اینجا چند راهنما را میخوانیم.
از کامنتهای درون خطی دوری کنید: با استفاده از متغیرهای با نام معنادار، میتوان از این نوع کامنتها جلوگیری کرد.
از کامنتهای واضح دوری کنید: در شکل ۱۰-۴، کامنت مربوط به حلقهی for غیرضروری است. همهی کدنویسها میدانند که حلقهی for چیست و احتیاجی به توضیح ندارد.
کدهای قدیمی را کامنت نکنید، حذف کنید: همیشه کدهای قدیمی و غیرلازم را حذف کنید. کامنت کردن آنها، خوانایی کد را کاهش میدهد. از ابزارهای کنترل ورژن مانند Git برای ذخیره کردن نسخههای اولیه کد استفاده کنید.
از قابلیتهای داکیومنتکردن استفاده کنید: بسیاری از زبانهای برنامهنویسی ابزارهای داکیومنت built-in دارند که به شما اجازه میدهند هدف هر تابع و کلاس را بنویسید. اگر هر کدام از آنها فقط یک مسئولیت دارند (در اصل ۱۰ بررسی خواهیم کرد)، معمولا استفاده از داکیومنت به جای کامنت برای توضیح کد ، کافی است.
این اصل بیان میکند که یک جزء از سیستم باید به روشی رفتار کند که بیشتر کاربران انتظار دارند رفتار کند. این اصل یکی از قانونهای طلایی به هنگام طراحی برنامههای کارا و تجربهی کاربری است. برای مثال، اگر گوگل را باز کنید، نشانگر به صورت خودکار در قسمت نوشتن ورودی قرار میگیرد و شما میتوانید در همان لحظه شروع به تایپ کردن عبارت مورد نظر بکنید. دقیقا به همان صورتی که انتظار دارید: بدون غافلگیری.
کد تمیز نیز از این اصل طراحی پیروی میکند. فرض کنید یک تابع برای تبدیل ارز کاربر از دلار آمریکا به ین چین نوشتهاید. ورودی کاربر را در یک متغیر ذخیره میکنید. کدام نام متغیر بهتر است؟ user_input یا var_x ؟ اصل کمترین غافلگیری پاسخ این سوال را میدهد!
اصل Don’t Repeat Yourself (DRY) یک اصل مشهور است که پیشنهاد میکند از کد تکراری اجتناب کنید. برای مثال، این کد پنج بار یک رشته را چاپ میکند.
این کد در واقع همان کار را انجام میدهد اما تکرار کمتری دارد.
استفاده از تابع نیز یک ابزار مناسب برای کاهش تکرار است. با این کار نگهداری کد نیز راحتتر است. برای مثال میتوانید تابع را ویرایش کنید تا دقت تبدیل بیشتر شود. با این کار کافی است فقط یکبار تابع را ویرایش کنید تا این تغییر در کل کد اعمال شود. اگر از تابع استفاده نمیکردید، باید در کل کد جستجو میکردید تا این ویرایش را در تمام قسمتها اعمال کنید.
تخطی از DRY اغلب WET نامیده میشود: ما تایپ کردن را دوست داریم (We Enjoy Typing)، همه چیز را دوبار بنویس (Write Everything Twice) و وقت همه را تلف کن (Waste Everyone's time)
اصل یگانگی مسئولیت (The Single Responsibility Principle) بیان میکند که هر تابع باید یک وظیفهی اصلی داشته باشد. بهتر است که از تعداد زیادی تابع کوچک استفاده شود تا یک تابع بزرگ که تمام کارها را یکجا انجام میدهد. این کار پیچیدگی کد را کاهش میدهد.
به عنوان یک قانون سرانگشتی، هر کلاس و هر متد باید یک وظیفه داشته باشد. Robert C. Martin، مبدع این اصل، مسئولیت (Responsibility) را یک دلیل برای تغییر (A reason to change) تعریف میکند. استاندارد طلایی او به هنگام تعریف کلاس و متد این است که اینها بر روی یک مسئولیت تمرکز کنند به طوری که فقط برنامهنویسی که نیاز دارد این یک مسئولیت تغییر کند، درخواست تغییر در تعریف بکند و دیگر برنامهنویسها با مسئولیتهای دیگر، با فرض صحیح بودن کد، درخواست تغییر برای کلاس ندهند.
برای مثال، تابعی که وظیفهی خواندن از دیتابیس را دارد، نباید وظیفهی پردازش داده را نیز داشته باشد. در غیر این صورت این تابع دو دلیل برای تغییر دارد: یک تغییر در مدل دیتابیس و یک تغییر در پردازشها. اگر چند علت برای تغییر وجود دارد، ممکن است چند برنامهنویس همزمان همان کلاس را تغییر دهند. کلاس شما مسئولیتهای زیادی دارد و آشفته و در هم ریخته میشود.
یک مثال کوچک از یک کد پایتون را در نظر بگیرید که بر روی یک کتابخوان الکترونیکی اجرا میشود و تجربهی کاربر را مدیریت میکند.
در شمارهی ۱ کلاس Book را تعریف میکنیم که ۴ attribute دارد: title, author, publisher و current page. در شمارهی ۲ متدهای getter را برای attribute ها تعریف میکنید. در شمارهی ۳ متدهای رفتن به صفحهی بعدی را پیادهسازی میکنید و شمارهی ۴ صفحهی فعلی را در دستگاه نشان میدهد. این صرفا یک مثال است و در جهان واقعی بسیار پیچیدهتر خواهد بود. در آخر یک آبجکت Book با نام python_one_liners میسازید و به توابعی که گفته شد، از طریق آن دسترسی خواهید داشت.
اگر چه که کد تمیز و ساده به نظر میرسد اما اصل یگانگی مسئولیت را زیر پا میگذارد. کلاس Book هم مسئولیت مدل کردن داده و هم مسئولیت پرینت کردن کتاب در دستگاه را دارد. مدل کردن و پرینت کردن دو عمل متفاوت هستند اما در یک کلاس آمدهاند. چند دلیل برای تغییر وجود دارد. شاید بخواهید نحوهی مدل کردن اطلاعات کتاب را تغییر دهید. برای مثال شاید بخواهید از دیتابیس استفاده کنید. همچنین شاید بخواهید نحوهی ارائهی دادهی مدل شده را نیز تغییر دهید. این مشکل را در شکل زیر حل کردیم.
برای نشان دادن اطلاعات کتاب از کلاس Book و برای نشان دادن کتاب در دستگاه از کلاس Printer استفاده میکنیم. با این کار، نحوهی مدل کردن داده (داده چه چیزی است؟) و نحوهی ارائهی داده (داده چطور به کاربر ارائه میشود؟) از هم جدا شدهاند و نگهداری کد نیز راحتتر شده است. اگر بخواهید یک attribute جدید به نام publishing_year اضافه کنید، آن را به کلاس Book اضافه میکنید. اگر بخواهید این تغییر را در نحوهی نشان دادن نیز تغییر دهید، تغییری در کلاس Printer ایجاد میکنید.
توسعهی مبتنی بر تست، یک بخش جداییناپذیر از توسعهی نرمافزار امروزی است. تفاوتی ندارد که چقدر مهارت دارید، قطعا اشتباههایی در کدتان خواهید کرد. برای گرفتن این خطاها، باید تستهای دورهای بگیرید یا در وهلهی اول، کد مبتنی بر تست بنوسید. هر شرکت نرمافزاری بزرگ، قبل از دیپلوی کردن محصول برای عموم، آن را از چند مرحله تست میگذارند. در واقع بهتر است که این خطاها را خودشان کشف کنند تا اینکه از کاربران ناراضی دربارهی از آنها مطلع شوند.
اگرچه که هیچ محدودیتی در نوع تستی که انجام میدهید وجو ندارد، در اینجا انواع رایج آن را بررسی میکنیم.
تستهای واحد (Unit test): در Unit test، یک برنامهی جدا برای بررسی رابطهی صحیح بودن ورودی و خروجی برای ورودیهای مختلف هر تابع مینویسید. Unit testها معمولا در بازههای منظم اعمال میشوند؛ برای مثال هر بار که نسخهی جدید برنامه منتشر میشود. این کار احتمال از کار افتادن فیچرهای قبلی با انتشار نسخهی جدید را کم میکند.
تستهای تایید کاربر (User acceptance test): این تستها به افرادی که در بازار هدف شما هستند اجازه میدهد تا برنامهی شما را در یک محیط کنترل شده استفاده کنند. در این محیط شما رفتار آنها را بررسی میکنید و سپس از آنها میپرسید که از چه چیزهای برنامه راضی بودند و چطور میتوان آن را بهتر کرد. این تستها معمولا در مرحلهی آخر توسعه انجام میشوند، بعد از اینکه تستهای درون سازمانی انجام شده است.
تست دود (Smoke test): این تستها تستهای سختی هستند که طراحی شدهاند تا برنامهی در حال توسعه را از کار بیندازند، قبل از اینکه تیم توسعهدهنده آن را به تیم تست تحویل دهد. به عبارت دیگر، این تستها را اغلب تیم توسعهدهنده انجام میدهد تا قبل از اینکه کد را به تیم تست تحویل دهد، از کیفیت آن مطمئن شود. وقتی برنامه از Smoke testها عبور میکند، آمادهی مرحلهی بعدی تست است.
تست کارایی (Performance test): هدف این تستها این است که نشان دهند آیا کارایی برنامه مطابق انتظار یا حتی فراتر از آن است یا نه. برای مثال قبل از اینکه نتفلیکس یک فیچر جدید منتشر کنید، باید سرعت بارگزاری صفحهی وبسایت را تست کند. اگر فیچر جدید سرعت بارگزاری را کم میکند، آن را منتشر نمیکند تا آسیبی به تجربهی کاربری نرسد.
تست مقیاسپذیری (Scalability test): اگر برنامهی شما موفق شود، شاید مجبور شوید به جای ۲ درخواست، ۱۰۰۰ درخواست در دقیقه را مدیریت کنید. تست مقیاسپیذری نشان میدهد که آیا برنامهی شما به اندازه کافی مقیاسپذیر برای مدیریت این شرایط است یا نه. دقت کنید که یک برنامه با کارایی خوب لزوما مقیاسپذیر نیست و برعکس. مثلا یک قایق سرعتی کارایی خوبی دارد اما هزار نفر را نمیتواند جابجا کند!
تست و ریفکتور کردن اغلب از پیچیدگی و تعداد خطاهای کد کم میکند. اگرچه، دقت کنید که بیش-مهندسی نکنید (در اصل ۱۴ میخوانیم)؛ باید سناریوهایی را بررسی کنید که در دنیای واقعی نیز رخ میدهند. مثلا نتفلیکس لازم نیست بررسی کند ببیند توانایی پشتیبانی از ۱۰۰ میلیارد دستگاه استریم دارد یا نه چون جمعیت زمین ۷ میلیارد است!
کد کوچک (Small code) کدی است که به تعداد نسبتا کمی از خطوط کد نیاز دارد تا یک کار را انجام دهد. اینجا یک نمونه از کد کوچک را میبینیم که یک عدد را از کاربر میگیرد و مطمئن میشود که ورودی عدد است:
کد تا جایی ادامه پیدا میکند که کاربر یک عدد وارد کند.
با جدا کردن منطق خواندن عدد از ورودی، میتوانید یک تابع را چند بار استفاده کنید. اما مهمتر از همه، کد را به قسمتهای کوچکی تبدیل کردید که فهمیدن و خواندن آنها آسان است.
اما بسیاری از کدنویسهای تازهکار یا کدنویسهای متوسط تنبل، توابع بزرگ مینویسند که همهی کارها را انجام میدهند. به این توابع God Object نیز گفته میشود. این کد بلاکهای یکپارچه به سختی قابل نگهداری هستند. درک یک تابع کد کوچک در آن واحد آسانتر از فهمیدن یک فیچر در یک کد بلاک ۱۰ هزار خطی است. احتمالا در یک کد بلاک بزرگ به نسبت چند کد بلاک کوچک، خطاهای بیشتری داشته باشید.
در ابتدای این فصل شکل ۱-۴ نشان داد که نوشتن کد با اضافه شدن کدهای جدید، زمان بیشتری میگیرد. نوشتن کد تمیز در طولانی مدت سریعتر از نوشتن کد کثیف است. شکل ۲-۴ زمان مورد نیاز برای کار کردن با کد بلاکهای کوچک در برابر کد بلاکهای بزرگ یکپارچه را مقیاسه میکند. برای کد بلاکهای بزرگ، زمان مورد نیاز برای اضافه کردن هر خط کد جدید، نمایی افزایش پیدا میکند. اما اگر از چند تابع کوچک با هم دیگر استفاده کنید، زمان مورد نیاز به ازای هر خط جدید شبهخطی افزایش پیدا میکند. برای دستیابی به این نتیجه، باید مطمئن باشید که هر تابع تقریبا مستقل از توابع دیگر است. این قانون را در مورد بعدی بررسی میکنیم.
وقتی یک کتابخانه را در کد import میکنیم، کد شما بر این کتابخانه متکی میشود؛ همچنین خود این کتابخانه نیز درون خود dependency هایی دارد. dependency ها همه جا هستند. در برنامهنویسی شیگرا، یک تابع ممکن است به تابع دیگری، یک آبجکت به آبجکت دیگری و یک کلاس به کلاس دیگری احتیاج داشته باشد.
برای نوشتن کد تمیز، وابستگیهای متقابل (interdependency) را با پیروی از Law of Demeter به حداقل برسانید. Ian Holland این قانون را در سال ۱۹۸۰ وقتی بر روی یک پروژه با نام Demeter، الهه کشاورزی، رشد و باروری یونان، کار میکرد، پیشنهاد داد. گروه این پروژه، ایدهی بزرگ کردن نرمافزار را به جای صرفا درست کردن آن ارائه دادند. اگرچه، چیزی که به Law of Demeter شناخته شد زیاد به ایدههای متافیزیکی ربطی نداشت. در اینجا یک نقل قول خلاصه از سایت پروژه میخوانیم که به طور خلاصه این قانون را توضیح میدهد.
یک مفهوم مهم Demeter این است که نرمافزار را به حداقل دو قسمت تقسیم کنیم: اولین قسمت آبجکتها را تعریف میکند. دومین قسمت عملها را تعریف میکند. هدف Demeter این است که بین عمل و آبجکت یک همراهی آزادانه برقرار کند به طوری که هر کدام بتوانند بر دیگری تغییراتی انجام دهند، بدون آنکه تاثیر جدیای روی دیگری داشته باشند. این کار از زمان نگهداری، کم میکند.
به عبارت دیگر باید dependencyهای آبجکتهای کدتان را به حداقل برسانید. با کم شدن این dependency، پیچیدگی کد نیز کاهش میابد و نگهداری آن راحتتر میشود. یک مفهوم مهم این است که هر آبجکت باید تنها متدهای خودش یا متدهای آبجکتهای نزدیک را فراخوانی کند. برای مثال، فرض کنید دو آبجکت A و B داریم که در صورتی این دو را با هم دوست میدانیم که A تابعی را که B ارائه میدهد، فراخوانی میکند. اما اگر تابع B یک آبجکت C برگرداند چه؟ حال A شاید چنین کاری کند:
B.method_of_B().method_of_C()
به این کار زنجیر کردن فراخوانیهای تابع یا chaining of method calls میگویند؛ به زبان خودمان، شما با دوست دوستتان صحبت میکنید. قانون دمتر میگوید که فقط با دوستان نزدیک خود صحبت کنید. پس چنین فراخوانی تابع را منع میکند. ممکن است در ابتدا گیجکننده باید پس مثال زیر را در نظر بگیرید.
شکل ۳-۴ دو پروژهی شیگرایی را نشان میدهد که برای یک شخص، قیمت هر فنجان قهوه را محاسبه میکند. یکی از پیادهسازیها، قانون دمتر را زیر پا میگذارد و دیگری بر آن پایبند است. ابتدا مورد اول را بررسی میکنیم که در آن از method chaining در کلاس Person استفاده میکنیم تا با یک غریبه صحبت کنیم (خط کدی که با شمارهی ۱ مشخص شده است)
شما تابع price_per_cup() را میسازید که بر اساس قیمت ماشین قهوهساز و تعداد قهوهها، هزینهی هر فنجان قهوه را محاسبه میکند. آبجکت Coffee_Cup اطلاعات قیمت قهوهی ماشین قهوهساز را جمعآوری میکند که قیمت هر فنجان را تحت تاثیر قرار میدهد و آن را به صدا زنندهی price_per_cup() در آبجکت Person پاس میدهد.
توضیح قدم به قدم این کد:
۱- متد price_per_cup() تابع Coffee_Cup.get_creator_machine() را فراخوانی میکند تا یک رفرنس از آبجکت Coffee_Machine به دست بیاورد.
۲- متد get_creator_machine() آبجکتی که به Coffee_Machine رفرنس میدهد را بازمیگرداند.
۳- متد price_per_cup() متد Coffee_Machine.get_price() را روی آبجکت Coffee_Machine فراخوانی میکند.
۴- متد get_price() قیمت دستگاه را بازمیگرداند.
۵- متد price_per_cup() هزینهی هر فنجان را محاسبه میکند.
این یک استراتژی بد است چون کلاس Person متکی بر دو آبجکت Coffee_Cup و Coffee_Machine است. یک برنامهنویس که وظیفهی نگهداری از این را برعهده دارد باید دربارهی هر دو کلاس والد بداند؛ هر تغییری در هر کدام کلاس Person را هم تحت تاثیر قرار میدهد.
قانون دمیتر چنین dependencyهایی را به حداقل میرساند. مدل بهتر را میتوانید در شکل ۳-۴ ببینید. در اینجا کلاس Person مستقیما با کلاس Machine صحبت نمیکند؛ در واقع اصلا احتیاجی ندارد که بداند چنین کلاسی وجود دارد!
۱- متد price_per_cup() متد Coffee_Cup.get_cost_per_cup() را فراخوانی میکند تا قیمت حدودی هر فنجان را به دست بیاورد.
۲- متد get_cost_per_cup() ، قبل از اینکه به متدی که آن را فراخوانی کرده پاسخ دهد، متد Coffee_Machine.get_price() را فراخوانی میکند تا قیمت دستگاه را به دست بیاورد.
۳- متد get_price() اطلاعات قیمت را برمیگرداند
۴- متد get_cost_per_cup() قیمت هر فنجان را محاسبه میکند و آن را در جواب فراخوانی تابع price_per_cup() بازمیگرداند.
۵- متد price_per_cup() این مقدار را به صدازننده بازمیگرداند.
این رویکرد بهتری است چرا که کلاس Person حالا مستقل از کلاس Coffee_Machine است. تعداد dependencyها کاهش یافته است. برای یک پروژه با صدها کلاس، کم کردن dependecyها پیچیدگی کلی برنامه را کاهش میدهد. خطر افزایش پیچیدگی در پروژههای بزرگ چنین است: تعداد dependencyها با افزایش آبجکتها، بهصورت نمایی افزایش میابد. مثلا دو برابر شدن تعداد آبجکتها میتواند dependencyها را ۴ برابر افزایش دهد. اما با پیروی از قانون دیمتر میتوان جلوی این را گرفت. اگر هر آبجکت فقط با k آبجکت دیگر صحبت کند و شما n آبجکت داشته باشید، تعداد dependencyها حداکثر k*n است که اگر k یک ثابت باشد، به یک رابطهی خطی میرسیم. پس قانون دیمتر از نظر ریاضی میتواند به شما کمک کند تا مقیاس برنامههای خود را به خوبی افزایش دهید.
این اصل پیشنهاد میکند که اگر شک دارید که به کدی در آینده نیاز پیدا خواهید کرد، هیچوقت نباید آن را پیادهسازی کنید، چون هیچوقت قرار نیست به آن نیاز پیدا کنید! فقط کدی را بنویسید که ۱۰۰ درصد مطمئن هستید ضروری است؛ کدی که برای امروز است و نه فردا. اگر در آینده دیدید که به این کد احتیاج دارید، به راحتی میتوانید آن را پیاده سازی کنید. با این کار از خطوط غیرضروری جلوگیری کردهاید.
بیاید از اصولی که یاد گرفتیم استفاده کنیم: سادهترین و تمیزترین کد، یک فایل خالی است. حال از اینجا به بعد، چه چیزی لازم است که به آن اضافه کنید؟ در فصل ۳ دربارهی MVP خواندید؛ کدی که فیچرهای اضافه ندارد و فقط بر روی عملکردهای اصلی متمرکز است. اگر تعداد فیچرهایی را که به دنبالش هستید، به حداقل برسانید، کد تمیزتر و سادهتری به دست میآورید تا اینکه بخواهید کد را مطابق اصولی که گفتیم ریفکتور کنید. فیچرهایی که ارزش کمی به نسبت بقیه دارند حذف کنید. قبل از اینکه پیادهسازی یک فیچر را در نظر بگیرید، باید واقعا به آن احتیاج پیدا کرده باشید.
یک پیامد این کار، اجتناب از مهندسی بیش از حد (overengineering) است: یعنی ساختن محصولی که خیلی عملکرد خوبی دارد و قوی است یا فیچرهای بیش از حد نیاز دارد. این کار پیچیدگی غیرضروری را افزایش میدهد.
اغلب زبانهای برنامهنویسی از تورفتگی (Indentation) برای نمایش ساختار سلسله مراتبی بلاکهای شرط، تعاریف توابع و حلقهها استفاده میکنند. استفاده بیش از حد از این تورفتگیها، میتواند از خوانایی کد کم کند. در اینجا یک مثال میبینیم.
حال اگر سعی کنید که خروجی این تابع را حدس بزنید، میبینید که دنبال کردن کد کار سختی است. در شکل زیر یک کد دیگر میبینیم که همان کار را انجام میدهد اما سادهتر و تمیزتر است.
بیشتر کدنویسها از خواندن کدهای flat بیشتر از کدهای nested لذت میبرند؛ حتی اگر این کار به قیمت بررسیهای اضافه باشد، مثلا در اینجا x>y چند بار بررسی شده است.
برای دنبال کردن میزان پیچیدگی کد خود، از متریکهای کیفیت کد استفاده کنید. متریک نهایی و البته غیررسمی، تعداد WTF ها در دقیقه است. با این کار میزان آشفتگی خوانندهی کد اندازهگیری میشود. این تعداد برای کد تمیز و ساده کم است و برای کد کثیف بالا است. همچنین در بسیاری از IDEها، پلاگینهایی وجود دارند که پیچیدگی کد را بررسی میکنند.
قانون پیشاهنگ ساده است: کمپ را تمیزتر از موقعی که آن را پیدا کردید، ترک کنید. این عادت را توسعه دهید که هر وقت به هر کدی رسیدید، آن را تمیز کنید. این کار نه تنها کدبیسی که در آن هستید بهتر میکند بلکه به شما کمک میکند تا یاد بگیرید مانند یک برنامهنویس حرفهای، کد را سریع ارزیابی کنید. دقت کنید که این کار نباید قانونی که بالاتر گفتیم نقض کنید (مهندسی بیش از حد) به طور خلاصه، مهندسی بیش از حد احتمالا پیچیدگی را افزایش دهد حال اینکه تمیز کردن کد از آن کم میکند.
فرآیند بهتر کردن کد را refactoring میگویند. به عنوان یک برنامهنویس عالی، از خیلی از اصلهایی که گفتیم استفاده خواهید کرد. اما باز هم، بعضی اوقات نیاز است که کد را ریفکتور کنیم. به طور خاص، قبل از اینکه یک نسخهی جدید منتشر کنید، بهتر است کد را ریفکتور کنید.
راههای مختلفی برای ریفکتور کردن وجود دارد. یک راه این است که کد را برای همکار خود توضیح دهید یا از آنها بخواهید که به کد شما نگاه کنند تا تصمیمهای بد شما را که خودتان متوجه نشدید، کشف کنند. این کار شما را مجبور میکند تا به تصمیمهای خود فکر کنید و به کارهای خود از بالا نگاه کنید و سعی کنید آنها را توضیح دهید.
اگر کدنویس درونگرایی هستید، میتوانید کد خود را به یک اردک پلاستیکی توضیح دهید؛ تکنیکی که به آن rubber duck debugging میگویند.
علاوه بر صحبت کردن با همکار یا اردک، میتوانید از اصول کد تمیزی که گفته شد برای ارزیابی کدتان استفاده کنید.