Smelly Code: اصطلاحی حاکی از اینکه یک جای سورس‌کد می‌لنگد!

Smelly Code: اصطلاحی حاکی از اینکه یک جای سورس‌کد می‌لنگد!

ممکن است شما هم تاکنون به کدهایی برخورده باشید که به نظر می‌رسد کاملاً درست و بی‌اشکال هستند، اما با این وجود باز هم حس می‌کنید که به اصطلاح یک جای کار می‌لنگد! این‌ها همان Smelly Code هستند. در برنامه‌نویسی، این اصطلاح به مجموعه‌ای از علائمی اطلاق می‌شود که از وجود یک مشکل عمیق در سورس‌کد خبر می‌دهند. در واقع، هر علامتی که نشان دهد بخشی از کد شما نیاز به ریفکتور شدن دارد را می‌توان Smelly Code (کد مشکوک) نامید.

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

در این پست به ۱۰ مورد از رایج‌ترین علائم و نشانه‌های کدهای مشکوک اشاره نموده و به شما خواهیم گفت که برای پیدا کردن منشأ شَک باید به دنبال چه چیزی بگردید و چگونه می‌توانید آن را مرتفع سازید. پس اگر با کدهای مشکوک سروکار دارید -به ویژه اگر هنوز یک برنامه‌نویس تازه‌کار هستید- در ادامه راه‌کارهای خوبی در اختیار شما قرار خواهد گرفت.

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

class Worker {
  Bike bike = new Bike();
  public void commute() {
    bike.drive();
  }
}

در اینجا Worker و Bike دچار مشکل Tight Coupling شده‌اند. حال اگر روزی قرار باشد که یکی از این آبجکت‌ها عوض شود -مثلاً به جای Bike از Car استفاده شود- چه کار باید کرد؟ برای این کار مجبورید به کلاس Worker مراجعه نموده و سپس تمام کدهای مربوط به Bike را با کدهای مربوط به Car جایگزین کنید. این کار، پردردسر بوده و احتمال اشتباه نیز در آن زیاد است.

با افزودن یک لایهٔ به اصطلاح Abstraction (انتزاع) به نرم‌افزار، می‌توان پیوند میان این دو عنصر را کمی سُست‌تر نمود. در مثال فوق، کلاس Worker ممکن است به جز Bike، مایل باشد از وسایل دیگری همچون Truck ،Car و یا Scooter هم استفاده کند. از آنجا که همهٔ این‌ها وسائل نقلیه هستند، می‌توان همهٔ آنها را تحت عنوان Vehicle (وسیلهٔ نقلیه) تعریف نمود و در موقع نیاز هر یک از این وسائل را جایگزین دیگری نمود:

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() {
  }
}

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

نخست اینکه سایر کلاس‌ها و ماژول‌ها از لحاظ دیتا، به این اَبَرکلاس یا اَبَرماژول وابسته خواهند بود (و مشکل Tight Coupling ایجاد خواهد شد) و دوم اینکه ساختار کلی برنامه حالتی ناهمگن و غیرعادی به خود خواهد گرفت؛ زیرا تقریباً همه‌چیز در یک کلاس یا ماژول جای داده شده است.

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

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;
  }
}

در این کد یک کلاس خیلی بزرگ به نام User تعریف شده است. همان‌طور که گفته شد، وجود چنین کلاسی مشکل‌زا خواهد بود و بهتر است که این کلاس به صورت زیر ریفکتور شود:

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;
  }
}

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

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

فانکشن‌های طولانی باید به فانکشن‌های فرعی کوچک‌تری تقسیم شوند؛ به‌ طوری که هر فانکشن فرعی تنها یک وظیفه را بر عهده داشته باشد و تنها برای رفع یک مشکل نوشته شده باشد. با تبدیل هر فانکشن طولانی به مجموعه‌ای از فانکشن‌های فرعی، کد تمیزتر و خواناتری خواهیم داشت.

Excessive Parameter 
فانکشن و یا کلاسی که نیاز به پارامترهای خیلی زیادی دارد، به دو دلیل مشکل‌ساز است؛ اول اینکه خوانایی کد را کاهش می‌دهد و تست نمودن آن را دشوارتر می‌کند و دلیل دوم و مهم‌تر اینکه معمولاً چنین فانکشن یا کلاسی هدف مشخص و تعیین‌شده‌ای ندارد و گویا قرار است انجام وظایف زیادی به عهدهٔ آن گذاشته شود.

هرچند در رابطه با تعداد پارامترها در هر فانکشن قانون مشخصی وجود ندارد، اما عموماً توصیه می‌شود که در هر فانکشن بیش از ۳ پارامتر تعریف نشود (البته گاهی بر حسب ضرورت می‌توان تا ۵ پارامتر را نیز در یک فانکشن جای داد اما تنها به شرطی که دلیل قانع‌کننده‌ای برای این کار وجود داشته باشد).

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

