در این مقاله قصد داریم تا با برخی از بهترین شیوهها برای نوشتن Clean Code یا کد تمیز آشنا شویم. به طور ساده، نوشتن Clean Code در ابتدا ممکن است کندتر به نظر برسد، اما در بلند مدت باعث صرفهجویی در زمان شده و کار روی پروژه را راحتتر میکند. همچنین باعث میشود تا نرمافزاری که ساختیم قابل اعتمادتر باشد. نوشتن Clean Code عادتی است که توسعهدهندگان حرفهای به آن پایبند هستند و نشاندهنده تعهد به کیفیت و اخلاق کاری بالا است.
در این بخش قصد داریم تا ۱۰ نکته عملی برای این که کد خوانا، ساختاریافته و کارآمد داشته باشیم را باهم بررسی کنیم.
هنگام نامگذاری متغیرها، توابع و کلاسها، بهتر است نامهایی انتخاب کنیم که به وضوح هدف آنها را توصیف میکند.
به جای اینکه یک متغیر را b
بنامیم، باید سعی کنیم نامی مثل numberOfUsers
انتخاب نماییم. به این ترتیب، هر توسعهدهنده دیگری که کد ما را میخواند، میتواند به راحتی بدون اینکه نیاز به توضیحات اضافی باشد هدف آن را متوجه بشود. یک نام معنیدار حدس و گمان را از بین میبرد و از سردرگمی جلوگیری میکند. به عنوان مثال:
// Good let numberOfUsers = 5; // Clear and easy to understand // Bad let b = 5; // Vague and unclear
userAge
یا totalAmount
.calculateTotal()
یا fetchUserData()
.User
یا Order
، تا آنچه که هستند را نمایش دهند.// Variable: Describes the data it holds let userAge = 25; // Function: Uses an action word to describe what it does function calculateTotal(price, quantity) { return price * quantity; } // Class: Singular noun representing a type of object class User { constructor(name, age) { this.name = name; this.age = age; } }
اصل SRP یا مسئولیتپذیری تکگانه یعنی هر تابع یا متد باید یک کار مشخص و واحد را انجام دهد. این کار باعث میشود تا توابع ما کوتاه و متمرکز باشند که در این صورت خواندن، تست و نگهداری آنها راحتتر میشود.
برای مثال، اگر تابعی به نام calculateTotal
داشته باشیم، باید فقط مسئول محاسبه مجموع باشد. اگر کارهای اضافی به آن اضافه نماییم، کد ما گیجکننده شده و نگهداری آن دشوار میشود. در ادامه یک مثال برای نشان دادن اهمیت متمرکز نگه داشتن توابع را با هم بررسی میکنیم:
فرض کنید میخواهیم یک مجموع محاسبه کنیم و یک آبجکت با اطلاعات اضافی، مثل اینکه چه کسی در چه زمانی آن را محاسبه کرده است را return نماییم. به جای این که این اطلاعات را مستقیما به calculateTotal
بیفزاییم، میتوانیم از یک تابع دوم استفاده کنیم.
۱- مثال برای زمانی که تسکها به صورت جداگانه هستند:
// This function only calculates the total function calculateTotal(a, b) { return a + b; } // This function creates an object with extra details function createCalculationRecord(a, b, user) { let sum = calculateTotal(a, b); // Calls the calculate function return { user: user, total: sum, timestamp: new Date() }; } let record = createCalculationRecord(5, 10, "Shahan"); console.log(record);
مزایا:
در این مثال، هر تابع وظیفه واضح و متمرکزی دارد. تابع calculateTotal
فقط محاسبات ریاضی را انجام میدهد، در حالی که تابع createCalculationRecord
جزئیات اضافی را اضافه میکند. اگر بخواهیم نحوه محاسبه مجموع را تغییر دهیم، فقط باید تابع calculateTotal
را بهروزرسانی نماییم و اگر بخواهیم فرمت رکورد را تغییر دهیم، فقط تابع createCalculationRecord
را تغییر میدهیم.
۲- مثال برای زمانی که تسکها در یک تابع باهم ترکیب شدهاند:
// This function calculates the total and creates an object in one step function calculateTotalAndReturnRecord(a, b, user) { let sum = a + b; return { user: user, total: sum, timestamp: new Date() }; } let record = calculateTotalAndReturnRecord(5, 10, "Shahan"); console.log(record);
معایب:
در این مثال، نام تابع calculateTotalAndReturnRecord
نشان میدهد که این تابع در تلاش است چند کار مختلف انجام دهد. اگر بخواهیم فقط محاسبه را استفاده کنیم، نمیتوانیم این تابع را بدون قسمت رکورد دوباره مورد استفاده قرار دهیم. همچنین بهروزرسانی و تست هر تسک بهطور جداگانه کار دشواری میباشد.
کد خوب باید بهطور خودکار، بدون نیاز به کامنتهای زیاد، واضح و شفاف باشد. یعنی، ما باید روی نوشتن کدی که به خودی خود واضح و قابل فهم است تمرکز کنیم.
کامنتها وقتی مفیدند که بخواهیم منطق پیچیدهای را توضیح دهیم یا رویکرد خاصی را شرح دهیم، اما کامنتهای زیاد میتوانند کد ما را شلوغ کرده و نگهداری آن را سخت کنند.
چه زمانی باید از کامنتها استفاده کنیم:
به عنوان مثال:
// Clear name, no comment needed let userAge = 25; // Unclear name, comment needed let a; // age of the user
کد خوانا از تورفتگیها، line breakها و spaceها استفاده میکند تا همه چیز مرتب و سازمانیافته باشد.
تصور کنید در حال خواندن یک داستان هستیم، پاراگرافها با تقسیمبندی متنهای طولانی خواندن آن را آسانتر میکنند. در کد نویسی نیز line breakها همان هدف را دارند. به عنوان مثال:
// Good Code if (isLoggedIn) { console.log("Welcome!"); } else { console.log("Please log in."); } // Bad Code if(isLoggedIn){console.log("Welcome!");}else{console.log("Please log in.");}
Prettier در VS Code، از formatterهای محبوب است که بهطور خودکار استایل Clean Code را برای زبانهای مختلف اعمال میکند. این ابزارها تضمین میکنند که کد ما در سراسر پروژهها خوانا و منظم باشد و کمترین تلاش دستی را نیاز داشته باشد.
تستهای واحد کمک میکنند تا مطمئن شویم که هر بخش از کد ما همانطور که انتظار میرود عمل میکند. با تست کردن قسمتهای کوچک و جداگانه (مثل توابع)، میتوانیم خطاها را زود تشخیص دهیم و از گسترش آنها به سایر بخشهای کد جلوگیری نماییم. به طور مشخص، تستهای واحد در واقع چکهای کیفیت کوچک برای هر بخش از کد ما هستند تا مطمئن شویم همانطور که باید، عمل میکنند.
در ادامه نحوه تست یک آبجکت جاوااسکریپت پیچیده با چندین متد را با استفاده از یک کلاس Calculator
بررسی میکنیم.
این روش به ما کمک میکند تا بفهمیم چرا مهم است که هر متد روی یک تسک متمرکز باشد و با استفاده از تستهای واحد مطمئن شویم که هر کدام به درستی کار میکنند.
در این مثال، کلاس Calculator
است که شامل متدهای عملیات ریاضی پایه مانند جمع، تفریق، ضرب و تقسیم میباشد.
class Calculator { constructor() { this.result = 0; } add(a, b) { return a + b; } subtract(a, b) { return a - b; } multiply(a, b) { return a * b; } divide(a, b) { if (b === 0) throw new Error("Cannot divide by zero"); return a / b; } }
همانطور که مشاهده میکنیم، هر متد یک عملیات خاصی را انجام میدهد. متد divide
دارای منطق اضافی برای مدیریت تقسیم بر صفر است که در غیر این صورت باعث خطا میشود.
اکنون قصد داریم تا تستهای واحد بنویسیم تا تأیید کنیم که هر متد همانطور که انتظار میرود عمل میکند.
برای تست کلاس Calculator
، میتوانیم تستهایی بنویسیم که هم موارد عادی و هم موارد حاشیهای را پوشش دهند. در ادامه نحوه راهاندازی تستها برای هر متد را داریم:
// Initialize the Calculator instance const calculator = new Calculator(); // Test add method console.assert(calculator.add(2, 3) === 5, 'Test failed: 2 + 3 should be 5'); console.assert(calculator.add(-1, 1) === 0, 'Test failed: -1 + 1 should be 0'); // Test subtract method console.assert(calculator.subtract(5, 3) === 2, 'Test failed: 5 - 3 should be 2'); console.assert(calculator.subtract(0, 0) === 0, 'Test failed: 0 - 0 should be 0'); // Test multiply method console.assert(calculator.multiply(2, 3) === 6, 'Test failed: 2 * 3 should be 6'); console.assert(calculator.multiply(-1, 2) === -2, 'Test failed: -1 * 2 should be -2'); // Test divide method console.assert(calculator.divide(6, 3) === 2, 'Test failed: 6 / 3 should be 2'); try { calculator.divide(1, 0); console.assert(false, 'Test failed: Division by zero should throw an error'); } catch (e) { console.assert(e.message === "Cannot divide by zero", 'Test failed: Incorrect error message for division by zero'); }
توضیح تستها:
جمع (متد add
): تست میکنیم که add(2, 3)
مقدار ۵
، و add(-1, 1)
مقدار ۰
را return کند. اگر این تستها قبول شوند، میدانیم که منطق جمع به درستی عمل میکند.
تفریق (متد subtract
): بررسی میکنیم که subtract(5, 3)
مقدار ۲
، و subtract(0, 0)
مقدار ۰
را return کند. این چکها تأیید میکنند که تفریق دقیق است.
ضرب (متد multiply
): تست میکنیم که تابع ضرب با مقادیر مثبت و منفی به درستی عمل کند، مانند اینکه multiply(2, 3)
مقدار ۶
، و multiply(-1, 2)
مقدار -۲
را return کند.
تقسیم (متد divide
): تأیید میکنیم که تقسیم ۶
بر ۳
مقدار ۲
را return کند. برای تقسیم بر صفر، از بلاک try...catch
استفاده میکنیم تا مطمئن شویم که یک خطا با پیام صحیح تولید میشود. این تست اطمینان میدهد که متد به درستی خطاها را مدیریت میکند.
میبینیم که اگر هر متدی شکست بخورد تست، پیام خطای واضحی تولید میکند که به ما کمک میکند سریعاً مشکل را شناسایی و برطرف نماییم. تست کردن متدها به طور جداگانه به ما کمک میکند تا خطاها را زود تشخیص دهیم و قابل اعتماد بودن و تمیزی کد را حفظ کنیم.
dependencyها قطعات نرمافزاری هستند که کد ما به آنها وابسته است.
تصور کنید وب اپلیکیشنی میسازیم که ایمیل ارسال میکند. به جای این که خودمان کد ارسال ایمیل را بنویسیم میتوانیم از یک کتابخانه خارجی مانند Nodemailer استفاده کنیم. در این مثال، Nodemailer یک dependency است؛ اپلیکیشن ما برای مدیریت قابلیت ارسال ایمیل به آن وابسته میباشد. به عنوان مثال:
const nodemailer = require('nodemailer'); function sendEmail(to, subject, message) { const transporter = nodemailer.createTransport({ service: 'gmail', auth: { user: 'your-email@gmail.com', pass: 'your-email-password' } }); const mailOptions = { from: 'your-email@gmail.com', to: to, subject: subject, text: message }; return transporter.sendMail(mailOptions); }
در این مثال، nodemailer
import میشود و برای ایجاد یک transporter برای ارسال ایمیلها مورد استفاده قرار میگیرد. بدون آن، باید تمام قابلیتهای ارسال ایمیل را از ابتدا خودمان بسازیم، که بسیار پیچیده و وقتگیر خواهد بود. با استفاده از Nodemailer به عنوان dependency، اپلیکیشن ما میتواند ایمیلها را به راحتی ارسال کند.
اگرچه dependencyها مفید هستند، اما باید سعی کنیم از ایجاد وابستگی بیش از حد به نرمافزار یا کتابخانههای خارجی خودداری نماییم. فقط زمانی باید از dependencyها استفاده کنیم که کار ما را سادهتر کنند یا قابلیت مهمی اضافه نمایند.
مدیریت مؤثر dependencyها برای نوشتن Clean Code حیاتی است. در ادامه چند نکته داریم که عبارتند از:
اکنون قصد داریم تا یک مثال از کد قبلی Nodemailer برای پیادهسازی مفهوم جدا کردن منطق در کد خود را باهم بررسی کنیم.
میتوانیم یک تابع Wrapper بسازیم که جزئیات ارسال ایمیل را مخفی کند. به این ترتیب، میتوانیم سرویس ایمیل زیرین را تغییر دهیم یا dependency به Nodemailer را بدون تأثیر بر کد باقیمانده حذف نماییم. در ادامه نحوه ساختاردهی کد برای رسیدن به این هدف را مشاهده میکنیم:
const nodemailer = require('nodemailer'); // Core function to send email function sendEmail(to, subject, message) { const transporter = createTransporter(); const mailOptions = createMailOptions(to, subject, message); return transporter.sendMail(mailOptions); } // Function to create the transporter function createTransporter() { return nodemailer.createTransport({ service: 'gmail', auth: { user: 'your-email@gmail.com', pass: 'your-email-password' } }); } // Function to create mail options function createMailOptions(to, subject, message) { return { from: 'your-email@gmail.com', to: to, subject: subject, text: message }; } // Example usage sendEmail('recipient@example.com', 'Test Subject', 'Hello, this is a test email.') .then(() => { console.log('Email sent successfully!'); }) .catch((error) => { console.error('Error sending email:', error); });
sendEmail
، createTransporter
و createMailOptions
جداگانه هستند؛ بنابراین، به ما این امکان را میدهند که بتوانیم یکی را تغییر دهیم بدون اینکه به بقیه کدها آسیب بزنیم.createTransporter
را تغییر دهیم.یک ساختار پروژه سازمانیافته به اندازه خود کد اهمیت دارد. این کار را باید مثل سازماندهی فضای کار خود بدانیم. ما به مکانهای مشخصی برای هر چیز نیاز داریم تا بتوانیم به راحتی آنها را پیدا کنیم. برای پروژههای برنامهنویسی، باید پوشههایی برای بخشهای مختلف مانند components
، utils
و services
ایجاد کنیم.
برای راهاندازی یک پروژه تمیز و سازمانیافته، باید بخشهای مختلف کد خود را در پوشههای جداگانه دستهبندی کنیم. در ادامه یک مثال ساده از ساختار یک پروژه سازمانیافته را داریم:
myProject ├── src │ ├── components │ ├── services │ ├── utils └── tests
مثالی از ساختار درون پوشه components
:
components ├── Button.js ├── Header.js └── Form.js
مثالی از ساختار درون پوشه services
:
services ├── emailService.js ├── userService.js └── productService.js
ساختار درون پوشه utils
:
utils ├── formatDate.js ├── validateEmail.js └── generateId.js
ساختار نمونه از درون پوشه tests
:
tests ├── emailService.test.js ├── userService.test.js └── component.test.js
فرض کنید در حال ساخت اپلیکیشنی هستیم که ایمیلهایی را به کاربران ارسال میکند. ممکن است ساختار پروژهای که داریم به این شکل باشد:
myEmailApp ├── src │ ├── components │ │ ├── EmailForm.js │ │ └── SuccessMessage.js │ ├── services │ │ └── emailService.js │ ├── utils │ │ └── validateEmail.js └── tests ├── emailService.test.js └── EmailForm.test.js
فرمتبندی منسجم، خوانایی کد را بهبود میبخشد.
بهتر است یک الگو ثابت برای نوشتن کد خود ایجاد کنیم، مانند استفاده از دو space برای تو رفتگی یا قرار دادن یک line break قبل از commentها.
پیروی از فرمتبندی ثابت باعث میشود کد ما تمیز و سازمانیافته به نظر برسد.
هاردکد کردن به معنای تعبیه مستقیم مقادیر داده در کد است، مثل تنظیم یک ID کاربر به ۱۲۳
به جای استفاده از یک متغیر.
اجتناب از مقدارهای هاردکد شده به ما این امکان را میدهد که کد خود را بدون تغییرات مکرر استفاده کنیم. بهتر است مقادیر را در متغیرها، ثابتها یا فایلهای پیکربندی ذخیره نماییم.
مشکلی که هاردکد کردن میتواند ایجاد کند:
// Bad: Hardcoding user limit function createUser(name) { let numberOfUsers = 100; // Hardcoded value if (numberOfUsers >= 100) { return 'User limit reached.'; } // Code to create the user return 'User created.'; }
در این مثال، مقدار numberOfUsers
برابر با ۱۰۰
هاردکد شده است. اگر بخواهیم محدودیت تعداد کاربران را تغییر دهیم، باید این مقدار را در کد پیدا کرده و اصلاح کنیم. اگر این مقدار در چندین مکان استفاده شده باشد، انجام این کار دشوار بوده و مستعد خطا میباشد.
مثال بهبود یافته با استفاده از Constantها
اکنون این کد را به گونهای بازنویسی میکنیم که از یک constant استفاده کند:
// Good: Using a constant const MAX_USERS = 100; // Store the limit in a constant function createUser(name) { let numberOfUsers = getCurrentUserCount(); // Get the current count from a function or database if (numberOfUsers >= MAX_USERS) { return 'User limit reached.'; } // Code to create the user return 'User created.'; } // Example function to get current user count function getCurrentUserCount() { // Simulate fetching the current count, e.g., from a database return 90; // Example count }
MAX_USERS
در بالای کد تعریف شده است. با این روش، اگر نیاز به تغییر حداکثر تعداد کاربران داشته باشیم، تنها کافی است این مقدار را در یک مکان بهروزرسانی نماییم.getCurrentUserCount()
تعداد کاربران فعلی را به صورت پویا از یک دیتابیس یا سورس دیگر دریافت میکند. این رویکرد از هاردکد کردن تعداد جلوگیری کرده و تغییرات را آسان میکند.۱۵۰
باشد، میتوانیم مقدار MAX_USERS
را بهسادگی از ۱۰۰
به ۱۵۰
تغییر دهیم و این تغییر در تمام برنامه ما اعمال خواهد شد.MAX_USERS
) خوانایی کد ما را بهبود میبخشد. هر توسعهدهندهای که کد ما را مشاهده کند، میتواند به سرعت متوجه شود که این مقدار چه معنایی دارد.در اپلیکیشنهای بزرگتر، میتوانیم از فایلهای پیکربندی (مانند JSON ،YAML یا متغیرهای محیطی) برای ذخیره مقادیری که ممکن است بین محیطها (توسعه، آزمایشی، تولید) تغییر کنند استفاده کنیم.
برای مثال، میتوانیم در فایل config.json
مقدار maxUsers
را به صورت زیر هاردکد کنیم ( باید به این نکته توجه داشته باشیم که در config.json
بهتر است از سبک نامگذاری camelCase استفاده کنیم تا فرمتبندی ثابت بماند):
{ "maxUsers": 100, "emailService": { "service": "gmail", "user": "your-email@gmail.com", "pass": "your-email-password" } }
استفاده از پیکربندی در کد:
const config = require('./config.json'); function createUser(name) { let numberOfUsers = getCurrentUserCount(); if (numberOfUsers >= config.maxUsers) { return 'User limit reached.'; } // Code to create the user return 'User created.'; }
درک توابع طولانی سختتر بوده و نگهداری آنها دشوارتر است.
هیچ قانون سختگیرانهای در این رابطه وجود ندارد، اما به طور کلی، توابع نباید بیشتر از ۲۰-۳۰ خط باشند. اگر یک تابع مسئولیتهای متعددی دارد یا شامل مراحل زیادی است، احتمالاً طولانی است. تقسیم این توابع به «توابع کمکی» کوچکتر میتواند مدیریت آنها را سادهتر کرده و خود توابع را نیز قابلدرکتر کند.
به طور کلی در نوشتن Clean Code، کوتاه و متمرکز نگه داشتن توابع یکی از اصول کلیدی است که باعث بهبود خوانایی و سادهتر شدن کد میشود.
نمونهای از یک تابع طولانی و پیچیده:
function updateCart(cart, item, discountCode) { // Add the item to the cart cart.items.push(item); // Calculate the new total let total = 0; cart.items.forEach(cartItem => { total += cartItem.price * cartItem.quantity; }); // Apply discount if available if (discountCode) { total = applyDiscount(total, discountCode); } // Log the transaction console.log(`Item added: ${item.name}, New total: $${total}`); return total; }
این تابع کارهای متعددی انجام میدهد که عبارتند از:
این تابع ممکن است در حال حاضر قابل مدیریت به نظر برسد، اما با افزودن وظایف بیشتر به سرعت پیچیدهتر شده و نگهداری آن دشوار خواهد شد.
در ادامه، این تابع را بازنویسی کرده و آن را به توابع کوچکتر و با یک هدف مشخص تقسیم میکنیم:
function updateCart(cart, item, discountCode) { addItemToCart(cart, item); let total = calculateTotal(cart); if (discountCode) { total = applyDiscount(total, discountCode); } logTransaction(item, total); return total; } function addItemToCart(cart, item) { cart.items.push(item); } function calculateTotal(cart) { return cart.items.reduce((total, cartItem) => total + cartItem.price * cartItem.quantity, 0); } function logTransaction(item, total) { console.log(`Item added: ${item.name}, New total: $${total}`); }
addItemToCart
: این تابع فقط مسئول اضافه کردن یک آیتم به سبد خرید است. ساده بوده و هدف مشخصی دارد.calculateTotal
: این تابع قیمت کل آیتمهای موجود در سبد را محاسبه میکند. خواندن و درک آن آسان است و اگر نیاز به تغییر نحوه محاسبه قیمت باشد، تنها باید این تابع را تغییر دهیم.logTransaction
: این تابع مسئولیت ثبت جزئیات تراکنش را بر عهده دارد. اگر بخواهیم چیزی که ثبت میشود (مثلاً اضافه کردن timestamp) را تغییر دهیم، میتوانیم این کار را در این تابع انجام دهیم بدون اینکه به بقیه کد دست بزنیم.updateCart
: تابع اصلی اکنون مانند یک خلاصه از اقداماتی که انجام میشود خوانده میشود: اضافه کردن یک آیتم، محاسبه قیمت کل، اعمال تخفیفها، و ثبت نتیجه. این تابع در نگاه اول قابل فهمتر است.addItemToCart
،calculateTotal
و logTransaction
نمونههایی از توابع کمکی هستند.addItemToCart
) تا کدی که داریم به خودی خود واضح و شفاف باشد.اکنون که نکات مهمی را بررسی کردیم، بهتر است به اصول کلی بپردازیم که فلسفه پشت Clean Code نوشتن را تشکیل میدهند:
این اصول، کدنویسی را از صرف نوشتن، به طراحی راهحلها تبدیل میکند. نوشتن Clean Code یک مهارت است که با تمرین رشد میکند، بنابراین باید به یادگیری و بهبود این مهارت ادامه دهیم.
نکتهای درباره dependencyها
به جای هاردکد کردن مستقیم dependencyها در کد، بهتر است از package managerهایی مانند npm استفاده کنیم. این روش به ما این امکان را میدهد تا به راحتی dependencyها را بهروزرسانی یا حذف نماییم.
نوشتن Clean Code مانند ساختن یک پایه محکم برای یک خانه است. این کار همه چیز را مرتب نگه میدارد و افزودن ویژگیهای جدید یا رفع مشکلات را با رشد پروژه آسانتر میکند. همچنین، Clean Code نه تنها به بهبود عملکرد کد کمک میکند، بلکه همکاری با سایر توسعهدهندگان را نیز سادهتر و کارآمدتر میسازد.
با رعایت این نکات، میتوانیم عادتهایی ایجاد کنیم که کدمان را خواناتر، قابلنگهداریتر و لذتبخشتر برای کار کردن کند.
دیدگاهها: