کدنویسی تمیز: قالب‌بندی افقی و حل تمرین

Clean Coding: Horizontal Formatting and Practice Solving

18 فروردین 1400
درسنامه درس 6 از سری کدنویسی تمیز
کدنویسی تمیز: قالب بندی افقی و حل تمرین (قسمت 06)

نزدیک نگه داشتن مفاهیم مرتبط

در جلسه قبل کد زیر را به شما نشان دادم و توضیح دادم که چرا باید فاصله گذاری عمودی یا vertical formatting را انجام بدهید:

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

  }

// ادامه کدها

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

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('Ahmad.txt', 'Ahmad');

}, 1500);

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

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('Ahmad.txt', 'Ahmad');

}, 1500);

همانطور که می بینید بین خطوط این کد فاصله ای وجود ندارد چرا که همگی با هم یک کار خاص را انجام می دهند و از هم جدا نیستند.

روند خوانده شدن کد

قبل از اینکه بخواهیم وارد بحث قالب بندی افقی بشویم باید نکته خاصی را توضیح بدهیم. در کد بالا (کلاس DiskStorage) سعی شده است که تا حد ممکن متدهای مرتبط،‌ نزدیک هم تعریف شوند. به طور مثال متد 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.');

    }

  }

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

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

start();




function start() {

  console.log('start');

  next();

}




function next() {

  console.log('next');

  last();

}




function last() {

  console.log('last');

}

اگر کد جاوا اسکریپت بالا را اجرا کنید، همه چیز بدون مشکل اجرا خواهد شد؛ یعنی ابتدا متد start اجرا شده و سپس متد next اجرا شده و سپس متد last اجرا خواهد شد. حالا به مثال زیر از زبان پایتون توجه کنید:

# کار نمی کند

# start()







def start():

    print('Start')

    next()







# کار نمی کند

# start()







def next():

    print('Next')

    last()







def last():

    print('Last')




# کار می کند

start()

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

قالب دهی افقی (Horizontal Formatting)

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

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

class DiskStorage { constructor(storageDirectory) { this.storagePath = path.join(__dirname, storageDirectory); this.setupStorageDirectory(); } }

این همان کلاس DiskStorage است اما این بار constructor را به همراه تعریف کلاس در یک خط نوشته ام و از indentation استفاده نکرده ام. فکر نمی کنم نیاز به توضیح باشد که چرا چنین کدی به راحتی خوانده نمی شود. هر زمان که متوجه شدید کدهای شما بیش از حد طولانی شده اند بهتر است آن ها را بشکنید و در خط جدیدی قرار بدهید.

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

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");

userStorage.insertFileWithData("Ahmad.txt", "Ahmad");

}, 1500);

من در کد بالا تمام indentation ها را حذف کرده ام و همانطور که می بینید خواندن این کد چندین برابر دشوار شده است و در نگاه اول انگار یک تابع بسیار بزرگ را داریم.

قانون سوم قالب دهی افقی این است که statement های خود را به statement های کوچکتری تقسیم کنید. به طور مثال در جاوا اسکریپت (node.js) تابعی به نام mkdir وجود دارد که یک پوشه جدید می سازد و دو آرگومان می گیرد: آرگومان اول آدرس پوشه جدید و آرگومان دوم یک تابع callback است که پس از اتمام عملیات اجرا می شود. به کد زیر توجه کنید:

  createStorageDirectory() {

    fs.mkdir(path.join(__dirname, 'temp', '2021-3', 'images'), this.handleOperationCompletion);

  }

برای ساخت مسیر در جاوا اسکریپت باید از path.join استفاده کنیم تا پوشه ها را به شکل صحیح به هم بسچبانیم چرا که نحوه آدرس دهی در لینوکس و ویندوز متفاوت است (استفاده از / و \) بنابراین اگر آدرس دهی را به صورت دستی انجام بدهیم، کدها فقط در یک سیستم عامل خاص اجرا می شوند. مشکل کد بالا این است که statement ما بیش از حد طولانی است، یعنی عملیات تولید آرگومان اول یا ساخت آدرس را در همان خط انجام داده ایم که باعث طولانی شدن بیش از حد این خط شده است. ما می توانستیم کد بالا را به شکل زیر بنویسیم:

  createStorageDirectory() {

    const storagePath = path.join(__dirname, "temp", "2021-3", "images");

    fs.mkdir(storagePath, this.handleOperationCompletion);

  }

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

  createStorageDirectory() {

    const storagePathForStoringImagesInATemporaryFolderFor2020 = path.join(__dirname, "temp", "2021-3", "images");

    fs.mkdir(storagePathForStoringImagesInATemporaryFolderFor2020, this.handleOperationCompletion);

  }

انتخاب چنین نامی که بیش از حد توصیفی است باعث شلوغ شدن کدها و ناخوانایی آن ها می شود.

چالش و حل مسئله

حالا که با مفاهیم ساختار کد و قالب دهی آشنا شده ایم، نوبت به یک چالش و حل مسئله است. من یک کد پایتون را برایتان آماده کرده ام که دارای چند مشکل کوچک است و شما باید این مشکلات را حل کنید:

# (c) Amir Zouerami / Roxo.ir




# *********

# Imports

# *********

from os import path, makedirs

from pathlib import Path




# *********

# Main

# *********

# A class which allows us to create DiskStorage instances







