بهبود خوانایی کد — ده نکته اساسی که کدنویسی شما را بهتر میکند
خوانایی کد یکی از مهمترین مسائل موجود در حوزه برنامهنویسی و کدنویسی است. اهمیت این مسئله به قدری است که رعایت نکردن آن میتواند منجر به مشکلات اساسی در ساختار و طراحی یک مجموعه کد شود. معمولاً در دنیای برنامه نویسی، مشکل ناخوانا بودن کد را با اصطلاح «Code Smell» بیان میکنند. مفهوم لغوی این اصطلاح یعنی کدی که بوی بد میدهد.
ناخوانا بودن را میتوان به عنوان نشانهای برای تغییر و اصلاح کد در نظر گرفت. منشا این مشکل، وجود باگ یا عملکرد نادرست کد نیست و در اغلب موارد، اجرای کدهای ناخوانا به خوبی صورت میگیرد اما نگهداری و توسعه آنها دشوار میشود. این مسئله در نهایت منجر به مشکلات فنی، بخصوص در پروژههای بزرگ خواهد شد.
در این مقاله، 10 مورد از رایجترین دلایل ناخوانایی کد را معرفی خواهیم کرد و روشهای رفع هر یک آنها را نیز به شما آموزش خواهیم داد. در صورتی که به تازگی وارد عرصه برنامهنویسی شدهاید، سعی کنید از چنین مشکلاتی اجتناب کنید تا کیفیت کد شما به طور قابل توجهی بهبود یابد.
1. جفتگری (Coupling) نزدیک به هم
بیان مسئله
«جفتگری» (Coupling) نزدیک به هم هنگامی رخ میدهد که دو شیء وابستگی بسیار شدیدی به دادههای یکدیگر داشته باشند و یا در صورت اعمال تغییر در یک تابع، نیاز باشد که توابع دیگر نیز تغییر کنند. جفتگری بسیار نزدیک بین دو شیء منجر به دشوار شدن اعمال تغییرات در کد میشود.
در این حالت، امکان ایجاد باگ در اثر اعمال هر نوع تغییری وجود خواهد داشت. به مثال زیر دقت کنید:
class Worker { Bike bike = new Bike(); public void commute() { bike.drive(); } }
این کد نشان میدهد که یک کارمند برای رفت و آمد خود از دوچرخه استفاده میکند. در این مثال، «Worker» (کارمند) و «Bike» (دوچرخه) جفتگری بسیار نزدیکی دارند و این سوال پیش میآید که اگر کارمند بخواهد به جای دوچرخه از یک ماشین استفاده کند، چه اتفاقی برای کد رخ خواهد داد؟ شما باید به کلاس Worker بروید و تمام کدهای مرتبط به Bike را با کدهای مرتبط به «Car» (ماشین) جایگزین کنید. اتخاذ چنین رویکردی باعث شلوغکاری زیاد و احتمال ایجاد خطا میشود.
راه حل
با اضافه کردن یک لایه انتزاعی (Abstraction Layer) در کد، میتوان این جفتگری را از بین ببرید. در این مثال، کارمند نمیخواهد که تنها با دوچرخه رفت و آمد داشته باشد بلکه شاید بخواهد از ماشین، کامیون و یا اسکیت نیز استفاده کند. تمام این موارد، وسایل نقلیه هستند. از اینرو، باید برای جایگزین کردن وسایل نقلیه مختلف مورد نظر خود، مانند کد زیر یک «رابط» (Interface) ایجاد کنید:
class Worker { Vehicle vehicle; public void changeVehicle(Vehicle v) { vehicle = v; } public void commute() { vehicle.drive(); } } interface Vehicle { void drive(); } class Bike implements Vehicle { public void drive() { } } class Car implements Vehicle { public void drive() { } }
2. شیء خدا
بیان مسئله
«شیء خدا» (God Object)، به کلاس یا ماژول بزرگی گفته میشود که متغیرها و توابع زیادی را در خود جای داده باشد. این شیء چیزهای زیادی را میداند و کارهای زیادی را انجام میدهد که این مسئله به دو دلیل مشکلساز خواهد شد.
اولاً، کلاسها یا ماژولهای دیگر برای به دست آوردن داده، بیش از حد به این شیء متکی میشوند (جفتگری نزدیک به هم). دوما، به دلیل قرار دادن همه دستورات در یک محل، ساختار کلی برنامه شلوغ میشود.
راه حل
یک شیء خدا را در نظر بگیرید. دادهها و توابع آن را با توجه به مسائلی که حل میکنند، به گروههای جداگانه تقسیم کنید. سپس، هر گروه را به یک شیء تبدیل کنید. اگر یک شیء خدا در کد شما وجود دارد، بهتر است آن را به صورت ترکیبی از تعداد زیادی اشیا کوچک درآورید.
به عنوان مثال، یک کلاس «User» بسیار بزرگ را در نظر بگیرید:
class User { public String username; public String password; public String address; public String zipcode; public int age; ... public String getUsername() { return username; } public void setUsername(String u) { username = u; } }
میتوان این کلاس را به صورت ترکیبی از اشیا زیر تبدیل کرد:
class User { Credentials credentials; Profile profile; ... } class Credentials { public String username; public String password; ... public String getUsername() { return username; } public void setUsername(String u) { username = u; } }
از این پس، هنگامی که بخواهید تغییری در فرآیند ورود کاربر ایجاد کنید، نیازی به بررسی کامل این کلاس بزرگ نخواهید داشت؛ چراکه مدیریت کلاس «Credentials» آسانتر خواهد بود.
3. توابع طولانی
بیان مسئله
«تابع طولانی»، به تابعی گفته میشود که بیش از حد گسترش یافته باشد. تعداد خط کد مشخصی برای طولانی در نظر گرفتن یک تابع معرفی نشده است. با این حال، میتوان با مشاهده یک تابع، متوجه طولانی بودن آن شد. این مشکل را میتوان حالت جمع و جورتری از شیء خدا در نظر گرفت؛ چراکه یک تابع طولانی، وظایف بسیار زیادی را برعهده دارد.
راه حل
توابع طولانی باید به زیرتابعهای کوچکتر تقسیم شوند که هر زیرتابع برای رسیدگی به یک کار یا یک مسئله بخصوص طراحی شده است. تابع طولانی در حالت ایدئال، به فهرستی برای فراخوانی زیرتابعها تبدیل میشود که کد را تمیزتر و خوانایی آن را بهتر میکند.
4. پارامترهای بیش از اندازه
بیان مسئله
یک تابع یا سازنده کلاس که به پارامترهای زیای نیاز داشته باشد، به دو دلیل مشکلساز خواهد بود. اولاً، خوانایی کد کمتر و بررسی آن دشوارتر میشود. دوما، این مسئله میتواند ابهام زیاد در هدف تابع و تلاش آن برای رسیدگی به وظایف بسیار زیادی را نشان دهد.
راه حل
تعیین خیلی زیاد بودن پارامترها، به نظر هر فرد بستگی دارد. با این حال، پیشنهاد میشود که در صورت وجود بیش از سه پارامتر در تابع، با احتیاط بیشتری رفتار شود. گاهی اوقات، استفاده از پنج یا شش پارامتر در یک تابع نیز منطقی به نظر میرسد اما مطمئناً دلیل مناسبی برای این کار وجود دارد. از طرف دیگر در اکثر مواقع، دلیل خوبی برای زیاد بودن پارامتر وجود ندارد و بهتر است که تابع، به دو یا چند تابع دیگر شکسته شود. برخلاف توابع طولانی، مشکل پارامترهای زیاد را نمیتوان تنها با جایگزینی زیرتابع در کد رفع کرد. برای مقابله با این مسئله، باید تابع اصلی را به توابع جداگانهای تقسیم کرد تا هر تابع وظیفه مختص به خود را انجام دهد.
5. نامگذاری نامناسب معرفها
بیان مسئله
نامگذاری یک یا دوحرفی برای متغیرها، نامشخص بودن نام توابع، انتخاب اسامی طولانی برای کلاسها، علامتگذاری نام متغیرها با نوع آنها (مثلاً انتخاب نام «b_isCounted» برای یک متغیر بولی) و از همه بدتر، ترکیب کردن روشهای نامگذاری متفاوت در یک سورسکد واحد میتوانند خوانایی، فهم و تغییر کد را دشوار کنند.
راه حل
انتخاب نام مناسب برای متغیرها، توابع و کلاسها، مهارتی است که به سختی حاصل میشود. اگر به یک پروژه موجود پیوستهاید، کدهای آن را با دقت بررسی کنید و ببینید که چگونه هر یک از معرفها و نامها انتخاب شدهاند. در صورتی که دستورالعملی برای سبک نامگذاری وجود دارد، آن را به خاطر بسپارید و به آن پایبند باشید. برای پروژههای جدید، سبک نامگذاری خود را ایجاد کنید و طبق آن پیش بروید.
به طور معمول، نام متغیرها باید به صورت کوتاه و توصیفی باشد. نام توابع معمولاً باید شامل حداقل یک فعل بوده و به گونهای باشد که تنها با نگاه کردن به نام آن بتوان عملکرد تابع را تشخیص داد. با این حال، نباید کلمات زیادی در نام توابع قرار گیرد. برای کلاسها نیز شرایط به همینگونه است.
6. اعداد جادویی
بیان مسئله
فرض کنید در حال جستجو درون کدی هستید که شخص دیگری آن را نوشته است و اعدادی را پیدا میکنید که به صورت «هارد کد» (Hard Code) نوشته شدهاند. این اعداد میتوانند بخشی از یک دستور «if» یا قسمتی از یک محاسبات عجیب و غریب باشند که برای شما منطقی به نظر نمیرسد. شاید بخواهید که تابع را تغییر دهید اما اعداد موجود برای شما نامفهوم هستند و اعمال تغییرات را دشوار میکند.
راه حل
در هنگام نوشتن کد، باید به هر قیمتی از این اعداد که به عنوان «اعداد جادویی» شناخته میشوند، پرهیز کنید. اعداد هارد کد، در هنگام کدنویسی منطقی به نظر میرسند اما با گذشت زمان، امکان از فراموش کردن معنای آنها وجود خواهد داشت؛ مخصوصاً هنگامی که شخص دیگری قصد ایجاد تغییر در کد شما را داشته باشد.
یکی از راه حلهای این مشکل، درج کامنت برای توضیح عملکرد اعداد جادویی است اما تبدیل اعداد جادویی به متغیرهای ثابت (برای محاسبات) یا شمارندهها (برای دستورات شرطی مانند if-else یا switch-case) گزینه بهتری خواهد بود. با اختصاص یک نام به اعداد جادویی، خوانایی کد به طرز چشمگیری افزایش مییابد و امکان اعمال تغییرات باگدار کمتر میشود.
7. کدهای بسیار تودرتو
بیان مسئله
دو راه اصلی برای مقابله با کدهای بسیار تودرتو وجود دارد: 1) استفاده از حلقهها و 2) استفاده از عبارات شرطی. کدهای تودرتو همیشه بد نیستند اما به دلیل تجزیه و تحلیل دشوار (مخصوصاً اگر نامگذاری متغیرها به خوبی صورت نگرفته باشد) و ایجاد تغییرات دشوارتر، این کدها میتوانند مشکلساز شوند.
راه حل
اگر در حال نوشتن یک حلقه تودرتو باشید، شاید کد شما برای دسترسی به دادهها، خیلی زیاد از محدوه خود خارج شود. به جای این کار، راهی را فراهم کنید تا دادههای موجود در هر شیء یا ماژول، از طریق یک تابع فراخوانی شوند. از سوی دیگر، دستورات شرطی بسیار تودرتو، نشانه تلاش شما برای رسیدگی به تعداد زیادی منطق در یک تابع یا کلاس است. در واقع، توابع طولانی و تودرتو، رابطه نزدیکی با هم دارند و معمولاً با هم رخ میدهند.
اگر کد شما دارای دستورات «switch» طولانی یا «if-else» تودرتو است، شاید بهتر باشد که از یک «ماشین حالت» (State Machine) یا «الگوی استراتژی» (Strategy Pattern) به جای آن دستورات استفاده کنید. کدهای بسیار تودرتو، در بین برنامهنویسان کمتجربه حوزه بازی رایجتر است.
8. خطاهای «Exception»
بیان مسئله
گاهی اوقات در حین اجرای برنامه، یک خطای خاص رخ میدهد که با رسیدن به آن، اجرای کد متوقف میشود. به این خطا، «استثنا» یا اصطلاحاً «Exception» میگویند. برای جلوگیری از صدور پیغام خطای Exeption، میتوان از دستورات «throw-catch» استفاده کرد اما به کارگیری نادرست این دستورات میتواند منجر به دشوارتر شدن فرآیند اشکالزدایی شود. نادیده گرفتن یا مخفی کردن خطای «Caught Exception»، یکی از موارد استفاده نادرست از throw-catch است.
راه حل
به جای نادیده گرفتن خطاهای Caught Exception، میتوانید حداقل از مسیر پشته Exeption یک خروجی بگیرید تا در فرآیندهای احتمالی اشکالزدایی، چیزی برای بررسی وجود داشته باشد. اگر به برنامه خود اجازه دهید تا بدون هیچ علامتی خراب شود، مطمئن باشید که این مسئله در آینده مشکلاتی را به بار خواهد آورد. سعی کنید برای catch کردن چنین خطاهایی، Exception های خاص را نسبت به Exception های عمومی در اولویت قرار دهید.
9. کدهای تکراری
بیان مسئله
مشکل کدهای تکراری زمانی ظاهر میشود که شما از یک منطق در چندین محل غیر مرتبط درون برنامه خود استفاده کنید. اگر بعدها بخواهید منطق مذکور را تغییر دهید، امکان فراموشی تمام محلهای استفاده از آن وجود خواهد داشت. به عنوان مثال، شاید تنها 5 محل از 8 محل به کارگیری منطق را تغییر دهید و این امر در نهایت، منجر به رفتار متناقض و باگدار برنامه شما شود.
راه حل
یک کد تکراری، اولین گزینه برای تبدیل شدن به یک تابع است. به عنوان مثال، فرض کنیم شما در حال توسعه یک اپلیکیشن چت هستید و کد زیر را مینویسید:
String queryUsername = getSomeUsername(); boolean isUserOnline = false; for (String username : onlineUsers) { if (username.equals(queryUsername)) { isUserOnline = true; } } if (isUserOnline) { ... }
در بخشی از کد، متوجه میشوید که باید قسمت بررسی «isUserOnline» که مربوط به آنلاین بودن کاربر است را دوباره تکرار کنید. به جای کپی کردن این قسمت، میتوانید آن را در تابعی مانند زیر قرار دهید:
public boolean isUserOnline(String queryUsername) { for (String username : onlineUsers) { if (username.equals(queryUsername)) { return true; } } return false; }
اکنون، هرجایی از کد که نیاز به بررسی آنلاین بودن کاربر باشد، میتوان از این کد استفاده کرد. در هنگام نیاز به اصلاح نیز تنها باید تابع اولیه را تغییر داد تا اصلاحات در تمام نواحی فراخوانی تابع اعمال شوند.
10. کمبود کامنت
بیان مسئله
ایراد کمبود درج توضیح یا کامنت هنگامی ظاهر میشود که از هیچ کامنتی در کد استفاده نشود. در این حالت، هیچ بلوک مستندی برای توابع، اظهارنظری راجع به کلاسها، توضیحی برای الگوریتمها و غیره درون کد وجود نخواهد داشت. شاید از نظر برخی، کدی که به خوبی نوشته شده باشد، نیازی به کامنت ندارد اما واقعیت این است که حتی درک بهترین کدها نیز انرژی ذهنی زیادی میبرد.
راه حل
هدف از ایجاد مجموعه کدی با قابلیت اصلاح و تغییر آسان این است که خوانایی کد به گونهای باشد که نیازی به درج کامنت احساس نشود اما با این حال کامنت نیز درون آن وجود داشته باشد. هنگام نوشتن کامنتها، به جای توضیح راجع به کاری که یک قطعه کد انجام میدهد، دلیل وجود آن را ذکر کنید. کامنتها، از نظر روحی و روانی تأثیر مثبتی دارد. از اینرو، پیشنهاد میکنیم که آنها را نادیده نگیرید.
سخن آخر
بیشتر دلایل ناخوانا بودن کد، از درک نادرست و بیتوجهی به الگوها و قواعد مناسب برنامهنویسی نشات میگیرند. به عنوان مثال، اصلی در برنامهنویسی با عنوان «خودت را تکرار نکن» (Don't Repeat Yourself) یا اصطلاحاً «DRY» وجود دارد. این اصل، پیشنهاد میکند که به جای تکرار یک یا چند خط کد در مکانهای مختلف، آنها را در داخل یک تابع یا کلاس قرار داده و سپس در موارد مورد نیاز آنها را فراخوانی کنید. با پایبند بودن به DRY، از تکرار کدها در اکثر مواقع جلوگیری میشود. به علاوه، اصل دیگری با نام «مسئولیت واحد» (Single Responsibility)، بیان میکند که هر ماژول یا کلاس باید مسئولیت یک بخش از عملکرد نرمافزار را بر عهده داشته باشد. رعایت این قاعده نیز به وجود آمدن مشکل اشیا خدا را تقریباً غیرممکن میکند. با در نظر داشتن این اصول و اجتناب از عوامل بیان شده در این مقاله، میتوان کدی خوانا و باکیفیت نوشت.
#