مقاله تخصصی باورژن

خلاصه و نکات کلیدی کتاب کلین کد

با اصول کدنویسی تمیز در کتاب Clean Code آشنا شوید، از نام‌گذاری معنادار و طراحی توابع تا مدیریت خطا، تست‌نویسی، بازآرایی کد و هم‌زمانی.

خلاصه کتاب Clean Code و اصول کدنویسی تمیز، شامل نام‌گذاری معنادار، تست‌نویسی و ریفکتور کد
این مقاله را با تمرکز بر مفاهیم و مثال‌های فنی بخوانید. زمان مطالعه تقریبی: 12 دقیقه

مدت‌ها پیش، برای مطالعه کتاب Clean Code: A Handbook of Agile Software Craftsmanship، خلاصه‌ای از نکات مهم آن تهیه کردم. امیدوارم این خلاصه برای دیگران هم مفید باشد.

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

کد تمیز (Clean Code) چیست؟

  • کیفیت کد را می‌توان در فرایند بازبینی کد (Code Review) و حتی از طریق مدت‌زمانی که برای توضیح آن صرف می‌شود، ارزیابی کرد.
  • کد تمیز باید خوانا، ساده، کارآمد، خوش‌ساخت و عاری از تکرارهای غیرضروری باشد.
  • کد شما باید برای کسب‌وکار ارزش ایجاد کند.
  • کد تمیز باعث می‌شود هنگام باز کردن یک کلاس، بتوانیم به‌سرعت ساختار و منطق آن را درک کنیم.
  • کد باید به‌اندازه‌ای خوانا و قابل‌فهم باشد که سایر توسعه‌دهندگان بتوانند به‌راحتی آن را بررسی و درک کنند. با نوشتن کد نامفهوم، وقت دیگران را تلف نکنید.

نام‌گذاری معنادار (Meaningful Names)

  • نام کلاس‌ها، متغیرها و متدها باید معنادار باشد و به‌روشنی مشخص کند هر متد چه کاری انجام می‌دهد یا هر متغیر چه مفهومی دارد.
  • نام‌هایی انتخاب کنید که تلفظ آن‌ها آسان باشد تا ارتباط و گفت‌وگو درباره کد ساده‌تر شود.
  • از مخفف‌های نامفهوم و نام‌های گمراه‌کننده پرهیز کنید، زیرا ممکن است خواننده را به برداشت اشتباه از عملکرد کد برسانند.
  • نام‌ها را متناسب با دامنه مسئله (Domain)، زمینه سیستم و مسئله‌ای که قرار است حل شود، انتخاب کنید.

توابع و متدها (Functions)

  • متد باید خوانا و قابل‌درک باشد.
  • نام متد و ساختار آن باید هدف و منظورش را به‌روشنی بیان کند.
  • متدها باید کوچک باشند، حتی بهتر است تا جای ممکن کوچک‌تر و متمرکزتر نوشته شوند.
  • طبق توصیه مطرح‌شده در این کتاب، متدها بهتر است حداکثر ۲۰ خط داشته باشند. البته این عدد یک قانون قطعی نیست، مهم‌تر از تعداد خطوط، خوانایی و انسجام متد است.

  • هر متد باید تنها یک کار مشخص انجام دهد و آن کار را به‌درستی انجام دهد.
  • برای نام‌گذاری متدها از واژه‌هایی استفاده کنید که عملکرد واقعی آن‌ها را توصیف می‌کنند.
  • تعداد ایده‌آل پارامترهای متد صفر است. پس از آن، یک یا دو پارامتر انتخاب‌های مناسبی هستند.
  • تا حد امکان از سه پارامتر یا بیشتر پرهیز کنید. اگر استفاده از آن‌ها ضروری است، دلیل مشخصی برای این تصمیم داشته باشید.
  • وجود پارامتر بولی (Boolean) اغلب نشان می‌دهد که متد چند رفتار متفاوت را مدیریت می‌کند. در چنین شرایطی، بهتر است بررسی کنید که آیا می‌توان متد را به چند متد مستقل تقسیم کرد.
  • متدها باید یا عملی را انجام دهند یا نتیجه‌ای را برگردانند، بهتر است این دو مسئولیت در یک متد با یکدیگر ترکیب نشوند.
  • از تکرار کد پرهیز کنید و منطق مشترک را در جای مناسبی قرار دهید.

کامنت‌ها (Comments)

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

