Levelwise
فارسی
طراحی کد

کد تمیز (Clean Code)

کد را برای خواننده بنویس، نه برای کامپایلر. اسم‌ها باید کار را بگویند، متدها کوچک و با یک کار باشند و حالت‌های خاص زود بیرون بروند. کد تمیز یعنی تغییر دادن آن ارزان و بی‌ترس است.

بازبینی نشدهبا کمک AI نوشته شدهزمان خواندن: ۱۳ دقیقهمثال حساب کردن قیمت سبد خریدکد C# و .NET 10

نویسنده: bezzad

مشکل: کدی که فقط نویسنده‌اش می‌فهمد

در فروشگاه اینترنتی ما، یک متد قیمت نهایی سبد خرید را حساب می‌کند. این متد کار می‌کند و تست‌ها هم سبز هستند:

public decimal Calc(Order o, Customer c, string? code)
{
    decimal t = 0;
    if (o != null)
    {
        if (o.Lines.Count > 0)
        {
            foreach (var l in o.Lines)
                t += l.P * l.Q;

            if (c.Type == 2)
                t = t * 0.9m;

            if (code == "NOWRUZ")
                t = t - 50_000;
        }
        else
        {
            return 0;
        }
    }
    else
    {
        throw new Exception("error");
    }
    return t;
}

حالا تیم فروش می‌گوید: «تخفیف مشتری ویژه از این ماه ۱۵ درصد است.» یک برنامه‌نویس تازه باید این کد را عوض کند. چند مشکل دارد:

  1. باید حدس بزند. عدد ۲ یعنی مشتری ویژه؟ عدد ۰٫۹ همان تخفیف است؟
  2. می‌ترسد. نمی‌داند اگر یک خط را عوض کند، چه چیز دیگری خراب می‌شود.
  3. یک باگ پنهان را نمی‌بیند. اگر سبد خیلی ارزان باشد، کد تخفیف قیمت را منفی می‌کند. در این شلوغی کسی متوجه نمی‌شود.

کد خیلی بیشتر از نوشته شدن، خوانده می‌شود. هر بار که کسی آن را می‌خواند و گیج می‌شود، تیم وقت و پول از دست می‌دهد. کد تمیز یعنی کدی که خواندن و تغییر دادنش ارزان است.

اسم خوب: نصف کار

اسم خوب سؤال خواننده را قبل از پرسیدن جواب می‌دهد.

اسم‌های بدdecimal t = 0;if (c.Type == 2)t = t * 0.9m;متغیر یک‌حرفی یعنی چه؟عدد ۲ کدام نوع مشتری است؟عدد ۰٫۹ از کجا آمده؟خواننده باید کل متد را بخواند تا حدس بزنداسم‌های خوبdecimal subtotal = 0;if (customer.IsVip)subtotal *= 1 - VipDiscountRate;هر کلمه خودش را توضیح می‌دهد.سؤالی باقی نمی‌ماند.خواننده یک بار می‌خواند و می‌فهمد
کد سمت راست و چپ یک کار را انجام می‌دهند. فقط اسم‌ها فرق دارند.

چند قانون ساده برای اسم‌ها:

  • اسم باید هدف را بگوید. به جای t بنویس subtotal. به جای d بنویس daysSinceLastOrder.
  • از کلمه‌های کسب‌وکار استفاده کن. اگر تیم فروش می‌گوید «مشتری ویژه»، در کد هم IsVip بنویس، نه Type برابر ۲. این همان زبان مشترک در درس DDD است.
  • کلاس اسم است، متد فعل است. کلاس PriceCalculator، متد CalculateTotal.
  • مقدار بولی را مثل یک سؤال بنویس. مثل IsVip، HasStock، CanCancel.
  • مخفف ننویس. نوشتن یک اسم بلند یک بار طول می‌کشد. خواندن یک مخفف مبهم هر بار طول می‌کشد.
  • اسم‌های بی‌معنا ممنوع. کلمه‌هایی مثل Manager، Helper، Data و Info چیزی نمی‌گویند. بپرس «این کلاس دقیقاً چه کاری می‌کند؟» و همان را اسم بگذار.
آزمایش ساده: اسم را بلند بخوان. اگر برای توضیح آن باید یک جمله اضافه بگویی، اسم هنوز خوب نیست.

متد کوچک، با یک کار

متد خوب یک کار انجام می‌دهد. اگر برای توضیح کار یک متد از کلمه «و» استفاده می‌کنی، احتمالاً دو کار انجام می‌دهد.