Poorly-named Identifier 
انتخاب نام‌های یک یا دو حرفی برای متغیرها، نام‌گذاری فانکشن‌ها با نام‌های نامتناسب و غیرمعمول، طولانی کردن بیش از حد نام‌ها بدون دلیل موجه، نام‌گذاری متغیرها بر اساس نوع آنها (مثلاً نام b_isCounted برای یک متغیر نوع بولین) و بدتر از همهٔ این‌ها نام‌گذاری با ترکیبی از روش‌های مختلف در یک کدبیس، موجب می‌شود تا خوانایی کد کاهش یافته، درک آن دشوارتر شده و نگهداری آن بسیار سخت گردد.

انتخاب نام‌های خوب و مناسب برای متغیرها، فانکشن‌ها و کلاس‌ها مهارتی است که به سادگی به دست نمی‌آید. اگر به تازگی به پروژه‌ای پیوسته‌اید که بخشی از آن قبلاً نوشته شده است، کدهای قبلی را بررسی کنید و ببینید که نام‌گذاری‌ها بر چه اساسی صورت گرفته است و همان روش را ادامه دهید.

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

به طور کلی، نام متغیر باید کوتاه اما توصیفی باشد (مثلاً برای متغیری که قرار است شناسهٔ کاربر را در خود ذخیره سازد، نامی همچون userId در نظر بگیرید). نام فانکشن‌ها حداقل باید شامل یک فعل باشد که به وضوح مشخص کند آن فانکشن چه کاری انجام می‌دهد (به طور مثال، برای فانکشنی که قرار است قیمت سبد خرید یک فروشگاه آنلاین را محاسبه کند، نامی همچون calculateCart برگزینید). به علاوه، از به کار بردن کلمات متعدد در نام‌ها خودداری کنید. همچنین برای فانکشن‌ها از کلماتی استفاده کنید که ویژهٔ آنها بوده و برای موارد دیگر -مثلاً کلاس‌ها- آن کلمات را به کار نبرید.

Magic Number 
در حین بررسی‌ کدهای قدیمی خودتان یا کدهایی که دیگران نوشته‌اند، ممکن است به اعدادی بربخورید که از آنها سر درنمی‌آورید و یا حتی شاید وجود آنها نامعقول به نظر برسد.

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

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

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

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

Deep Nesting 
دو عامل عمده ممکن است به ایجاد کدهای عمیقاً تو در تو (Deeply-nested Code) منجر شود که عبارتند از حلقه‌ (Loop) و دستور شرطی (Conditional Statement). کدهای عمیقاً‌ تو در تو همیشه‌ هم بد نیستند اما می‌توانند مشکل‌ساز شوند زیرا تجزیهٔ آنها دشوار و اصلاح آنها دشوارتر است (به ویژه در صورتی که متغیرها به خوبی نامگذاری نشده باشند).

به جای اینکه حلقه‌های تو در توی دوگانه، سه‌گانه و یا حتی چهارگانه بنویسید، بهتر است از فانکشن‌هایی مجزا برای انجام محاسبات مد نظر خود استفاده کنید. از سوی دیگر، دستورات شرطی عمیقاً تو در تو،‌ معمولاً‌ نشانه‌ای از این هستند که شما می‌خواهید حجم بزرگی از وظایف منطقی را در یک فانکشن یا یک کلاس جای دهید و این منجر به ایجاد مشکل فانکشن‌های طولانی خواهد شد (در واقع، دو مشکل Deep Nesting و Long Function معمولاً با هم اتفاق می‌افتند).

Unhandled Exceptions
دولوپرهای تنبل که به کرات با استفاده از بلوک try-catch اقدام به هَندل کردن اکسپشن‌ها می‌کنند، در بیشتر مواقع فرایند دیباگینگ در آینده را اگر نگوییم غیرممکن، بلکه بسیار دشوار خواهند کرد.

راه‌کار خوب این است که یک دولوپر حرفه‌ای باید تمرکز خود را روی اکسپشن‌های به خصوصی متمرکز کند و با لاگ‌گیری مناسب، راه‌ را برای فرایند دیباگینگ در آینده هموار سازد. 

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

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

String queryUsername = getSomeUsername();
boolean isUserOnline = false;

for (String username : onlineUsers) {
  if (username.equals(queryUsername)) {
    isUserOnline = true;
  }
}

if (isUserOnline) {
  ...
}

اما بهتر است که آن را به شکل زیر و به عنوان یک فانکشن بنویسید تا دیگر نیازی به کپی/پیست کردن آن در بخش‌های مختلف برنامه نداشته باشید، به راحتی بتوانید آن را فراخوانی نموده و در صورت نیاز آن را اصلاح کنید:

public boolean isUserOnline(String queryUsername) {
  for (String username : onlineUsers) {
    if (username.equals(queryUsername)) {
      return true;
    }
  }
  return false;
}

Lack of Comments
ممکن است در حال بررسی کدی باشید و متوجه شوید که به جز کد، هیچ چیز دیگری در آن نمی‌بینید و این اصلاً خوب نیست. در یک سورس‌کد خوب، هر چند کاملاً درست و واضح نوشته‌ شده باشد، باز هم وجود کامنت ضروری است.

باید در مورد کاربری کلاس‌ها، عملکرد فانکشن‌ها و الگوریتم‌ها و سایر موارد توضیحاتی نوشته شود تا کد قابل‌فهم‌تر شده و در آینده توسعه و ویرایش آن با مشکل مواجه نشود.

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

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

منبع


رائفه خلیلی