کدنویسی تمیز: اشتباهات نام‌گذاری و حل تمرین

Clean Coding: Spelling Mistakes and Solving Exercises

17 فروردین 1400
درسنامه درس 4 از سری کدنویسی تمیز
کدنویسی تمیز: اشتباهات نام گذاری و حل تمرین (قسمت 4)

اشتباهات رایج در نام گذاری

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

userWithNameAndAge = User('Amir', 25)

عبارت userWithNameAndAge یعنی «کاربر با نام و سن». جدا از اینکه انتخاب نام های طولانی برای متغیرها باعث سنگین تر شدن کد و همچنین شلوغ شدن آن می شود، اطلاعات ارائه شده (نام و سن) بدون انتخاب چنین نامی مشخص هستند. آرگومان اول (Amir) یک نام است و از آنجایی که نام کلاس User است به احتمال زیاد آرگومان دوم سن می باشد بنابرین هیچ کمکی به خوانایی کد نکرده ایم. تصور کنید که کلاس User آرگومان های بیشتری مانند «تفریحات» و «سطح دسترسی» و «علاقه مندی ها» را نیز بگیرد. در این حالت نام متغیرها بیش از حد طولانی خواهد شد.

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

اشتباه رایج دوم این است که توسعه دهندگان از عبارات مخفف عجیب و غریب یا اصطلاحات کمتر شناخته شده استفاده می کنند. اگر به گیت هاب نگاهی بیندازید می بینید که حتی در برخی از موارد توسعه دهندگان از اصطلاحات خیابانی و شوخی های عجیب برای نام گذاری استفاده می کنند. به طور مثال استفاده از product.diePlease (عبارت die please یعنی «لطفا بمیر») یا user.facePalm (عبارت face palm یعنی گذاشتن دست روی صورت به نشانه تاسف) اشتباه است. با اینکه این عبارات لطیفه گونه هستند و شاید برای شما خنده دار باشند اما اگر دیگر توسعه دهندگان کد شما را بخوانند مجبور خواهند بود که وقت بیشتری برای شناسایی این خصوصیات و متدها بگذارند. به جای این دو نام می توانیم از نام های صحیح مانند product.remove و user.sendErrorMessage استفاده کنیم.

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

message(n)

ymdt = '20210121CET'

ما در اینجا معنای حرف n یا معنای ymdt را نمی دانیم بنابراین باید برای درک آن وقت بگذریم. احتمالا منظور از ymdt چیزی شبیه Year Month Day Time باشد اما لزوما مشخص نیست. نام صحیح و بهتر برای این مقادیر به شکل زیر خواهد بود:

message(newUser)

dataWithTimezone = '20210121CET'

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

userList = { u1: ... , u2: ... }

allAccounts = accounts.filter()

در مثال اول، نام متغیر را userList گذاشته ایم اما این غلط است چرا که ما یک شیء (Object) را داریم نه یک لیست! ما می دانیم که در زبان های برنامه نویسی list معنای خاصی دارد و همان آرایه (Arrray) است بنابراین با استفاده از عبارت list ممکن است توسعه دهندگان دیگر را گمراه کنیم. در مثال دوم نیز نام allAccounts را انتخاب کرده ایم ما این نیز غلط است. allAccounts یعنی «تمام حساب ها» در صورتی که متد filter را روی حساب ها صدا زده ایم و برخی از آن ها را کنار گذاشته ایم. بدین ترتیب «تمام حساب ها» را نداریم بلکه برخی از آن ها را داریم. نام های بهتر برای این دو مثال به شکل زیر است:

userMap = { u1: ... , u2: ... }

filteredAccounts = accounts.filter()

من در اینجا از نام filteredAccounts (حساب های فیلتر شده) استفاده کرده ام که یک نام کلی است. شما می توانید بر اساس برنامه ای که نوشته اید، این نام را تغییر بدهید. به طور مثال در این کد حساب ها را بر چه اساسی فیلتر می کنیم؟ اگر بر اساس کاربران VIP است می توانیم نام متغیر را VIPAccounts بگذاریم یا اگر بر اساس کاربرانی است که حساب پولی دارند می توانیم نام paidAccounts را انتخاب کنیم.

مشکل بعدی استفاده از نام های شبیه به یکدیگر است. اگر می خواهید کیفیت و خوانایی کدهایتان بالاتر برود باید نام هایی «تفکیک پذیر» انتخاب کنید، یعنی اگر یک نفر به کدهایتان نگاهی انداخت بتواند بین متغیرها یا متدهای مختلف تفاوت قائل شود. مثلا فرض کنید که یک کلاس به نام Analytics دارید که درون آن متدهای مختلفی برای کار با آمار وب سایت خود وجود دارد. استفاده از متدهایی مانند متدهای زیر یک مشکل اساسی دارد:

analytics.getDailyData(day)

analytics.getDayData(day)

analytics.getRawDailyData(day)

analytics.getParsedDailyData(day)

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

نام های بهتر برای این مثال به شکل زیر هستند:

analytics.getDailyReport(day)

analytics.getDataForToday(day)

analytics.getRawDailyData(day)

analytics.getParsedDailyData(day)

نام getDailyReport به معنی دریافت گزارش روزانه است بنابراین می دانیم که می خواهیم یک فایل PDF را دانلود کنیم که گزارش آمار روزانه ما را دارد. متد getDataForToday واضح تر از getDayData است چرا که مشخص است اطلاعات امروز را به ما می دهد. با تصحیح این دو نام، نام های getRawDailyData و  getParsedDailyData نیز به طور خودکار تفکیک پذیر می شوند چرا که با متدی مانند getDailyData ترکیب نشده و باعث سردرگمی نمی شوند.

آخرین مشکلی رایجی که در کدهای کاربران دیده می شود عدم وجود «انسجام» در سورس کد است. فرض کنید می خواهید متدی را برای دریافت کاربران تعریف کنید. نام این متد می تواند getUsers یا fetchUsers یا retrieveUsers باشد. هر سه نام کاملا خوانا بوده و هیچ مشکلی ندارند اما هر کدام را که انتخاب کردید باید در سر تا سر سورس کد از همان نام استفاده کنید. یعنی چه؟ به کد زیر توجه کنید:

database = Database()

database.get_users()

database.get_products()

database.fetch_products()

اگر می خواهیم از get به عنوان کلمه «دریافت» استفاده کنیم باید در تمام سورس کد از get استفاده کنیم اما در کد بالا یک get_products داریم و سپس یک fetch_products نیز وجود دارد. تفاوت این دو چیست؟ آیا بین get و fetch در برنامه شما تفاوت خاصی وجود دارد یا اینکه هر دو متد تقریبا یک کار را انجام می دهند؟

زمان حل تمرین است!

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

from datetime import datetime







class Entity:

    def __init__(self, title, description, ymdhm):

        self.title = title

        self.description = description

        self.ymdhm = ymdhm







def output(item):

    print('Title: ' + item.title)

    print('Description: ' + item.description)

    print('Published: ' + item.ymdhm)







summary = 'Clean Code Is Great!'

desc = 'Actually, writing Clean Code can be pretty fun. You\'ll see!'

new_date = datetime.now()

publish = new_date.strftime('%Y-%m-%d %H:%M')




item = Entity(summary, desc, publish)




output(item)

ما در این کد یک کلاس به نام entity داریم که سه خصوصیت title و description و ymdhm را تعریف می کند. در مرحله بعدی یک تابع را داریم (متد نیست، خارج از کلاس است) که این خصوصیات را چاپ می کند. نهایتا تاریخ و مقادیر مورد نیاز را تعریف کرده و از کلاس entity نمونه سازی کرده ایم. از شما می خواهم که فرض کنید entity یک blog post است (یک پست ساده). بر این اساس می توانیم کدهای بالا را به روش بسیار بهتری بنویسیم. در ابتدا نام کلاس را از entity به blog post تغییر می دهیم:

from datetime import datetime







class BlogPost:

    def __init__(self, title, description, date_published):

        self.title = title

        self.description = description

        self.date_published = date_published

علاوه بر تغییر نام کلاس، نام ymdhm را نیز به date_published تغییر داده ایم تا واضح تر باشد. در مرحله بعدی بهتر است نام تابع output را به print_blog_post تغییر بدهیم چرا که توصیفی است و دقیقا مشخص می کند وظیفه این تابع چیست. کلمه output (خروجی) یا کلماتی کلی مانند آن (مثل print_data) نام های خوبی نیستند چرا که کلی هستند بنابراین ممکن است فردی تصور کند کار آن ها چاپ کردن هر نوع داده ای است در حالی که متد output به دنبال خصوصیات شیء blogPost می گردد و اگر این خصوصیات پیدا نشوند کد ما به خطا برخورد می کند.