قالب‌بندی کد (Formatting)

  • قالب‌بندی باید اهمیت بخش‌های مختلف کد را نشان دهد، زیرا کد علاوه بر ابزار اجرا، وسیله‌ای برای برقراری ارتباط میان توسعه‌دهندگان است.
  • کد نامرتب و شلوغ، خواندن و درک منطق برنامه را دشوار می‌کند.
  • خوانایی کد بر تمام تغییراتی که در آینده روی آن انجام می‌شود، تأثیر می‌گذارد.
  • طبق توصیه این کتاب، بهتر است کلاس‌ها حداکثر ۵۰۰ خط داشته باشند. بااین‌حال، تعداد خطوط به‌تنهایی معیار مناسبی برای سنجش کیفیت یا مسئولیت‌های یک کلاس نیست، کلاس باید به‌اندازه‌ای کوچک باشد که بتوان آن را به‌راحتی درک و نگهداری کرد.
  • برای طول خطوط کد محدودیت مشخصی در نظر بگیرید.
  • طول ۱۲۰ کاراکتر برای هر خط می‌تواند یک نقطه شروع مناسب باشد، البته این مقدار باید با استانداردهای پروژه هماهنگ باشد.
  • مفاهیم مرتبط را از نظر عمودی نزدیک به یکدیگر قرار دهید تا ساختار کد بهتر دیده شود و دنبال کردن جریان اجرای آن آسان‌تر باشد.
  • بین عملگرها، پارامترها و ویرگول‌ها از فاصله‌گذاری مناسب استفاده کنید تا کد خواناتر شود.

اشیا و ساختارهای داده (Objects and Data Structures)

  • از قانون دمتِر (Law of Demeter) پیروی کنید. طبق این قانون، یک متد متعلق به شیء O باید تعاملات خود را به موارد مشخصی محدود کند:
    • خود شیء O
    • پارامترهای متد
    • اشیایی که درون متد ایجاد یا نمونه‌سازی می‌شوند
    • اجزای مستقیم شیء O
  • از انتزاع مناسب و کپسوله‌سازی (Encapsulation) صحیح استفاده کنید.
  • از ایجاد اشیایی که صرفا مجموعه‌ای از داده‌ها را در خود نگه می‌دارند، بدون آنکه ساختار و مسئولیت مشخصی داشته باشند، پرهیز کنید. البته اشیای داده‌ای در طراحی نرم‌افزار کاربردهای مشروعی دارند و همیشه انتخاب نامناسبی نیستند.
  • اشیا معمولا جزئیات داده‌های داخلی خود را پنهان می‌کنند و از طریق متدها، رفتارهای مرتبط با آن داده‌ها را در اختیار سایر بخش‌های برنامه قرار می‌دهند.
  • ساختارهای داده معمولا داده‌ها را مستقیما در معرض دسترسی قرار می‌دهند و رفتارهای پیچیده‌ای را در خود پنهان نمی‌کنند.

مدیریت خطاها (Error Handling)

ارور
  • مدیریت خطا باید با دقت طراحی شود و بخشی از مسئولیت توسعه‌دهندگان باشد.
  • وقتی شرایط نامعتبر یا خطایی رخ می‌دهد، برنامه باید به شکلی قابل‌پیش‌بینی و مناسب به آن واکنش نشان دهد.
  • به‌جای پنهان کردن خطاها، در موارد مناسب از پرتاب اکسپشن (Throwing an Exception) استفاده کنید تا خطا به‌درستی مدیریت شود.
  • پیام‌های خطا باید اطلاعات کافی برای تشخیص و رفع مشکل داشته باشند.
  • مشخص کنید چه عملی با شکست مواجه شده، خطا در کدام بخش رخ داده و در صورت امکان، علت آن چه بوده است.
  • منطق مربوط به قوانین کسب‌وکار را از سازوکار مدیریت خطا جدا نگه دارید تا هرکدام مسئولیت مشخصی داشته باشند.
  • در موارد مناسب، به‌جای بازگرداندن null، از یک شی نتیجه مشخص یا مجموعه خالی استفاده کنید. البته این انتخاب به معنای مورد انتظار و قرارداد متد بستگی دارد.
  • تا حد امکان از ارسال null به متدها خودداری کنید، زیرا می‌تواند باعث خطاهای مربوط به مقدار تهی، مانند NullPointerException در جاوا، شود.

مرزهای سیستم (Boundaries)

system boundary

