کدنویسی تمیز: کامنت‌گذاری و ساختار کد

Clean Coding: Commenting and Code Structure

18 فروردین 1400
درسنامه درس 5 از سری کدنویسی تمیز
کدنویسی تمیز: کامنت گذاری و ساختار کد (قسمت 05)

به فصل جدید خوش آمدید

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

دسته بندی کامنت های بد

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

class Database {

    private dbDriver: any; // the database engine to which we connect




    lodaDatabaseDriver(driver: string) {

        if (driver === 'sql') {

            // connect to sql driver if it is set to sql

            this.dbDriver = sqlDriver;

        } else {

            // otherwise, connect to Mongo

            this.dbDriver = mongoDriver;

        }

    }




    connect() {

        this.dbDriver.connect(); // this may fail and throw an error

    }

}

همانطور که در کد بالا مشخص است تمامی توضیحات کاملا اضافی و به دردنخور هستند. مثلا در متد lodaDatabaseDriver توضیح داده ایم که اگر driver روی sql تنظیم شده بود باید به MySQL متصل شویم اما چرا؟ ‌چرا چنین اطلاعاتی را به صورت کامنت نوشته ایم؟ آیا این طبیعت دستورات if نیست؟ بنابراین چه نیازی به توضیحات اضافی است؟

دسته بعدی از کامنت های بد، کامنت هایی هستند که بخش های مختلف کد را از هم جدا می کنند. به مثال زیر توجه کنید:

// ***************

// GLOBALS

// ***************

let sqlDriver: any;

let mongoDbDriver: any;




// ***************

// CLASSES

// ***************

// ادامه کد

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

دسته سوم از کامنت های بد، کامنت های گمراه کننده هستند! به طور مثال به کد زیر توجه کنید:

// ***************

// GLOBALS

// ***************

let sqlDriver: any;

let mongoDbDriver: any;




// ***************

// CLASSES

// ***************

// Acts as an adapter, connecting models to various database engines (SQL, MongoDB)

class Database {

  private dbDriver: any; // the database engine to which we connect




  loadDatabaseDriver(driver: string) {

    if (driver === 'sql') {

      // Connect to the SQL Driver if "driver" is set to SQL

      this.dbDriver = sqlDriver;

    } else {

      // Otherwise, connect to MongoDB

      this.dbDriver = mongoDbDriver;

    }

  }




  connect() {

    this.dbDriver.connect(); // This may fail and throw an error

  }




  insertData(data: any) {

    this.dbDriver.insert(data); // updates a user

  }

در انتهای این کد متدی به نام insertData را داریم و با نگاه به کدهای درونش متوجه می شویم که وظیفه آن ثبت یک کاربر جدید است در حالی که کامنت می گوید updates a user (یعنی «یک کاربر را به روز رسانی می کند») که گمراه کننده است. طبیعتا عملیات های به روز رسانی (update) و ثبت (insert) یکی نیستند چرا که عملیات به روز رسانی فقط زمانی انجام می شود که چیزی از قبل ثبت شده و وجود داشته باشد. اگر کدها کمی پیچیده تر باشند، چنین کامنتی باعث کج فهمی دیگر توسعه دهندگان خواهد شد.

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

دسته بندی کامنت های خوب

همانطور که گفتم اکثر کامنت ها بد هستند و به دلیل عدم توانایی یا کمبود تجربه نوشته می شوند اما کامنت هایی نیز وجود دارند که خوب هستند و باید نوشته شوند. دسته اول از کامنت های خوب کامنت هایی هستند که حاوی اطلاعات حقوقی و کپی رایت می باشند به طور مثال:

// (c) Amir / Roxo.ir

// Created in 2021




let sqlDriver: any;

let mongoDbDriver: any;




class DatabaseAdapter {

  private dbEngine: any;




  loadDatabaseDriver(engine: string) {

    if (engine === 'sql') {

      this.dbEngine = sqlDriver;

    } else {

      this.dbEngine = mongoDbDriver;

    }

  }

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

دسته دوم از کامنت های خوب، کامنت هایی هستند که مسئله ای را توضیح می دهند که با استفاده از انتخاب نام خوب قابل انتقال نیست. یک مثال ساده از این کامنت ها، کامنت هایی هستند که Regex یا Regular Expression ها را توضیح می دهند. به مثال زیر توجه کنید:

// accepts [text]@[text].[text], i.e. it simply requires an "@" and a dot

const emailRegex = /\S+@\S+\.\S+/;

انتخاب نام emailRegex به ما می فهماند که این کد یک Regex برای اعتبارسنجی ایمیل ها است اما اعتبارسنجی ایمیل ها یک فرآیند واحد نیست. مثلا ممکن است یک وب سایت خاص فقط از دامنه های Google و Microsoft و Yahoo پشتیبانی کند و بخواهد تمام ایمیل های دیگر را رد کند. قوانین مختلفی برای اعتبارسنجی ایمیل وجود دارد و انجام این کار به چندین روش مختلف انجام می شود بنابراین باید توضیحات آن را در کنار Regex بنویسید. در مثال بالا تنها کاری که انجام داده ایم بررسی ساختار زیر است:

رشته . رشته @ رشته

البته Regex ما در این مثال نسبتا ساده است بنابراین توسعه دهندگان با تجربه می توانند به راحتی آن را بخوانند اما در بسیاری از مواقع Regex می تواند بسیار طولانی شود. به مثال های زیر از Regex توجه کنید:

Email Validation: /^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,5})$/