class DiskStorage:

    def __init__(self, directory_name):

        self.storage_directory = directory_name




    def get_directory_path(self):

        return Path(self.storage_directory)




    # این تابع باید قبل از ثبت فایل صدا زده شود

    def create_directory(self):

        if (not path.exists(self.get_directory_path())):

            makedirs(self.storage_directory)




    # پوشه مورد نظر باید از قبل وجود داشته باشد

    def insert_file(self, file_name, content):

        file = open(self.get_directory_path() / file_name, 'w')

        file.write(content)

        file.close()

        # Todo: سیستم مدیریت خطا اضافه شود







log_storage = DiskStorage('logs')




log_storage.insert_file('test.txt', 'Test')

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

من ابتدا با کامنت ها شروع می کنم. در ابتدا یک کامنت داریم که نویسنده فایل را مشخص می کند و توضیح دادیم که این کامنت ها مشکلی ندارند و کاملا مجاز هستند. در مرحله بعدی کامنت هایی را داریم که بخش های مختلف فایل را از هم جدا می کنند و من برایتان توضیح دادم که این کامنت ها اصلا مناسب نیستند. علاوه بر آن کامنتی داریم که توضیح می دهد کلاس DiskStorage به ما اجازه می دهد از آن یک شیء بسازیم! این کامنت نیز کامنت بسیار بدی است چرا که تمام کلاس ها همینطور هستند و این کلاس کار خاصی را نمی کند. توضیحاتی که در این کامنت نوشته شده است به هیچ عنوان کارایی ندارند و حتی doc string نیز نیستند. برای نوشتن کامنت های documentation که در پایتون به doc string معروف هستند باید از سه علامت quote استفاده کنیم:

"""

the documentation goes here

"""

بنابراین هیچ کدام از این کامنت ها کاربردی نیستند و آن ها را حذف می کنیم:

# (c) Amir Zouerami / Roxo.ir




from os import path, makedirs

from pathlib import Path







class DiskStorage:

// ادامه کدها

در ادامه دو کامنت دیگر را نیز داشتیم که هر دو warning (هشدار) به حساب می آیند:

// بقیه کدها

    # این تابع باید قبل از ثبت فایل صدا زده شود

    def create_directory(self):

        if (not path.exists(self.get_directory_path())):

            makedirs(self.storage_directory)




    # پوشه مورد نظر باید از قبل وجود داشته باشد

    def insert_file(self, file_name, content):

        file = open(self.get_directory_path() / file_name, 'w')

        file.write(content)

        file.close()

// بقیه کدها

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

// بقیه کدها

    def create_directory(self):

        if (not path.exists(self.get_directory_path())):

            makedirs(self.storage_directory)




    # پوشه مورد نظر باید از قبل وجود داشته باشد

    def insert_file(self, file_name, content):

        file = open(self.get_directory_path() / file_name, 'w')

        file.write(content)

        file.close()

// بقیه کدها

کامنت نهایی ما نیز یک کامنت TODO است که قبلا نحوه کارشان را توضیح دادم. مسئله اینجاست که بهتر است به جای نوشتن «پیاده سازی سیستم مدیریت خطا» واقعا یک سیستم مدیریت خطا را پیاده سازی کنیم و آن را به زمان دیگری موکول نکنیم اما به عنوان یک یادآوری موقت مشکلی در استفاده از آن وجود ندارد بنابراین آن را نیز باقی می گذاریم. همچنین از نظر قالب دهی عمودی کدها مشکلی ندارند و خوانا محسوب می شوند و تنها مسئله ای که دارند در انتهای فایل و وجود اسپیس میان دو خط انتهایی کد است:

log_storage = DiskStorage('logs')




log_storage.insert_file('test.txt', 'Test')

این دو خط به هم مرتبط هستند و اضافه کردن فاصله به آن ها کار جالبی نیست بنابراین من این فاصله را حذف می کنم. همچنین باید به کامنت هشدار خودمان نیز گوش داده و دستور create_directory را  قبل از insert کردن فایل ها صدا بزنیم. همچنین یک روش دیگر فکر کردن درباره این موضوع این است که بگوییم نمونه سازی از یک کلاس متفاوت با اجرای عملیت های مختلف روی شیء ساخته شده است بنابراین باید این دو خط جدا باشند:

log_storage = DiskStorage('logs')




log_storage.create_directory()

log_storage.insert_file('test.txt', 'Test')

به هر روشی که عمل می کنید باید دلیلی برای آن داشته باشید.

از نظر قالب دهی افقی نیز داری هیچ خط طولانی نیستیم اما بهتر است خط زیر را بشکنیم تا خوانا تر شود:

file = open(self.get_directory_path() / file_name, 'w')

در این خط مسیر فایل مورد نظر را ساخته ایم اما همه چیز به صورت آرگومان اول محاسبه شده است. با اینکه این کد، کد بدی نیست اما هنوز هم می تواند بهتر شود:

file_path = self.get_directory_path() / file_name

file = open(file_path, 'w')

بنابراین کد تصحیح شده نهایی ما به شکل زیر خواهد بود:

# (c) Amir Zouerarmi / Roxo.ir




from os import path, makedirs

from pathlib import Path







class DiskStorage:

    def __init__(self, directory_name):

        self.storage_directory = directory_name




    def get_directory_path(self):

        return Path(self.storage_directory)




    def create_directory(self):

        if (not path.exists(self.get_directory_path())):

            makedirs(self.storage_directory)




    # پوشه مورد نظر باید از قبل وجود داشته باشد

    def insert_file(self, file_name, content):

        file_path = self.get_directory_path() / file_name

        file = open(file_path, 'w')

        file.write(content)

        file.close()

        # Todo: سیستم مدیریت خطا اضافه شود







log_storage = DiskStorage('logs')




log_storage.create_directory()

log_storage.insert_file('test.txt', 'Test')


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

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