در این بخش، منظور از مرزها، نقاط تعامل برنامه با کتابخانه‌ها، فریم‌ورک‌ها و APIهای خارجی است.

  • برای کاهش وابستگی مستقیم به پیاده‌سازی‌های خارجی، تعامل با آن‌ها را تا حد امکان پشت رابط‌ها یا لایه‌های مشخصی قرار دهید.
  • برای APIهای شخص ثالث، آزمایش‌های مناسبی طراحی کنید تا رفتار آن‌ها را بررسی کنید.
  • پیش از استفاده از یک API، مستندات آن را مطالعه و قابلیت‌های موردنیاز خود را آزمایش کنید.
  • امکاناتی را که قصد استفاده از آن‌ها را دارید، به‌دقت بررسی کنید تا محدودیت‌ها و رفتارهایشان برای شما روشن باشد.
  • این اقدامات به شما کمک می‌کنند پس از انتشار نسخه‌های جدید API، تغییرات احتمالی را سریع‌تر تشخیص دهید و سازگاری برنامه را بررسی کنید.
  • برای قابلیت‌های مورد استفاده از API، تست‌های مشخصی بنویسید تا رفتار مورد انتظار آن‌ها قابل‌بررسی باشد.

تست‌های واحد (Unit Tests)

تست
  • اطمینان حاصل کنید که هر بخش از کد، رفتار مورد انتظار را دارد.
  • از اصول توسعه مبتنی بر آزمون (Test-Driven Development یا TDD) پیروی کنید:
    • پیش از نوشتن کد تولیدی، ابتدا یک تست بنویسید که شکست بخورد.
    • فقط به‌اندازه‌ای تست بنویسید که رفتار موردنظر را مشخص کند و شکست آن قابل‌مشاهده باشد.
    • فقط به‌اندازه‌ای کد تولیدی بنویسید که تست شکست‌خورده را با موفقیت پشت سر بگذارد.
  • تست‌ها را تمیز، خوانا و قابل‌نگهداری نگه دارید.
  • تست‌ها نیز باید همراه با تغییرات کد تولیدی، در صورت نیاز اصلاح شوند.
  • هرچه کد تولیدی نامرتب‌تر و پیچیده‌تر باشد، نگهداری از تست‌های آن نیز دشوارتر می‌شود.
  • از اصل FIRST برای طراحی تست‌ها پیروی کنید:
    • Fast (سریع): تست‌ها باید سریع اجرا شوند.
    • Independent (مستقل): تست‌ها نباید به ترتیب اجرای یکدیگر وابسته باشند.
    • Repeatable (تکرارپذیر): تست‌ها باید در محیط‌های مختلف، با شرایط یکسان، نتایج قابل‌اعتماد ارائه دهند.
    • Self-Validating (خوداعتبارسنج): نتیجه تست باید به‌صورت خودکار مشخص کند که تست موفق بوده یا شکست خورده است.
    • Timely (به‌موقع): تست‌ها باید در زمان مناسب، ترجیحا هم‌زمان با توسعه قابلیت، نوشته شوند.
  • تست‌ها به‌اندازه کد تولیدی اهمیت دارند، زیرا درستی رفتار برنامه و امکان تغییر مطمئن آن را تضمین می‌کنند.

کلاس‌ها (Classes)

  • طبق قرارداد پیشنهادی این کتاب، در کلاس‌های جاوا ابتدا متغیرها و سپس متدها قرار می‌گیرند. ترتیب پیشنهادی متغیرها عبارت است از:
    • فیلدهای public static final، یعنی ثابت‌های عمومی و استاتیک
    • فیلدهای private static، یعنی متغیرهای استاتیک خصوصی
    • فیلدهای نمونه‌ای خصوصی، یعنی متغیرهایی که به هر نمونه از کلاس تعلق دارند
    • پس از آن، متدهای کلاس قرار می‌گیرند.
  • نام کلاس باید مسئولیت اصلی آن را نشان دهد.
  • هر کلاس باید مسئولیت مشخص و منسجمی داشته باشد.
  • اندازه کلاس به‌تنهایی معیار مناسبی برای ارزیابی مسئولیت آن نیست، مهم این است که کلاس وظیفه‌ای روشن داشته باشد و به‌راحتی قابل‌درک باشد.
  • بتوانید مسئولیت کلاس را در چند جمله کوتاه توضیح دهید.
  • متدهای کلاس باید:
    • کوچک و خوانا باشند.
    • تا حد امکان ساده و متمرکز باشند.
    • هرکدام مسئولیت مشخصی را بر عهده بگیرند.