Password Validation: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/g

Hex Code: /#?([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})/g

Removing HTML Tags: /(<script(\s|\S)*?<\/script>)|(<style(\s|\S)*?<\/style>)|(<!--(\s|\S)*?-->)|(<\/?(\s|\S)*?>)/g

همانطور که می بینید Regex می تواند بسیار بسیار پیچیده تر شود و اضافه کردن کامنت حتما لازم است. کامنتی که شما برای Regex ها می نویسید نباید به سادگی «اعتبارسنجی ایمیل» باشد بلکه باید ساختار Regex را توضیح بدهید؛ اینکه دقیقا چه ساختاری را قبول می کند (مانند کامنتی که بالاتر برای مثال خودمان اضافه کرده ام).

دسته سوم از کامنت های خوب، کامنت هایی هستند که حاوی هشداری خاص هستند. این دسته از کامنت ها قسمتی خاص از کد را که ممکن است باعث ناسازگاری با برخی سیستم ها باشد توضیح می دهد. به مثال زیر توجه کنید:

// Only works in browser environment

localStorage.setItem('user', 'test@test.com');

این کامنت می گوید «فقط در محیط مرورگر کار می کند» و یک کامنت بسیار خوب است چرا که اطلاعات بسیار مهمی را در اختیار ما می گذارد. localStorage فقط در مرورگر کار می کند بنابراین اگر کسی بدون توجه به این مسئله کد ما را درون سرور خود قرار بدهد (Node.js) برنامه اش crash کرده و سرور متوقف خواهد شد.

دسته چهارم از کامنت های خوب کامنت های todo هستند. کامنت های todo کامنت هایی هستند که برای مشخص کردن برنامه های آینده کاربرد دارند. به مثال زیر توجه کنید:

// (c) Amir / Roxo.ir

// Created in 2021




let sqlDriver: any;

let mongoDbDriver: any;




class DatabaseAdapter {

  private dbEngine: any;




  loadDatabaseDriver(engine: string) {

    if (engine === 'sql') {

      this.dbEngine = sqlDriver;

    } else {

      this.dbEngine = mongoDbDriver;

    }

  }




  connect() {

    this.dbEngine.tryConnect();

  }




  insertData(data: any) {

    this.dbEngine.insert(data);

  }




  findOne(id: string) {

    // Todo: Needs to be implemented

  }

}

ما تابعی به نام findOne داریم که کارش پیدا کردن یک ردیف یا سند خاص از پایگاه داده است اما نوشتن کامل این متد کمی زمان بر است و فعلا برای انجام آن وقت نداریم. اگر آن را بدون نوشتن چیزی رها کنیم ممکن است یادمان برود و یا کدهای ناقص درون یک تابع نیمه نوشته شده را به عنوان کد کامل در نظر بگیریم بنابراین با استفاده از یک کامنت اضافه می کنیم که این قسمت نیاز به تکمیل شدن داشته و هنوز ناقص است. البته افزونه های مختلفی برای vscode و ویرایشگر های دیگر وجود دارد که به شما اجازه می دهد کامنت های Todo را هایلایت کنید با به روش خاصی به آن ها دسترسی داشته باشید.

آخرین دسته از کامنت های خوب، کامنت هایی هستند که به Document String شناخته می شوند. این مسئله به زبان برنامه نویسی شما بستگی دارد اما چنین کامنت هایی معمولا برای اضافه کردن documentation (توضیحات و مستندات یک کد، ماژول، پکیج و ...) کاربرد دارند. به طور مثال در زبان پایتون DocString را داریم. این دسته از کامنت ها توضیحات خاصی هستند که توسط IDE ها شناسایی شده و برای کاربری که از کتابخانه شما استفاده می کند نمایش داده می شوند. یک مثال ساده از زبان پایتون و روش استفاده از Docstring ها به شکل زیر است:

def my_function():

                '''Demonstrates triple double quotes

                docstrings and does nothing really.'''




                return None




print("Using __doc__:")

print(my_function.__doc__)




print("Using help:")

help(my_function)

نتیجه اجرای این کد به شکل زیر خواهد بود:

Using __doc__:

Demonstrates triple double quotes

    docstrings and does nothing really.

Using help:

Help on function my_function in module __main__:




my_function()

    Demonstrates triple double quotes

    docstrings and does nothing really.

اگر با زبان پایتون آشنا باشید می دانید که این قابلیت بسیار کاربردی است و تقریبا همه توسعه دهندگان از آن طرفداری می کنند.

Code Formatting چیست؟

