هر برنامهنویسی میداند که لذتبخشترین بخش این کار، لحظهای است که کد بدون هیچ خطا و البته با خروجی صحیح اجرا میشود. بنابراین تلاش بیشتر برنامهنویسها، نوشتن کدی است که در نهایت درست اجرا شود. اما این کار میتواند باعث شود تا سورس کد از یک کد تمیز (Clean Code) به یک کد پیچیده و بهم ریخته تبدیل شود.
کدنویسی تمیز باعث میشود تا همکاری روی یک پروژه توسعه نرمافزار راحتتر شود و در وقت و انرژی که باید برای بهبود، بهروزرسانی و نگهداشت یک کد صرف کنیم، صرفهجویی هم میشود. در این مقاله قصد داریم ابتدا با مفهوم کلین کد یا کد تمیز آشنا شویم و سپس اصول و قواعدی که برای داشتن این کد باید رعایت شود را بررسی کنیم. با ما همراه باشید.
کد تمیز (Clean Code) چیست؟
«هر کسی میتواند کدی بنویسد که کامپیوتر آن را بخواند. اما برنامهنویس خوب، کدی مینویسد که انسان هم میتواند بخواند.»
اگر این نقل قول از «مارتین فولر» را همیشه گوشه ذهنتان داشته باشید، امکان ندارد که تمیز کد نزنید. کد تمیز یا Clean Code، تعریفی در توسعه نرمافزار است که به پیادهسازی منظم، منطقی، تمیز، جامع و قابل ردیابی کد اشاره دارد. هدف از کد تمیز، توسعه موثر و بهینه نرمافزار و طراحی کدی است که خوانا، قابل تغییر، قابل توسعه و قابل نگهداشت باشد. نوشتن Clean code، در حوزه دواپس هم اهمیت زیادی دارد و باعث میشود تا خوانایی کد برای یک مهندس دواپس افزایش پیدا کند.
چرا باید تمیز کد بزنیم؟
در روش امروزی توسعه نرمافزار، نوشتن کدی که کار کند و الزاما «تمیز» نباشد، چندان عجیب نیست. نبود کد تمیز در توسعه، باعث بروز خطاهای مختلف در فاز سازگاری و مشکلات مختلف هنگام افزودن بخشهای جدید میشود.
برای مثال، یک تغییر کوچک در سورس کد، میتواند زنجیرهای از تغییرات اجتناب ناپذیر را به دنبال داشته باشد. به همین خاطر نگهداری و توسعه کدهای کثیف در آینده، به یک فرایند سخت تبدیل میشود.
علاوه بر این، در یک سناریوی واقعی شما به عنوان عضوی از یک تیم توسعهدهنده فعالیت میکنید؛ به همین خاطر مهم است که کد خود را تمیز و قابل فهم بنویسید، چرا که به جز شما افراد دیگری هم روی پروژه کار میکنند. اگر تمیز کد بزنید، هم به خود آیندهتان و هم به همکارانتان لطف کردهاید.
در نهایت، کد تمیز ارزش پروژه را بالا میبرد؛ چرا که با این کار مطمئن میشوید که چه توسعهدهندگان و چه سایر افراد (ذینفعان) میتوانند به کمک ساختارها و فرایندها، نحوه عملکرد کد را درک کنند.
تمرکز روی خواننده
وقتی از کد تمیز صحبت میکنیم، منظورمان توسعه کاربرمحور است که در نتیجه آن، نرمافزار قابل نوشتن، خواندن و نگهداری است. از آنجایی که افراد مختلفی باید از عملکرد کد سر در بیاورند، منطقی است بگوییم کدی که مینویسید باید هم برای ماشین (کامپیوتر) و هم برای انسان قابل فهم باشد.
«برنامهنویسی هنری است که در آن باید نیازی که از کامپیوتر دارید را، به افراد دیگر منتقل کنید.» – دونالد نوت
اصول نوشتن کد تمیز
حالا که با کد تمیز و لزوم پایبندی به آن آشنا شدیم، نوبت به شناخت اصول کدنویسی تمیز میرسد. اصول کد تمیز منحصر به زبان خاصی نیستند و توسط جامعه برنامهنویسان برای تمامی زبانها طراحی شدهاند. در ادامه مهمترین اصول نوشتن Clean Code را بررسی میکنیم.
تا حد ممکن، کد را ساده بنویسید (KISS)
ریشه این اصل مهم در طراحی، به نیروی دریایی آمریکا در سال ۱۹۶۰ برمیگردد؛ یعنی عبارت Keep It Simple, Stupid! طبق این اصل، اکثر سیستمها باید تا حد ممکن ساده باشند و پیچیدگی غیرضروری نداشته باشند. برای رسیدن به این سادگی، در زمان نوشتن برنامه این سوال را از خود بپرسید که «آیا میتوانم این بخش را سادهتر بنویسم؟»
تا جایی که مجبور نیستید، از پیچیدگی استفاده نکنید و مسائل را برای خود سخت نکنید. با ساده نگاه کردن به مسائل، میتوانید یک کد باکیفیت بنویسید، مشکلات را سریعتر حل کنید، با تیم توسعه بهتر تعامل کنید و سورس کد انعطافپذیرتری داشته باشید.
قبل از هر کاری، عملکرد کد را درک کنید
به عنوان یک برنامهنویس تازهکار، حتی اگر در حال نوشتن یک تابع ساده مانند «if else» هم هستید، با نوشتن این تابع روی کاغذ و فهم عمیق آن شروع کنید. اگر ایده پشت کد را بدانید، عملکرد الگوریتمها و کامپایلر برایتان قابل درکتر میشود.
حتی برای متخصصان این حوزه هم، راهِ حل یک مشکل پیچیده یا طراحی الگوریتمی که بتواند مشکلات پیچیده را حل کند، شکستن این مشکل به بخشهای کوچکتر و درک هر بخش است. به محض رسیدن به شکست کار لازم و حل مشکلات کوچک، مشکلات بزرگتر برایتان آسان میشوند.
در صورت لزوم، از کامنت استفاده کنید
ارزش استفاده از کامنت در کد باورنکردنی است. کامنتها میتوانند به راحتی نحوه کار یک تابع پیچیده و حتی ترتیب انجام یک کار خاص را توضیح دهند. هرچه برنامهنویسها باتجربهتر میشوند، اهمیت سادهترین نکتهای که در مراحل اولیه برنامهنویسی یاد گرفتهاند، یعنی گذاشتن کامنت را فراموش میکنند.
شما از هر زبان برنامهنویس که استفاده کنید میتوانید در آن کامنت بگذارید. گذاشتن کامنت باعث میشود تا بهروزرسانی، دیباگ کردن، تحلیل و فرایندهای بعد از توسعه راحتتر و بهینهتر شوند. همچنین گذاشتن کامنت به سایر همتیمیهای شما کمک میکند تا کد شما را بهتر درک کنند.
از طرفی، باید توجه کنید که استفاده بیش از حد از کامنت، میتواند کد شما را نامرتب کند. پس باید در جای درست و تنها در صورت لزوم از کامنت استفاده کنید.
از تکرار بپرهیزید (DRY)
اصل 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();
آیا این تابع به خاطر این که در یک خط پیادهسازی شده است، تابع خوبی به حساب میآید؟ مسلما خیر. پس میتوان نتیجه گرفت اگر شرایط ذکرشده را رعایت کنیم، تعداد خطوط در توابع مهم نیست و کدنویسی تمیز نیز بسیار اهمیت دارد. هر چند باید به این نکته نیز اشاره کرد که با رعایت قوانین ذکرشده، تعداد خطوط در توابع به صورت خودکار کاهش مییابد و معمولا بیشتر از ۱۲ خط نخواهد بود.
جمعبندی
کدنویسی تمیز یکی روش توسعه خوانندهمحور است که منجر به توسعه نرمافزاری با خوانایی خوب و نگهداشت آسان میشود. در اکثر مواقع، برنامهنویسها تصور میکنند که اگر کد خروجی لازم را به کاربر بدهد، کافی است؛ اما نوشتن یک کد تمیز به اندازه نوشتن کدی که درست کار میکند، اهمیت دارد. در این مقاله سعی کردیم با Clean Code و اصول نوشتن آن آشنا شویم. باید به این نکته توجه کنید که کدنویسی تمیز یک مهارت یک شبه نیست و مجموعهای از عادات است که به کمک اصول بالا، باید به مرور در خود پرورش دهید.
منابع:
دیدگاهتان را بنویسید