کد تمیز (Clean Code) یکی از مهمترین مفاهیم در برنامهنویسی حرفهای است که نقش کلیدی در توسعه، نگهداری و ارتقای نرمافزار ایفا میکند. در واقع اجرای موفق یک کد بدون خطا و با خروجی صحیح، یکی از لذتبخشترین بخشهای مسیر کدنویسی برای هر برنامهنویس است. اما تمرکز صرف بر اجرای صحیح میتواند به کدی پیچیده و نامنظم منجر شود. رعایت اصول کدنویسی تمیز نهتنها باعث افزایش خوانایی و کارایی کد میشود، بلکه همکاری تیمی، توسعه و نگهداری پروژه را نیز سادهتر میکند. در ادامه با ما همراه باشید تا با مفهوم کد تمیز و اصول آن بهصورت کامل آشنا شوید.
کد تمیز (Clean Code) چیست؟
«هر کسی میتواند کدی بنویسد که کامپیوتر آن را بخواند. اما برنامهنویس خوب، کدی مینویسد که انسان هم میتواند بخواند.»
اگر این نقل قول از «مارتین فولر» را همیشه گوشه ذهنتان داشته باشید، امکان ندارد که تمیز کد نزنید. کد تمیز یا Clean Code، تعریفی در توسعه نرمافزار است که به پیادهسازی منظم، منطقی، تمیز، جامع و قابل ردیابی کد اشاره دارد. هدف از کد تمیز، توسعه موثر و بهینه نرمافزار و طراحی کدی است که خوانا، قابل تغییر، توسعه و نگهداشت باشد. نوشتن Clean code، در حوزه دواپس هم اهمیت زیادی دارد و باعث میشود تا خوانایی کد برای یک مهندس دواپس افزایش پیدا کند.
چرا باید تمیز کد بزنیم؟
در فرایندهای نوین توسعه نرمافزار، اهمیت نوشتن کد تمیز فراتر از آن است که صرفا یک کد بدون خطا اجرا شود. از مهمترین دلایل اهمیت رعایت اصول کدنویسی تمیز برای موفقیت در یک پروژه میتوان به موارد زیر اشاره کرد:
- جلوگیری از پیچیدگیهای ناخواسته: کدی که فقط «کار کند» ولی ساختارمند و قابل فهم نباشد، در بلندمدت باعث افزایش وابستگیها و بروز خطا در مراحل مختلف توسعه، بهویژه هنگام یکپارچهسازی یا افزودن ویژگیهای جدید میشود.
- سهولت در نگهداری و توسعه: کد تمیز خواناتر است، اشکالزدایی را آسانتر میکند و روند افزودن قابلیتها یا بهروزرسانی بخشهای موجود را ساده و کمهزینه میسازد.
- توسعه تیممحور و همکاری موثر: در پروژههای واقعی، توسعه نرمافزار معمولا بهصورت گروهی انجام میشود. کدی که واضح، مستند و منظم نوشته شده باشد، مانع سردرگمی سایر اعضای تیم میشود و امکان مشارکت موثر را فراهم میکند.
- افزایش کیفیت محصول نهایی: کد تمیز، درک عملکرد سیستم را برای همه ذینفعان آسانتر میکند؛ از مدیر پروژه گرفته تا توسعهدهندگان تازهوارد. این موضوع باعث افزایش کیفیت کلی نرمافزار و رضایت کاربران میشود.
- تمرکز بر مخاطب انسانی، نه فقط ماشین: کدنویسی تمیز، نوشتن برای انسانهاست؛ یعنی باید طوری بنویسیم که دیگران (و خود ما در آینده) بتوانند بهراحتی مفهوم کد را درک کنند. به قول دونالد نوت:
«برنامهنویسی هنری است برای انتقال خواستهها از ذهن انسان به زبان ماشین، با درک کامل انسانهای دیگر.»
وقتی از کد تمیز صحبت میکنیم، منظورمان توسعه کاربرمحور است که در نتیجه آن، نرمافزار قابل نوشتن، خواندن و نگهداری است. از آنجایی که افراد مختلفی باید از عملکرد کد سر در بیاورند، منطقی است بگوییم کدی که مینویسید باید هم برای ماشین (کامپیوتر) و هم برای انسان قابل فهم باشد.
اصول نوشتن کد تمیز
حالا که با کد تمیز و لزوم پایبندی به آن آشنا شدیم، نوبت به شناخت اصول کدنویسی تمیز میرسد. اصول کد تمیز منحصر به زبان خاصی نیستند و توسط جامعه برنامهنویسان برای تمامی زبانها طراحی شدهاند. در ادامه مهمترین اصول نوشتن Clean Code را بررسی میکنیم.
۱. تاحدممکن، کد را ساده بنویسید (KISS)
۱ ۲ ۳ ۴ ۵ |
def faculty(number: int) -> int: if number <= ۱: return ۱ else: return number * faculty(number – ۱) |
ریشه این اصل مهم در طراحی، به نیروی دریایی آمریکا در سال ۱۹۶۰ برمیگردد؛ یعنی عبارت Keep It Simple, Stupid! طبق این اصل، اکثر سیستمها باید تا حد ممکن ساده باشند و پیچیدگی غیرضروری نداشته باشند. برای رسیدن به این سادگی، در زمان نوشتن برنامه این سوال را از خود بپرسید که «آیا میتوانم این بخش را سادهتر بنویسم؟»
تا جایی که مجبور نیستید، از پیچیدگی استفاده نکنید و مسائل را برای خود سخت نکنید. با ساده نگاه کردن به مسائل، میتوانید یک کد باکیفیت بنویسید، مشکلات را سریعتر حل کنید، با تیم توسعه بهتر تعامل کنید و سورس کد انعطافپذیرتری داشته باشید.
۲. قبل از هر کاری، عملکرد کد را درک کنید
بهعنوان یک برنامهنویس تازهکار، حتی اگر در حال نوشتن یک تابع ساده مانند «if else» هم هستید، با نوشتن این تابع روی کاغذ و فهم عمیق آن شروع کنید. اگر ایده پشت کد را بدانید، عملکرد الگوریتمها و کامپایلر برایتان قابل درکتر میشود.
حتی برای متخصصان این حوزه هم، راهِحل یک مشکل پیچیده یا طراحی الگوریتمی که بتواند مشکلات پیچیده را حل کند، شکستن این مشکل به بخشهای کوچکتر و درک هر بخش است. به محض رسیدن به شکست کار لازم و حل مشکلات کوچک، مشکلات بزرگتر برایتان آسان میشوند.
۳. در صورت لزوم، از کامنت استفاده کنید
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ ۱۴ ۱۵ ۱۶ ۱۷ ۱۸ |
namespace UnitTests { [TestFixture] public class Class1 { Fixture _fixture = new Fixture(); [Test] // TODO: rename this test to be more meaningful public void Test1() { Assert.That(۱, Is.EqualTo(۱)); } // TODO: consider boundary values // UNDONE: add more tests! } } |
ارزش استفاده از کامنت در کد باورنکردنی است. کامنتها میتوانند بهراحتی نحوه کار یک تابع پیچیده و حتی ترتیب انجام یک کار خاص را توضیح دهند. هرچه برنامهنویسها باتجربهتر میشوند، اهمیت سادهترین نکتهای که در مراحل اولیه برنامهنویسی یاد گرفتهاند، یعنی گذاشتن کامنت را فراموش میکنند.
شما از هر زبان برنامهنویس که استفاده کنید میتوانید در آن کامنت بگذارید. گذاشتن کامنت باعث میشود تا بهروزرسانی، دیباگ کردن، تحلیل و فرایندهای بعد از توسعه راحتتر و بهینهتر شوند. همچنین گذاشتن کامنت به سایر همتیمیهای شما کمک میکند تا کد شما را بهتر درک کنند.
از طرفی، باید توجه کنید که استفاده بیشازحد از کامنت، میتواند کد شما را نامرتب کند. پس باید در جای درست و تنها در صورت لزوم از کامنت استفاده کنید.
۴. از تکرار بپرهیزید (DRY)
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ ۱۴ ۱۵ |
export const validateEmpty = (str: string): boolean => { if (str.length > ۰) { return true; } else { return false; } }; export const validatePassword = (str: string): boolean => { if (str.length >= ۶) { return true; } else { return false; } }; |
اصل DRY یا Don’t Repeat Yourself ارتباط نزدیکی با KISS و فلسفه طراحی مینیمال دارد. طبق این اصل، هر قطعهای از دانش (در اینجا یعنی هر قطعه کد) در یک سیستم (یا همان سورس کد) باید تنها یک بار، آن هم کاملا صحیح و بدون ابهام ظاهر شود.
البته نقطه مقابل DRY، مفهوم WET است! این مفهوم به سه عادت مخرب برای کد تمیز اشاره دارد:
- ما از نوشتن لذت میبریم (We Enjoy Typing)
- هر چیز را دوبار بنویس (Write Everything Twice)
- وقت همه را تلف کن (Waste Everyone’s Time)
۵. هر چیزی را که نیاز ندارید، پاک کنید (YAGNI)
بهعنوان یک برنامهنویس، جز در مواقع کاملا ضروری نباید تابعی به کد اضافه کنید. اصل YAGNI بخشی از متد برنامهنویسی افراطی یا XP است که هدف آن، بهبود کیفیت نرمافزار و افزایش پاسخگویی به نیازهای کاربر است.
از متد YAGNI یا You Aren’t Gonna Need It در کنار ریفکتور مداوم، یونیت تست و یکپارچهسازی استفاده کنید و از تاثیر آن لذت ببرید.
۶. با ایجاد تورفتگی (Indentation) کد خود را مرتب کنید
تصور کنید که به یک سوپرمارکت میروید و هیچ نظم خاصی در چیدمان اقلام وجود ندارد. بعضی از لبنیات در بخش پوشاک هستند، بعضی دیگر در بخش آرایشی و نان هم کنار سبزیجات قرار گرفته است. این بینظمی اذیتکننده است، نه؟
در کد نویسی هم باید نظم وجود داشته باشد. فاصلهگذاری از اول سطر یا Indentation نظمی است که کد به آن نیاز دارد. وقتی کد شما با کمک تورفتگی و فاصله سطری نوشته شده باشد، خوانایی آن بیشتر میشود و راحتتر میتوانید آن چیزی را که دنبالش هستید، پیدا کنید.
۷. از فضای خالی استفاده کنید
استفاده از فضای خالی بین بخش های مختلف کد، میتواند تاثیر خارقالعادهای در خوانایی آن داشته باشد. استفاده از فضای سفید یا فضای خالی، معمولا مشکلی در کد ایجاد نمیکند؛ اما در زبانهایی مانند JavaScript که حجم سورسکد مهم است، فضای خالی ممکن است چند کیلوبایت به این حجم اضافه کند.
در چنین حالتی بهتر است در زمان توسعه، فضای خالی را نگه دارید و درست قبل از انتشار کد، با کمک ابزارهای هوشمندی که میتوانند سورسکد را مدیریت کنند، این فضاهای خالی را پاک کنید.
۸. از اصول متداول نامگذاری پیروی کنید
تنها نکتهای که در تمام مقالاتی که درباره نحوه صحیح برنامهنویسی و نوشتن Clean code نوشته شدهاند، میبینید، استفاده از اصول متداول نامگذاری است؛ با این حال بیشتر افراد یا این نکته را فراموش میکنند و یا آن را نادیده میگیرند.
پیروی از قواعد نامگذاری صحیح یک نکته بسیار مهم است که باعث میشود تغییرات و بهبودهای بعدی راحتتر انجام شوند. شاید در وهله اول استفاده از اسامی منحصر و متفاوت برای متغیرها، آرایهها، توابع و … جالب به نظر برسد، اما به مشکلاتی که در آینده برایتان ایجاد میکند، نمیارزد.
به همین خاطر بخشهای مختلف کد را با توجه به عملکردی که دارند، نامگذاری کنید و سعی کنید در تمام کد، از یک قاعده و استاندارد مشخص استفاده کنید. برای مثال متغیر زیر یک متغیر نامفهوم است که برای نشان دادن عملکرد آن، باید از قبل توضیحاتی ارائه دهید.
۱ |
int d; |
اما اگر همین متغیر را به شکل زیر بنویسید، نوع عملکرد آن کاملا مشخص میشود:
۱ |
int TimeleftTillshutdown; |
۹. با ثبات و یکپارچه بنویسید
این اصل با اختلاف مهمترین اصل در نوشتن کد تمیز است. یکپارچه بودن کد، اعتبار بیشتری به آن و در نتیجه مهارت برنامهنویسی شما میدهد. اگر قصد دارید از روش خاصی در یک بخش استفاده کنید، تاحدممکن تمام کد همان روش را به کار ببرید.
اگر ناچار به تغییری ناهمگون شدید، بهتر است علت این ناهماهنگی را در کامنت بنویسید تا خواننده کد، متوجه این موضوع شود.
تعداد خطوط در توابع باید چقدر باشد؟
برای آشنایی با کدنویسی تمیز این قطعه کد زیر از کتاب کد تمیز (Clean Code) نوشته رابرت مارتین (Robert C. Martin) را در نظر بگیرید:
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ ۱۴ ۱۵ ۱۶ ۱۷ ۱۸ ۱۹ ۲۰ ۲۱ ۲۲ ۲۳ ۲۴ ۲۵ ۲۶ ۲۷ ۲۸ ۲۹ ۳۰ ۳۱ ۳۲ ۳۳ ۳۴ ۳۵ ۳۶ ۳۷ ۳۸ ۳۹ ۴۰ ۴۱ ۴۲ ۴۳ ۴۴ ۴۵ ۴۶ ۴۷ ۴۸ ۴۹ ۵۰ ۵۱ ۵۲ ۵۳ ۵۴ ۵۵ ۵۶ ۵۷ ۵۸ ۵۹ |
public static String testableHtml(PageData pageData, boolean includeSuiteSetup) throws Exception { WikiPage wikiPage = pageData.getWikiPage(); StringBuffer buffer = new StringBuffer(); if (pageData.hasAttribute(“Test”)) { if (includeSuiteSetup) { WikiPage suiteSetup = PageCrawlerImpl .getInheritedPage(SuiteResponder.SUITE_SETUP_NAME, wikiPage); if (suiteSetup != null) { WikiPagePath pagePath = suiteSetup.getPageCrawler() .getFullPath(suiteSetup); String pagePathName = PathParser.render(pagePath); buffer.append(“!include -setup .”) .append(pagePathName) .append(“\n”); } } WikiPage setup = PageCrawlerImpl .getInheritedPage(“SetUp”, wikiPage); if (setup != null) { WikiPagePath setupPath = wikiPage .getPageCrawler() .getFullPath(setup); String setupPathName = PathParser.render(setupPath); buffer.append(“!include -setup .”) .append(setupPathName) .append(“\n”); } } buffer.append(pageData.getContent()); if (pageData.hasAttribute(“Test”)) { WikiPage teardown = PageCrawlerImpl .getInheritedPage(“TearDown”, wikiPage); if (teardown != null) { WikiPagePath tearDownPath = wikiPage .getPageCrawler() .getFullPath(teardown); String tearDownPathName = PathParser.render(tearDownPath); buffer.append(“\n”).append(“!include -teardown .”) .append(tearDownPathName) .append(“\n”); } if (includeSuiteSetup) { WikiPage suiteTeardown = PageCrawlerImpl .getInheritedPage(SuiteResponder.SUITE_TEARDOWN_NAME,wikiPage); if (suiteTeardown != null) { WikiPagePath pagePath = suiteTeardown.getPageCrawler() .getFullPath (suiteTeardown); String pagePathName = PathParser.render(pagePath); buffer.append(“!include -teardown .”).append(pagePathName) .append(“\n”); } } } pageData.setContent(buffer.toString()); return pageData.getHtml(); } |
آیا هدف این قطعه کد به راحتی درک میشود؟ مطمئنا جواب خیر است. با شکستن این قطعه کد به چند روال ساده و کمی تغییر ساختار، میتوان آن را در ۹ خط بازنویسی کرده و به کدنویسی تمیز کمک کرد، به نحوی که هدف از قطعه کد در کمترین زمان ممکن قابل درک باشد:
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ |
public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception { boolean isTestPage = pageData.hasAttribute(“Test”); if (isTestPage) { WikiPage testPage = pageData.getWikiPage(); StringBuffer newPageContent = new StringBuffer(); includeSetupPages(testPage, newPageContent, isSuite); newPageContent.append(pageData.getContent()); includeTeardownPages(testPage, newPageContent, isSuite); pageData.setContent(newPageContent.toString()); } return pageData.getHtml(); } |
حال سوال این جاست که چه چیزی باعث میشود تابعی مانند تابع اول ناخوانا و تابعی مانند تابع بازنویسیشده، خوانا یا اصطلاحا با کدنویسی تمیز باشد؟
چه ویژگیهایی از توابع باعث خوانایی بیشتر کد میشوند؟
در کتاب کدنویسی تمیز ویژگیهایی برای افزایش خوانایی کد مطرح شده که یکی از آنها کم کردن تعداد خطوط تابع است. رابرت مارتین در این کتاب میگوید:
«من هیچ منبعی را نمیتوانم ارائه دهم که در آن گفته شده باشد توابع با تعداد خطوط کم بهتر هستند. آن چه که میتوانم بگویم این است که نزدیک به چهار دهه توابعی با اندازههای مختلف نوشتهام؛ توابعی با تعداد ۱۰۰ تا ۳۰۰ خط کد! همچنین توابعی با ۲۰ تا ۳۰ خط کد. این تجربیات همراه با آزمون و خطا، به من آموخته است که توابع با تعداد خطوط کم بهتر هستند.»
یک تابع خوب باید چند خط باشد؟
وقتی صحبت از کاهش تعداد خطوط و کدنویسی تمیز در توابع به میان میآید، پرسش اصلی این است که یک تابع خوب باید چند خط باشد؟ ۵۰ خط؟ ۲۵ خط؟ ۵ خط؟ ۱ خط؟
در دهه ۸۰ گفته میشد برای نمایش یک تابع، نباید نیاز به اسکرول افقی یا عمودی صفحه داشته باشیم و تابع نباید از یک صفحه تجاوز کند. البته این گفته برای زمانی بود که مانیتورها ۲۴ خط ۸۰ کاراکتری بودند و ویرایشگرها نیز از ۴ خط آن استفاده میکردند؛ یعنی هر تابع نباید از۲۰ خط تجاوز میکرد. البته ۲۰ خطی که هر یک شامل کمتر از ۸۰ کاراکتر میشدند.
امروزه با انتخاب یک فونت مناسب و یک مانیتور نسبتا بزرگ، میتوان در هر خط ۱۵۰ کاراکتر را جا داد. همچنین ۱۰۰ خط یا بیشتر را میتوان در صفحه بدون اسکرول جا داد. با این استدلال، توابع ما نباید از ۱۵۰ کاراکتر در یک خط تجاوز کند. همچنین تعداد خط در تابع نباید بیشتر از ۱۰۰ خط باشد اما به گفته رابرت مارتین، برنامهنویسی باید به گونهای باشد که تنها در موارد نادر توابعی با بیش از ۲۰ خط به کار ببریم.
چگونه میتوان تعداد خطوط توابع را برای کدنویسی تمیز کاهش داد؟
برای کاهش تعداد خطوط یک تابع و کدنویسی تمیز باید قسمتهایی از کد را استخراج و در قالب یک تابع جدید پیادهسازی کنیم. در این باره دستورالعملهای زیادی وجود دارد. یکی از دستورالعملها، کد را براساس استفاده مجدد بررسی میکند و میگوید:
«اگر در کدهای شما قطعه کدی قابل استفاده مجدد باشد، باید استخراج شود و در یک تابع جداگانه قرار بگیرد؛ در غیر این صورت باید inline باشد.»
دستورالعمل دیگری نیز در این زمینه وجود دارد که میگوید:
«هر تابع یا روال فقط و فقط باید یک کار را انجام دهد.»
استیو مک کانل (Steve Mcconnell) در کتاب Code Complete و رابرت مارتین در کتاب Clean Code (دو منبع معتبر در زمینه کد نویسی به بهترین شیوه و کدنویسی تمیز) و همچنین مارتین فاولر (Martin Fowler) در وبلاگ خود، این دستورالعمل را تایید کردهاند و روشهایی را برای رسیدن به این هدف ارائه دادهاند.
دوباره به کد اول نگاه کنید. در این قطعه کد به وضوح معلوم است که تابع بیشتر از یک کار را انجام میدهد. ایجاد بافر، واکشی صفحات، تولید HTML، جستوجو در متن و … از جمله کارهایی است که این تابع انجام میدهد.
اما این دستورالعمل یک مشکل دارد: تشخیص این که تابع یک کار انجام میدهد یا خیر، سخت است.
آیا تابع ما یک کار انجام میدهد؟
کد زیر را که اصلاحشده کد قبلی است در نظر بگیرید:
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ |
public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception { boolean isTestPage = pageData.hasAttribute(“Test”); if (isTestPage) { WikiPage testPage = pageData.getWikiPage(); StringBuffer newPageContent = new StringBuffer(); includeSetupPages(testPage, newPageContent, isSuite); newPageContent.append(pageData.getContent()); includeTeardownPages(testPage, newPageContent, isSuite); pageData.setContent(newPageContent.toString()); } return pageData.getHtml(); } |
به نظر شما این کد یک کار را انجام میدهد؟ قبل از پاسخ دادن به این سوال باید وظایف تابع را بررسی کنیم. این تابع با پردازش pageData قابل تست بودن صفحه را بررسی میکند و در صورتی که صفحه از نوع قابل تست باشد، تنظیمات مربوط به آن را اضافه و محتوای جدید را ارسال میکند. به نظر میرسد وظیفه تشخیص قابل تست بودن صفحه، یک سطح از انتزاع است (تکه کدی که تشخیص میدهد صفحه قابل تست است یا خیر) و اعمال تنظیمات یک سطح دیگر (تکه کدی که تنظیمات صفحه قابل تست را اعمال میکند). پس تابع را به شکل زیر refactor میکنیم:
۱ ۲ ۳ ۴ ۵ ۶ |
public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception { if (isTestPage(pageData)) includeSetupAndTeardownPages(pageData, isSuite); return pageData.getHtml(); } |
میتوان این سوال را مطرح کرد که آیا واقعا این تابع یک کار را انجام میدهد؟ باید گفت بله ولی همچنین میتوان این سناریو را مطرح کرد که تابع نه یک کار، بلکه سه کار زیر را انجام میدهد:
۱. تشخیص این که صفحه یک صفحه تست است.
۲. اگر چنین است، تنظیمات را اعمال میکند.
۳. واکشی HTML
حالا این تابع یک کار انجام میدهد یا سه کار؟ نکته این جا است که این سه مرحله در واقع برای انجام یک کار (یک انتزاع) تحت عنوان «RenderPageWithSetupsAndTeardowns» در کنار هم هستند.
پس اگر تابعی با یک نام، فقط یک مرحله از کار را در گامهای مختلف انجام دهد، میتوان گفت که آن تابع یک کار را انجام میدهد.
در کد قبلی بهصراحت معلوم بود تابع چندین کار را تحت یک نام انجام میدهد. حتی در کد اخیر نیز معلوم شد که دو سطح از انتزاع را انجام میدهد اما در تکه کد آخر، شکستن تابع به بخش کوچکتر سخت است. شاید بتوان گفت ما میتوانیم دستور شرطی موجود در کد را استخراج و در قالب نام «includeSetupsAndTeardownsIfTestPage» بهعنوان تابعی که یک کار را انجام میدهد، پیاده کنیم ولی واقعیت این است که این سادهسازی کد، تغییری در سطح انتزاع ایجاد نمیکند.
تشخیص کار و استخراج آن به شکل تابع
روشهایی برای فهمیدن این که تابع یک کار را انجام میدهد یا خیر وجود دارد. در این جا مواردی را ذکر میکنیم که بهعنوان خط قرمز در پیادهسازی تابع شناخته میشوند. در صورتی که این موارد در توابع پیادهسازیشده وجود داشت، باید در این موضوع که تابع ما یک کار را انجام میدهد، شک کنیم.
۱. انتزاع چند سطحی دستورات در تابع و کدنویسی تمیز
یکی از راههای تشخیص این که تابع یک کار انجام میدهد یا خیر، این است که در تابع پیادهسازیشده باید دستورات در یک سطح از انتزاع قرار داشته باشند. کد قبلی را در نظر بگیرید. واضح است که این قانون رعایت نشده است. مثلا getHtml() از سطح بالایی از انتزاع برخوردار است که در کنار دستوراتی با سطح انتزاع متوسط همچون pagePathName = PathParser.render(pagePath) و دستور سطح پایینی همچون append(“\n”) قرار گرفته است.
«ترکیب سطوح مختلف از انتزاع داخل تابع، همواره باعث سردرگمی میشود.»
در واقع با جداسازی سطوح انتزاع میخواهیم توابعی را پیادهسازی کنیم که در ابتدا فقط مفاهیم ضروری را بیان میکند و در صورت نیاز، میتواند وارد جزئیات شود.
۲. کامنتها و خطوط جداساز در تابع
گاهی اوقات هنگام نوشتن تابع، از خطوط خالی یا کامنت برای جدا کردن کارهای مختلف استفاده میکنیم. این جداسازیها به ما میگوید که تابع بیش از یک کار انجام میدهد و باید اصلاح شود. معمولا بخشی که توسط خطوط خالی یا کامنت جدا شده، گزینه خوبی برای جداسازی است.
۳. دستورات کنترلی تودرتو
دستورات کنترلی تودرتو از جمله دستورالعملهای تکرار مانند for و while و … در سه سطح یا حتی دستورات شرطی دوسطحی، نمایانگر این مسئله هستند که تابع از سطح انتزاع پایینی برخوردار است و باید اصلاح شود.
۴. وجود پارامتر بولی (boolean) در تابع flag argument
استفاده از پارامتر بولی (boolean) در توابع کاری نادرست است. در این گونه توابع بهصراحت میتوان گفت که تابع بیش از یک کار انجام میدهد. یعنی در صورتی که پارامتر true ارسال شد، یک کار و در غیر این صورت یک کار دیگر انجام میدهد. این گونه توابع در خوانایی نیز دچار مشکل هستند. برای مثال فراخوانی متد getFormTemplate(true) برای خوانندگان کد، کمی گیجکننده به نظر میرسد. با این حال شاید با دیدن امضای متد به شکل getFormTemplate(bool withHeadre) کمککننده باشد، ولی کافی نیست. بهترین راهحل برای این تابع، پیادهسازی دو تابع با نامهای getFormTemplateWithHeader و getFormTemplateWithoutHeader است. این دو تابع مشکل خوانایی در کد را برطرف میکنند و با یک پیادهسازی خوب در تابع، مطمئنا یک کار را انجام خواهند داد.
۵. تابع با عوارض جانبی (side effects)
کد زیر را در نظر بگیرید:
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ ۱۴ ۱۵ ۱۶ |
public class UserValidator { private Cryptographer cryptographer; public boolean checkPassword(String userName, String password) { User user = UserGateway.findByName(userName); if (user != User.NULL) { String codedPhrase = user.getPhraseEncodedByPassword(); String phrase = cryptographer.decrypt(codedPhrase, password); if (“Valid Password”.equals(phrase)) { Session.initialize(); return true; } } return false; } } |
این تابع قرار است صحت کلمه عبور و نام کاربری را بررسی کند. در صورتی که کلمه عبور و نام کاربری صحیح باشد، مقدار true در غیر این صورت مقدار false را برگشت خواهد داد اما این کد در قسمتهای دیگر باعث side effect خواهد شد.
فراخوانی Session.initialize() باعث این کار میشود. همان گونه که از نام تابع میتوان برداشت کرد، این تابع قرار است صحت نام کاربری و کلمه عبور را بررسی کند و در نام تابع ذکر نشده است که قرار است علاوهبر این Session را نیز وهلهسازی کند. در نتیجه خطوطی که باعث side effect میشوند، باید جدا شوند. اگر به هر دلیلی این جداسازی میسر نبود (در یک پیادهسازی خوب بهندرت اتفاق میافتد)، حداقل با انتخاب نام مناسب برای تابع، این موضوع باید بهصراحت ذکر شود؛ مثلا در این قطعه کد نام checkPasswordAndInitializeSession برای تابع مناسب به نظر میرسد؛ فارغ از این که قانون «انجام یک کار» را با این تابع نقض کردهایم.
۶. انجام همزمان یک عمل (command) و برگشت یک نتیجه (query)
انجام همزمان یک command و query توسط تابع، علاوهبر این که قانون «انجام یک کار توسط یک تابع» را نقض میکند، باعث سردرگمی نیز خواهد شد و کدنویسی تمیز را تحتتاثیر قرار میدهد. در واقع تابع برای تغییر وضعیت objectها یا برای ارائه اطلاعات درباره آنها پیادهسازی میشود.
امضاء تابع زیر را در نظر بگیرید:
۱ |
public bool set(String attribute, String value); |
این تابع در بدنه خود این وظیفه را پیادهسازی کرده است: در صورتی که attribute وجود داشت مقدار آن را به value تنظیم و مقدار true برگشت میدهد و در غیر این صورت، مقدار false را برگشت خواهد داد. این تابع باعث به وجود آمدن کد عجیب زیر خواهد شد:
۱ |
if (set(“username”, “unclebob”))... |
کسی که از محتوای تابع set باخبر نیست از این کد چه برداشتی خواهد داشت؟
- برداشت ۱: اگر خصوصیت username با موفقیت به unclebob تنظیم شد.
- برداشت ۲: اگر نام unclebob قبلا به خصوصیت username تنظیم شده باشد.
- برداشت ۳: اگر خصوصیت username وجود داشت و مقدار آن به unclebob تنظیم شد.
برای حل این مشکل ما میتوانیم نام تابع را به setAndCheckIfExists تغییر دهیم ولی این موضوع کمک زیادی نخواهد کرد. راهحل واقعی، جداسازی توابع command از توابع query (command query separation) به شکل زیر است:
۱ ۲ ۳ ۴ |
if (attributeExists(“username”)) { setAttribute(“username”, “unclebob”); ... } |
نکتهای درباره command query separation
اشاره شد توابع command نباید نتیجهای را بازگشت دهند و این وظیفه توابع query است. در این حالت، پاسخ به این سوال از اهمیت فراوانی برخوردار است: در صورتی که تابع از نوع command به هر دلیلی نتوانست کار خود را انجام دهد چه؟ نباید به تابع صداکننده اطلاع داده شود که کار با موفقیت انجام شد یا خیر؟
اصولا بازگشت کد خطا در توابع command باعث بروز مشکلاتی میشود و باید از این کار اجتناب کرد. از جمله مشکلات میتوان به دو مورد زیر اشاره کرد:
- اول: به وجود آمدن دستورات شرطی تو در تو در محلی که تابع صدا زده شده است.
- دوم: حل بلافاصله مشکل توسط صدازننده، در صورتی که تابع اصلی دچار مشکل شده است.
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ ۱۴ ۱۵ ۱۶ ۱۷ |
if (deletePage(page) == E_OK) { if (registry.deleteReference(page.name) == E_OK) { if (configKeys.deleteKey(page.name.makeKey()) == E_OK) logger.log(“page deleted”); else logger.log(“configKey not deleted”); } else logger.log(“deleteReference from registry failed”); } else { logger.log(“delete failed”); return E_ERROR; } |
اما راهحل چیست؟ در قطعه کد بالا اگر هر کدام از توابع از نوع command به جای بازگشت کد خطا یک استثنا تولید کنند، بهراحتی میتوان کد را به شکل زیر تغییر داد:
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ |
try { deletePage(page); registry.deleteReference(page.name); configKeys.deleteKey(page.name.makeKey()); } catch (Exception e) { logger.log(e.getMessage()); } |
نکتهای در مورد کد بالا وجود دارد: بهتر است بدنه try و catch بهصورت تابع پیادهسازی شوند. به شکل زیر:
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ ۱۴ ۱۵ ۱۶ ۱۷ ۱۸ |
public void delete(Page page) { try { deletePageAndAllReferences(page); } catch (Exception e) { logError(e); } } private void deletePageAndAllReferences(Page page) { deletePage(page); registry.deleteReference(page.name); configKeys.deleteKey(page.name.makeKey()); } private void logError(Exception e) { logger.log(e.getMessage()); } |
جداسازی دستورات با پردازش طبیعی از دستورات با پردازش خطادار، باعث کاهش پیچیدگی و کدنویسی تمیز خواهد شد.
صرف زمان طولانی برای فهمیدن وظیفه تابع در کدنویسی تمیز
مارتین فاولر در وبلاگ خود میگوید:
«اگر شما برای فهمیدن این که قسمتی از کد چه کاری را انجام میدهد زمان صرف کنید، یعنی در تابع مشکلی وجود دارد که باید حل شود.»
وجود دستوراتی با سطوح انتزاع مختلف و دستورات شرطی یا ساختارهای کنترلی تودرتو، میتواند باعث ایجاد پیچیدگی در تابع شود. اگر قسمتهای مختلف برنامه را با انتخاب نام مناسب بهعنوان یک تابع مستقل پیادهسازی کنید، هنگام مرور دوباره آنها بلافاصله بعد از دیدن نام تابع متوجه میشوید که هدف از این تابع چه بوده است و چه کاری را انجام میدهد.
هیچکدام از دستورالعملهایی که ذکر کردیم، درباره تعداد خطوط تابع صحبت نمیکنند؛ یعنی هیچکدام نگفتهاند تابع باید در ۱۰ خط یا ۵ خط پیادهسازی شود. تابع یک خطی زیر را در نظر بگیرید:
۱ ۲ ۳ ۴ ۵ |
return level4 != null ? GetResources().Where(r => (r.Level2 == (int)level2) && (r.Level3 == (int)level3) && (r.Level4 == (int)level4)).ToList() : level3 != null ? GetResources().Where(r => (r.Level2 == (int)level2) && (r.Level3 == (int)level3)).ToList() : level2 != null ? GetResources().Where(r => (r.Level2 == (int)level2)).ToList() : GetAllResourceList(); |
آیا این تابع به خاطر این که در یک خط پیادهسازی شده است، تابع خوبی به حساب میآید؟ مسلما خیر. پس میتوان نتیجه گرفت اگر شرایط ذکرشده را رعایت کنیم، تعداد خطوط در توابع مهم نیست و کدنویسی تمیز نیز بسیار اهمیت دارد. هر چند باید به این نکته نیز اشاره کرد که با رعایت قوانین ذکرشده، تعداد خطوط در توابع بهصورت خودکار کاهش مییابد و معمولا بیشتر از ۱۲ خط نخواهد بود.
مثال واقعی از یک کد کثیف که تبدیل به کد تمیز شده
برای اینکه درک بهتری از کد تمیزشده داشته باشید، بیایید با یک مثال واقعی جلو برویم. فرض کنید با دو کامپوننت مواجه هستید که تقریبا ساختاری یکسان دارند، اما بهجای اینکه بخشهای مشترک را استخراج کنیم، هر کدام بهصورت جداگانه پیادهسازی شدهاند. به نمونه زیر توجه کنید:
🟥 کد کثیف (Dirty Code):
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ ۱۴ ۱۵ ۱۶ ۱۷ ۱۸ |
import Title from ‘./Title’; export const Thingie = ({ description }) => ( <div class=“thingie”> <div class=“description-wrapper”> <Description value={description} /> </div> </div> ); export const ThingieWithTitle = ({ title, description }) => ( <div> <Title value={title} /> <div class=“description-wrapper”> <Description value={description} /> </div> </div> ); |
در اینجا تقریبا Thingie و ThingieWithTitle بهجز یک مورد یعنی وجود عنوان در نسخه دوم، مشابه هستند. این تکرار غیر ضروری است و باعث پایین آمدن خوانایی و نگهداری سختتر کد میشود. حال بیایید این کد را به شکل تمیز و قابل نگهداری بازنویسی کنیم:
✅ کد تمیز (Clean Code):
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ ۱۴ ۱۵ ۱۶ |
import Title from ‘./Title’; export const Thingie = ({ description, children }) => ( <div className=“thingie”> {children} <div className=“description-wrapper”> <Description value={description} /> </div> </div> ); export const ThingieWithTitle = ({ title, ...others }) => ( <Thingie {...others}> <Title value={title} /> </Thingie> ); |
در نسخه تمیز، منطق تکراری حذف شده و قابلیت استفاده مجدد افزایش یافته است. حال Thingie میتواند بهصورت منعطفتر عمل کند و کامپوننتهای دیگری مانند ThingieWithTitle میتوانند از آن بهسادگی استفاده کنند.
جمعبندی
کدنویسی تمیز یکی روش توسعه خوانندهمحور است که منجر به توسعه نرمافزاری با خوانایی خوب و نگهداشت آسان میشود. در اکثر مواقع، برنامهنویسها تصور میکنند که اگر کد خروجی لازم را به کاربر بدهد، کافی است؛ اما نوشتن یک کد تمیز به اندازه نوشتن کدی که درست کار میکند، اهمیت دارد. باید به این نکته توجه کنید که کدنویسی تمیز یک مهارت یکشبه نیست و مجموعهای از عادات است که به کمک اصول بالا، باید بهمرور در خود پرورش دهید.
منابع
www.ionos.com | www.cogut.medium.com | www.americanexpress.io
سوالات متداول
کد تمیز یعنی کدی که ساده، خوانا، منظم و قابل توسعه باشد، نه فقط اینکه "کار کند"، بلکه بهراحتی بتوان آن را فهمید، تغییر داد و نگهداری کرد.
خیر، اصول کدنویسی تمیز مستقل از زبان برنامهنویسی هستند و در همه زبانها (مثل Python، JavaScript، Java و…) قابل پیادهسازیاند.
سادگی و خوانایی، مهمترین اصل نوشتن کد تمیز است. کد باید به صورتی نوشته شود که حتی بعد از چند ماه بتوان بهراحتی آن را فهمید.
دیدگاهتان را بنویسید