Checkout()۶۰ خط، چند کارجدا کنCalculateTotal()3 calls, 3 linesSumLines()جمع ردیف‌هاApplyVipDiscount()تخفیف مشتری ویژهApplyCoupon()کد تخفیفهر متد یک کار دارد و اسمش همان کار را می‌گوید
متد اصلی فقط سه متد دیگر را صدا می‌زند. خواندن آن مثل خواندن فهرست یک کتاب است.

چرا متد کوچک بهتر است؟

  1. اسم متد، توضیح کد است. وقتی یک تکه کد را در متدی به اسم ApplyCoupon می‌گذاری، دیگر لازم نیست کسی آن را خط به خط بخواند.
  2. هر متد در یک سطح حرف می‌زند. متد اصلی درباره «جمع، تخفیف، کد تخفیف» حرف می‌زند. جزئیات حلقه و ضرب در متدهای پایین‌تر است.
  3. تست ساده‌تر می‌شود. هر قانون کوچک را جدا تست می‌کنی.
  4. تغییر جای مشخص دارد. تخفیف مشتری ویژه عوض شد؟ فقط متد ApplyVipDiscount عوض می‌شود.

درباره پارامترها هم دو نکته:

  • پارامتر کم بهتر است. اگر یک متد پنج یا شش پارامتر دارد، معمولاً چند تا از آن‌ها با هم یک مفهوم هستند. آن‌ها را در یک record جمع کن.
  • پارامتر بولی را حذف کن. خواندن فراخوانی Place با مقدار true سخت است. این true یعنی چه؟ بهتر است دو متد با اسم روشن بسازی، مثل PlaceAsDraft و PlaceAndPay.

شرط خروج در ابتدا (Guard Clause)

شرط‌های تو در تو کد را به شکل یک پیکان به سمت راست می‌برند. خواننده باید همه شرط‌ها را در ذهن نگه دارد تا به کار اصلی برسد.

کد پیکانی: تو در توif (order is not null)if (order.Lines.Count > 0)if (customer.IsActive)// the real workelse throw ...;else return 0;else throw ...;کار اصلی در عمق سوم گم شده استهر شرط و جوابش از هم دورندشرط خروج در ابتداif (order is null) throw ...;if (order.Lines.Count == 0) return 0;if (!customer.IsActive) throw ...;// the real workاول حالت‌های خاص بیرون می‌روندکار اصلی صاف و بدون تورفتگی است

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

عدد جادویی و تکرار

عدد جادویی عددی است که وسط کد آمده و هیچ اسمی ندارد. مثل عدد ۲ یا ۰٫۹ در کد بالا. به آن یک اسم بده:

private const decimal VipDiscountRate = 0.10m;

public enum CustomerType { Regular, Vip }

public sealed class Customer
{
    public CustomerType Type { get; init; }
    public bool IsVip => Type == CustomerType.Vip;
}

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

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

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

بدون اثر پنهان

اسم متد قول می‌دهد که چه کاری انجام شود. متد نباید کاری بیشتر از اسمش انجام دهد.

بدیک کار پنهان

public Cart GetCart(Guid customerId)
{
    var cart = _db.Carts.Find(customerId);
    if (cart is null)
    {
        cart = new Cart(customerId);
        _db.Carts.Add(cart);
        _db.SaveChanges();
    }
    return cart;
}

اسم می‌گوید «بخوان». ولی متد گاهی در دیتابیس می‌نویسد.

خوباسم، همه کار را می‌گوید

public Cart GetOrCreateCart(Guid customerId)
{
    // same body as before
}

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

یک قانون ساده‌تر هم هست: متدی که چیزی را برمی‌گرداند، چیزی را تغییر ندهد. متدی که چیزی را تغییر می‌دهد، فقط نتیجه کار را برگرداند. همین ایده در مقیاس بزرگ‌تر اسم CQRS گرفته است.

کامنت: «چرا»، نه «چه»

کد خوب خودش می‌گوید چه کاری انجام می‌دهد. کامنت برای چیزی است که کد نمی‌تواند بگوید: چرا.

// Bad: repeats the code.
// Subtract the coupon amount from the total.
total -= coupon.Amount;

// Good: explains a decision the code cannot show.
// Finance asked that a coupon never makes the price negative.
total = Math.Max(0, total - coupon.Amount);
  • کامنت جای اسم بد را نمی‌گیرد. اگر می‌خواهی کنار یک متغیر کامنت بگذاری، اول اسمش را بهتر کن.
  • کامنت قدیمی بدتر از هیچ است. کد عوض می‌شود، ولی کسی کامنت را عوض نمی‌کند. بعد از مدتی کامنت دروغ می‌گوید.
  • کد مرده را پاک کن. کد کامنت‌شده را نگه ندار. تاریخچه آن در Git هست.

کد کامل بعد از تمیز کردن