def print_blog_post(blog_post):

    print('Title: ' + blog_post.title)

    print('Description: ' + blog_post.description)

    print('Published: ' + blog_post.date_published)

من نام پارامتر را نیز به blog_post تغییر داده ام تا واضح تر باشد. در نهایت متغیر summary (به معنی «خلاصه») را داریم که اصلا نام خوبی نیست چرا که summary در واقع همان عنوان پست است بنابراین باید از کلمه title استفاده کنیم:

title = 'Clean Code Is Great!'

description = 'Actually, writing Clean Code can be pretty fun. You\'ll see!'

now = datetime.now()

formatted_date = now.strftime('%Y-%m-%d %H:%M')




blog_post = BlogPost(title, description, formatted_date)




print_blog_post(blog_post)

من این کد را در چند قسمت دیگر نیز تغییر داده ام. ابتدا متغیر desc را داشتیم که مخفف است و برای بسیاری از توسعه دهندگان معنی واضحی دارد اما من ترجیح می دهم نام آن را به صورت مخفف ننوشته و از description استفاده کرده ام. در مرحله بعدی متغیر new_date یا تاریخ جدید را داشتیم که نام بدی نیست اما توصیفی و واضح نیز نمی باشد بنابراین بهتر است نامش را now بگذاریم چرا که تاریخ حال حاضر را دریافت کرده ایم. در نهایت متغیر publish به معنای «انتشار» را داشتیم که اصلا نام مناسبی نیست و من آن را به formatted_date (تاریخ فرمت شده) تغییر داده ام. حالا زمانی که می خواهیم یک نمونه از کلاس BlogPost را ایجاد کنیم باید آن را در متغیری به نام blog_post ذخیره کنیم. نام قبلی (item) نامی بسیار عمومی است و اصلا مشخص نمی کند که محتوای این متغیر چه چیزی خواهد بود. ما به همین سادگی توانسته ایم کد خود را خوانا و با کیفیت کنیم اما من دوست دارم دوباره تاکید کنم که این تنها روش ممکن برای تصحیح این کد نیست؛ ممکن است شما بر اساس سلیقه خود، کدها را طوری تصحیح کرده باشید که با کدهای من فرق داشته باشد. مهم این است که اگر کسی برای اولین بار کدهای شما را مطالعه کرد، متوجه طرز کار آن ها بشود.

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

from datetime import datetime







class BlogPost:

    def __init__(self, title, description, date_published):

        self.title = title

        self.description = description

        self.date_published = date_published




    def print(self):

        print('Title: ' + self.title)

        print('Description: ' + self.description)

        print('Published: ' + self.date_published)







title = 'Clean Code Is Great!'

description = 'Actually, writing Clean Code can be pretty fun. You\'ll see!'

now = datetime.now()

formatted_date = now.strftime('%Y-%m-%d %H:%M')




blog_post = BlogPost(title, description, formatted_date)

blog_post.print()

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

چالشی برای شما

من کدی را برای شما آماده کرده ام که از نظر کیفی در سطح پایینی قرار دارد و اصلا خوانا نیست. این کد در زبان پایتون نوشته شده است. من از شما می خواهم که آن را بازنویسی کرده و خواناتر کنید.

class Point:

    def __init__(self, coordX, coordY):

        self.coordX = coordX

        self.coordY = coordY







class Rectangle:

    def __init__(self, starting_point, broad, high):

        self.starting_point = starting_point

        self.broad = broad

        self.high = high




    def area(self):

        return self.broad * self.high




    def end_points(self):

        top_right = self.starting_point.coordX + self.broad

        bottom_left = self.starting_point.coordY + self.high

        print('Starting Point (X)): ' + str(self.starting_point.coordX))

        print('Starting Point (Y)): ' + str(self.starting_point.coordY))

        print('End Point X-Axis (Top Right): ' + str(top_right))

        print('End Point Y-Axis (Bottom Left): ' + str(bottom_left))







def build_stuff():

    main_point = Point(50, 100)

    rect = Rectangle(main_point, 90, 10)




    return rect







my_rect = build_stuff()




print(my_rect.area())

my_rect.end_points()