سیستم‌ها (Systems)

  • شناسایی و تفکیک مسئولیت‌های مختلف سیستم اهمیت زیادی دارد.
  • منطق اجرای برنامه و وابستگی‌های آن را به‌صورت ماژولار طراحی کنید تا بتوان بخش‌های مختلف سیستم را مستقل‌تر توسعه داد و تغییر داد.
  • به تزریق وابستگی (Dependency Injection) توجه کنید تا وابستگی‌ها به‌شکلی شفاف مدیریت شوند و منطق کسب‌وکار از جزئیات ساخت و پیکربندی اشیا جدا بماند.
  • طراحی صحیح یک سیستم از همان ابتدا دشوار است. بهتر است ابتدا قابلیت موردنیاز را به ساده‌ترین شکل مناسب پیاده‌سازی کنید، سپس کد را ریفکتور کنید و در ادامه، قابلیت‌های جدید را به‌تدریج توسعه دهید.
  • برای بهره‌گیری موثر از TDD، به کدی خوانا و ساختاری مناسب برای ریفکتور نیاز دارید.
  • منطق کسب‌وکار را تا حد امکان در کلاس‌های ساده جاوا (POJO) نگه دارید و با کمک تست‌ها، آن را به‌تدریج توسعه دهید و اجزای موردنیاز را به یکدیگر متصل کنید.

طراحی ساده (Emergence)

در این بخش، پنج اصل طراحی ساده که در کتاب با الهام از دیدگاه‌های کنت بک (Kent Beck) مطرح شده‌اند، مرور می‌شوند:

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

حتی با اجرای یک‌باره این اصول، نمی‌توان انتظار داشت نرم‌افزاری باکیفیت به دست آید. برای دستیابی به بهبود مستمر، باید این فرایند را بارها تکرار کنید.

هم‌زمانی (Concurrency)

  • هم‌زمانی یکی از جنبه‌های مهم طراحی و پیاده‌سازی نرم‌افزار است.
  • جداسازی مسئولیت‌ها و کاهش وابستگی میان اجزا می‌تواند به بهبود ساختار برنامه و مدیریت اجرای هم‌زمان کمک کند.
  • هم‌زمانی در شرایط مناسب می‌تواند زمان پاسخ‌گویی را کاهش دهد و کارایی برنامه را افزایش دهد.
  • هنگام طراحی قابلیت‌های هم‌زمان، نکات زیر را در نظر بگیرید:
    • هم‌زمانی سربار اجرایی و پیچیدگی بیشتری به سیستم اضافه می‌کند.
    • پیاده‌سازی و نگهداری آن می‌تواند دشوار باشد.
    • بازتولید و تشخیص خطاهای ناشی از اجرای هم‌زمان، معمولا دشوار است.
    • استفاده از هم‌زمانی ممکن است به تغییر در طراحی سیستم نیاز داشته باشد.
  • یکی از چالش‌های هم‌زمانی، تداخل میان بخش‌های مختلف برنامه و تعاملات پیچیده میان رشته‌های اجرایی (Threads) است. این وضعیت می‌تواند به بروز مشکلاتی منجر شود که در اجرای عادی برنامه مشاهده نمی‌شوند.
  • تفکیک مسئولیت‌های کلاس‌ها به درک بهتر رفتار سیستم و کاهش پیچیدگی کمک می‌کند.
  • بخش‌های همگام‌سازی‌شده را تا حد امکان کوچک نگه دارید و از قفل‌گذاری غیرضروری خودداری کنید.
  • اجرای مکرر تست‌ها به شناسایی خطاها کمک می‌کند، بااین‌حال، تست کردن رفتارهای هم‌زمان به‌دلیل وابستگی به زمان‌بندی اجرا می‌تواند دشوار باشد.
  • برای بررسی مشکلات هم‌زمانی، از تست‌های اختصاصی، اجرای مکرر سناریوهای حساس و ابزارهای مناسب برای شناسایی رقابت داده‌ای (Race Condition) استفاده کنید.

پالایش تدریجی (Successive Refinement)

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

JUnit

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

ریفکتور کد (Refactoring)

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

جاوا (Java)

  • از استفاده از wildcard در دستورهای import، مانند import java.util.*، پرهیز کنید، زیرا مشخص بودن وابستگی‌ها می‌تواند خوانایی کد را افزایش دهد.
  • برای تعریف مجموعه‌ای از مقادیر ثابت و مرتبط، استفاده از enum را در نظر بگیرید. این کار در بسیاری از موارد از تعریف ثابت‌های پراکنده یا ارث‌بری برای دسترسی به ثابت‌ها مناسب‌تر است.

نام‌گذاری (Names)

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

نتیجه‌گیری

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

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

دیدگاه‌ها


ثبت دیدگاه