همان متد اول درس، بعد از همه این قانون‌ها:

public sealed class PriceCalculator
{
    private const decimal VipDiscountRate = 0.10m;

    public decimal CalculateTotal(Order order, Customer customer, Coupon? coupon)
    {
        ArgumentNullException.ThrowIfNull(order);
        if (order.Lines.Count == 0)
            return 0;

        var subtotal = SumLines(order);
        var afterVipDiscount = ApplyVipDiscount(subtotal, customer);
        return ApplyCoupon(afterVipDiscount, coupon);
    }

    private static decimal SumLines(Order order) =>
        order.Lines.Sum(line => line.UnitPrice * line.Quantity);

    private static decimal ApplyVipDiscount(decimal amount, Customer customer) =>
        customer.IsVip ? amount * (1 - VipDiscountRate) : amount;

    // Finance asked that a coupon never makes the price negative.
    private static decimal ApplyCoupon(decimal amount, Coupon? coupon) =>
        coupon is null ? amount : Math.Max(0, amount - coupon.Amount);
}

به چند چیز دقت کن:

  1. حالا تغییر تیم فروش یک خط است. فقط مقدار VipDiscountRate عوض می‌شود.
  2. باگ پنهان پیدا شد. وقتی ApplyCoupon جدا شد، سؤال «اگر قیمت منفی شود چه؟» خودش به ذهن رسید.
  3. کد تخفیف دیگر یک رشته ثابت در کد نیست. حالا یک شیء Coupon است که از دیتابیس می‌آید.
  4. خطای مبهم حذف شد. به جای یک Exception با پیام «error»، خطای دقیق و استاندارد داریم.

عادت‌های تیمی

کد تمیز فقط کار یک نفر نیست. چند عادت کمک می‌کند کل تیم تمیز بنویسد:

  • قانون پیشاهنگ (Boy Scout Rule). هر بار که به یک فایل دست می‌زنی، آن را کمی تمیزتر از قبل رها کن. یک اسم بهتر، یک متد کوچک‌تر. لازم نیست همه چیز را یک روز درست کنی.
  • قالب یکسان، خودکار. با فایل editorconfig و دستور dotnet format، قالب کد را ابزار چک می‌کند. بحث درباره فاصله‌ها در code review وقت تلف کردن است.
  • ساده نگه دار (KISS). ساده‌ترین راهی که درست کار می‌کند، معمولاً بهترین است.
  • چیزی که لازم نیست را نساز (YAGNI). اینترفیس و تنظیمات برای «شاید روزی لازم شود» کد را سنگین می‌کند.

اشتباه‌های رایج

اشتباه چرا بد است؟ راه درست
اسم‌های کوتاه و مبهم مثل x و tmp خواننده باید کل متد را بخواند تا معنا را حدس بزند. اسمی که هدف را بگوید، حتی اگر بلندتر باشد.
متد ۲۰۰ خطی با چند کار فهمیدن، تست و تغییر آن سخت است. هر کار در یک متد با اسم روشن.
شرط‌های تو در تو کار اصلی در عمق گم می‌شود. شرط خروج در ابتدای متد.
عدد و رشته ثابت وسط کد معنا ندارد و در چند جا تکرار می‌شود. ثابت با اسم، یا enum.
کامنت به جای اسم خوب کامنت قدیمی می‌شود و دروغ می‌گوید. اول اسم را درست کن.
تکه‌تکه کردن افراطی ده متد یک‌خطی که مدام بین آن‌ها می‌پری. متد را وقتی جدا کن که یک مفهوم مستقل باشد.
بازنویسی کامل کد قدیمی در یک روز ریسک بالا، بدون تست، و توقف کار تیم. تمیز کردن کم‌کم، همراه با تست.

کد تمیز تا کجا؟

درست

  • اسم‌ها، کار را می‌گویند.
  • متدها کوچک‌اند، ولی هر کدام یک مفهوم واقعی هستند.
  • کد ساده است، حتی اگر کمی تکرار دارد.
  • هدف این است که هم‌تیمی سریع بفهمد.

افراطی

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

خلاصه در شش خط

  1. کد بیشتر خوانده می‌شود تا نوشته. برای خواننده بنویس.
  2. اسم باید هدف را بگوید و از کلمه‌های کسب‌وکار باشد.
  3. هر متد یک کار انجام دهد. متد اصلی مثل فهرست خوانده شود.
  4. حالت‌های خاص را با شرط خروج، اول متد بیرون بفرست.
  5. عدد جادویی را اسم بگذار. کامنت فقط برای «چرا» است.
  6. هر بار کمی تمیزتر کن، ولی افراط نکن. سادگی مهم‌تر از ظاهر حرفه‌ای است.