همانطور که می بینید این کد یک کلاس برای ساخت point (نقاط محورهای X و Y) و یک کلاس برای ساخت rectangle (مستطیل) را دارد. سعی کنید خودتان این کد را بازنویسی کنید و سپس به پاسخ من نگاه کنید.

در قسمت اول از این کد نقاط محورهای X و Y را داریم که با نام های coordX و coordY مشخص شده اند. این نام ها بد نیستند اما اطلاعات اضافی دارند. نام کلاس ما point یا نقطه است بنابراین مشخص است که X و Y نقاط افقی و عمودی هستند و نیازی به ذکر coord (مخفف coordinate یا «مختصات») نیست. با این حساب می توانیم به شکل زیر عمل کنیم:

class Point:

    def __init__(self, x, y):

        self.x = x

        self.y = y

در مرحله بعدی به کلاس rectangle یا مستطیل می رسیم. پارامتر اول starting_point نام دارد که نام خوبی است و نیازی به تعویض ندارد. starting_point یا نقطه آغازین همان نقطه ای است که رسم مستطیل از آن شروع خواهد شد. با این حال من شخصا نام ساده ای مانند origin را ترجیح می دهم بنابراین starting_point را به آن تغییر خواهم داد. پارامترهای بعدی broad (به معنای «عریض») و high (به معنای «مرتفع») را داریم که اصلا نام های خوبی نیستند و حتما باید عوض شوند. من به جای این این دو نام، از نام های width (عرض) و height (ارتفاع) استفاده می کنم.

class Rectangle:

    def __init__(self, origin, width, height):

        self.origin = origin

        self.width = width

        self.height = height

در بخش بعدی متدی به نام area را داریم. اگر این متد فقط یک boolean را برمی گرداند این نام می توانست مناسب باشد اما این متد مساحت (area) را محاسبه می کند بنابراین نام بهتر getArea یا calculateArea می باشد. طبیعتا با تغییر نام خصوصیات (width و height) باید این خصوصیات را در متدهای بعدی نیز تغییر بدهیم. از نظر من در متد end_points هیچ نام بدی وجود ندارد. این متد نقاط مختلف مستطیل را مشخص و چاپ می کند و نام هایی مانند top_right (نقطه بالا و راست) نام هایی توصیفی و خوب هستند بنابراین چیزی را در آن تغییر نمی دهم اما خود نام end_points کمی گنگ است. این متد وظیفه چاپ نقاط مختلف مستطیل را دارد بنابراین از نظر من نام print_coordinates بسیار بهتر است.

در نهایت متد build_stuff را داریم که نام بسیار بدی است. این متد مسئول ساخت یک مستطیل است بنابراین باید نامش build_rectangle باشد. متغیر main_point (به معنای «نقطه اصلی») در این متد نیز نام خیلی خوبی نیست (گرچه بد نیز محسوب نمی شود) بنابراین من ترجیح می دهم آن را به rectangle_origin تغییر بدهم تا مشخص شود که این نقطه، نقطه آغازین مستطیل است. نهایتا متغیر rect را داریم که یک عبارت مخفف شده است اما هیچ دلیل برای انجام این کار نیست بنابرین من آن را به rectangle تغییر می دهم. طبیعتا my_rectangle نیز نام بسیار بدی است و آن را به rectangle تغییر می دهیم. با این حساب کد تکمیل شده ما بدین شکل خواهد بود:

class Point:

    def __init__(self, x, y):

        self.x = x

        self.y = y







class Rectangle:

    def __init__(self, origin, width, height):

        self.origin = origin

        self.width = width

        self.height = height




    def get_area(self):

        return self.width * self.height




    def print_coordinates(self):

        top_right = self.origin.x + self.width

        bottom_left = self.origin.y + self.height

        print('Starting Point (X)): ' + str(self.origin.x))

        print('Starting Point (Y)): ' + str(self.origin.y))

        print('End Point X-Axis (Top Right): ' + str(top_right))

        print('End Point Y-Axis (Bottom Left): ' + str(bottom_left))







def build_rectangle():

    rectangle_origin = Point(50, 100)

    rectangle = Rectangle(rectangle_origin, 90, 10)




    return rectangle







rectangle = build_rectangle()




print(rectangle.getArea())

rectangle.print_coordinates()

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

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

دیدگاه‌های شما

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