code formatting یا قالب دهی به کدها فرآیندی است که در آن نحوه نمایش و قرار گرفتن کدها در خطوط مختلف را تعیین می کنیم. برخلاف کامنت ها که اکثرا باعث ضربه زدن به خوانایی کد می شدند، code formatting باعث خوانایی هر چه بیشتر کدهای ما خواهد شد و به عبارتی معنای کدهای ما را به خواننده منتقل می کند. ما می توانیم مبحث قالب دهی کدها را به دو دسته زیر تقسیم کنیم:

  • Vertical Formatting: قالب دهی عمودی مربوط به ساختار کد در محور عمودی است. به طور مثال تعداد خطوط خالی بین خط های کد و گروه بندی کدها در این دسته از قالب دهی جای می گیرند.
  • Horizontal Formatting: قالب دهی افقی مربوط به ساختار کد در یک خط (افقی) است. به طور مثال indentation (فرورفتگی ابتدای خط) و طول هر خط در این دسته از قالب دهی جای می گیرند.

قوانین قالب دهی به کدها در این دو زمینه خودشان به دو زمینه دیگر تقسیم می شوند: قوانین قالب دهی عمومی و قوانین قالب دهی زبان های خاص برنامه نویسی. ما در این فصل در مورد هر دو دسته صحبت خواهیم کرد. بیایید هر دو دسته را بررسی کنیم.

Vertical Formatting

در مورد فرمت دهی عمودی یک قانون کلی وجود دارد؛ کد شما باید به صورت عمودی مانند یک کتاب خوانده شود و تکه تکه نباشد. منظورم از تکه تکه بودن این است که برای درک قسمتی از کدها مجبور به رفتن به قسمت دیگری از کدها شویم و در چرخه ای عجیب و غریب گیر کنیم. همانطور که هنگام خواندن کتاب از صفحه یک شروع کرده و به آخر صفحه یک می رسید کدهایتان نیز تا حد امکان باید به همین شکل نوشته شده باشند. یعنی چه؟ برخی از کدها طوری نوشته شده اند که ابتدا باید انتهای کدها را بخوانیم و سپس به وسط کدها برویم و سپس به فایل دیگری برویم و سپس به ابتدای کدها برگردیم. اگر یک کتاب از صفحه ۵ شروع شود و در صفحه ۱ تمام شود چه حالی به خواننده دست می دهد؟ بنابراین کدهایتان باید دارای یک روند عمودی و بدون «پَرش» باشند. برای دستیابی به چنین روند ساده و یکدستی که توضیح داده شد باید از چند قانون ساده پیروی کنید که در این بخش توضیح خواهم داد.

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

قانون دوم جداسازی مفاهیم از یکدیگر با استفاده از spacing (فضای خالی) است. منظور من از مفاهیم ارتباط بین اجزای کد است. به طور مثال کد زیر یک کد ساده از ذخیره داده روی دیسک است:

const path = require('path');

const fs = require('fs');




class DiskStorage {

  constructor(storageDirectory) {

    this.storagePath = path.join(__dirname, storageDirectory);

    this.setupStorageDirectory();

  }




  setupStorageDirectory() {

    if (!fs.existsSync(this.storagePath)) {

      this.createStorageDirectory();

    } else {

      console.log('Directory exists already.');

    }

  }




  createStorageDirectory() {

    fs.mkdir(this.storagePath, this.handleOperationCompletion);

  }




  insertFileWithData(fileName, data) {

    if (!fs.existsSync(this.storagePath)) {

      console.log("The storage directory hasn't been created yet.");

      return;

    }

    const filePath = path.join(this.storagePath, fileName);

    fs.writeFile(filePath, data, this.handleOperationCompletion);

  }




  handleOperationCompletion(error) {

    if (error) {

      this.handleFileSystemError(error);

    } else {

      console.log('Operation completed.');

    }

  }




  handleFileSystemError(error) {

    if (error) {

      console.log('Something went wrong - the operation did not complete.');

      console.log(error);

    }

  }

}




const logStorage = new DiskStorage('logs');

const userStorage = new DiskStorage('users');




setTimeout(function () {

  logStorage.insertFileWithData('2020-10-1.txt', 'A first demo log entry.');

  logStorage.insertFileWithData('2020-10-2.txt', 'A second demo log entry.');

  userStorage.insertFileWithData('Amir.txt', 'Amir Zouerami');

  userStorage.insertFileWithData('Nastaran.txt', 'Nastaran');

}, 1500);

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

const path = require('path');

const fs = require('fs');

class DiskStorage {

  constructor(storageDirectory) {

    this.storagePath = path.join(__dirname, storageDirectory);

    this.setupStorageDirectory();

  }

  setupStorageDirectory() {

    if (!fs.existsSync(this.storagePath)) {

      this.createStorageDirectory();

    } else {

      console.log('Directory exists already.');

    }

  }

  createStorageDirectory() {

    fs.mkdir(this.storagePath, this.handleOperationCompletion);

  }

// ادامه کدها

همانطور که می بینید خواندن این کد به مراتب سخت تر از خواندن کد اول است و به خواننده سرگیجه می دهد. من از شما می خواهم به این کد نگاه کنید و نکات بیشتری را از آن استخراج کنید.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری کدنویسی تمیز توصیه می‌کند:
نویسنده شوید
دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.