Mert Özen Her Satırda Daha İleri
Yazılım Tasarımı Clean Code — #04

Clean Code'da Comment'ler: Ne Zaman Yazılır, Ne Zaman Koda Dönüştürülür?

Mert Özen 7 Kas 2026 11 dk 1 görüntülenme
Clean Code'da Comment'ler: Ne Zaman Yazılır, Ne Zaman Koda Dönüştürülür?

Kodun anlatabildiğini koda (isim, const, enum, method), anlatamadığını (neden, uyarı, dış bağlam, public API contract'ı) comment'e yaz: legacy OrderService'teki comment'lerin çoğu kodu tekrar ediyor, ikisi yalan söylüyor, changelog ve commented-out kodun yeri ise Git. Yazı XML doc comment'lerin CS1572/CS1573/CS1574/CS1591 ile nasıl doğrulandığını ve Ousterhout'un interface comment savunusunu gerçek çıktılarla anlatıyor.

Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 ile derlenip denenmiştir.

Önce Tahmin Et

Legacy OrderService.cs'teki bütün comment'ler, yorum satırına alınmış kod hariç, satır numaralarıyla şunlar:

   1  // OrderService.cs
   2  // Bookstore sipariş modülü
   3  // 2017'de yazıldı, o zamandan beri herkes bir şey ekledi
  26  // Siparişi işler
  27  // 03.05.2019 A.K. - kupon eklendi
  28  // 17.11.2020 M.T. - express kargo eklendi
  29  // 09.02.2022 A.K. - VIP indirimi eklendi
  30  // 21.06.2023 S.Y. - hafta sonu indirimi eklendi
  71  // fiyatı hesapla
  81  // indirim
 102  // kargo
 104  if (flag) k += 12.5m; // hediye paketi
 107  o.Total = Math.Round((tmp - tmp2) * 1.1m + k, 2); // %10 vergi
 108  // ödeme
 126  // stok düş
 131  // puan ekle
 168  // fiyat teklifi (sepet sayfası için)
 248  // müşterinin siparişleri
 256  // sipariş geçerli mi?
 290  // kargoya ver
 323  // müşteri oluştur
 360  // fatura
 384  // durum güncelle
 406  // kitapları CSV'den yükle (isbn;title;author;price;stock;type)
 438  // eski indirim hesabı - kullanılmıyor ama silme!
 472  // ileride farklı sipariş tipleri için
 483  public int Status; // 0 yeni, 1 ödendi, 2 onaylandı, 3 kargoda, 4 iptal, 9 ödeme hatası
 575  // Ödeme sağlayıcısının SDK'sını taklit eder (third-party kod)

Bu listeyi üç gruba ayır: olduğu gibi kalacaklar, silinecekler ve koda dönüştürülecekler. Bir de şunu sor: bu comment'lerden hangisi yalan söylüyor? İpucu: comment'in yanlış olması için yanlış bir şey yazması gerekmez; eksik söylemesi de yeter.

Kodun Söyleyemediği, Comment'in Söylemesi Gereken

Benim ayırımım şöyle. Kalacak olan yalnız 575. satırdaki comment: PaymentClient class'ının gerçek bir SDK (Software Development Kit) değil, bir taklit olduğunu söylüyor ve bunu koddan çıkarmanın bir yolu yok. 406. satırdaki CSV (Comma-Separated Values) formatı açıklaması da değerli bir bilgi taşıyor, ama yeri method'un üstündeki serbest bir satır değil; aşağıda göreceğimiz XML (Extensible Markup Language) doc comment'i ya da formatı tanımlayan bir tip.

Silinecekler kalabalık. 26, 71, 81, 102, 108, 126, 131, 168, 248, 323, 360 ve 384. satırlar kodun zaten söylediğini tekrar ediyor: // kargo yazan satırın altında KargoUcretiHesapla çağrılıyor. 27–30 arasındaki tarihli satırlar bir changelog; Git bunu yazarı ve tarihiyle zaten tutuyor. 438. satırdaki "kullanılmıyor ama silme!" uyarısı ölü kodu koruyor ve neden silinmemesi gerektiğini söylemiyor. 472. satır kullanılmayan bir generic interface'i "ileride lazım olur" diye savunuyor.

Koda dönüştürülecekler en öğretici grup. 104. satırdaki // hediye paketi var, çünkü parametrenin adı flag. 107. satırdaki // %10 vergi var, çünkü 1.1m bir isim taşımıyor. 483. satırdaki // 0 yeni, 1 ödendi, ... var, çünkü Status bir int. Üçünde de comment, kodun anlatamadığını telafi ediyor. İkinci baskının Comments bölümü de "Compensating for Failure" başlığıyla açılıyor: Robert C. Martin (Clean Code kitabının yazarı) için comment, niyeti kodla ifade edemediğimiz yerde başvurduğumuz bir telafi. Telafinin ihtiyacını ortadan kaldırabiliyorsan, comment'i değil kodu düzelt.

Yalan söyleyenler ise 256 ve 290. // sipariş geçerli mi? comment'i IsValid'in bir soru sorduğunu söylüyor, ama method siparişin durumunu 2 yapıyor. // kargoya ver comment'i ShipOrder'ın kargo etiketini diske yazdığını, durumu değiştirdiğini ve müşteriye e-posta gönderdiğini söylemiyor. Kod değişti, comment değişmedi. Martin'in comment'lere temkinli yaklaşmasının asıl sebebi bu: derleyici comment'i okumaz, test comment'i doğrulamaz, refactoring aracı comment'i güncellemez.

Kötü Comment'ler ve Git'in Zaten Sakladıkları

Martin kötü comment'leri uzun bir liste hâlinde sayar; legacy dosya bu listenin neredeyse tamamını içeriyor. En sık gördüklerim:

Kodu tekrar eden comment. // stok düş ve altındaki oi.Book.Stock = oi.Book.Stock - oi.Qty; aynı bilgiyi iki kez verir. Okuyucuya bir şey kazandırmaz ama bakım maliyeti getirir: kod değişince comment'in de değişmesi gerekir.

Changelog comment'i. // 17.11.2020 M.T. - express kargo eklendi satırı kimin neyi ne zaman eklediğini söylüyor, ama hangi satırların eklendiğini, neden eklendiğini ve o günden beri ne değiştiğini söylemiyor. git log ve git blame bunların hepsini söyler; Git serisinin "Git Geçmişine Bakmak: git log, git show, git blame ve Alias'lar" yazısında bu komutları ayrıntılı görmüştük.

Commented-out kod. ProcessDataV2'nin yorum satırına alınmış dört satırı ve OldCalcDiscount ölü method'u "bir gün lazım olur" diye duruyor. Kimse silmeye cesaret edemiyor, çünkü neden orada olduklarını kimse bilmiyor. SonarAnalyzer (SonarSource'un C# için Roslyn analyzer paketi) bunları ilk derlemede yakalıyor:

OrderService.cs(439,25): warning S1144: Remove the unused private method 'OldCalcDiscount'.
OrderService.cs(446,9): warning S125: Remove this commented out code.

Silmekten korkmanın sebebi kaybetme korkusu, ama Git'te silinen kod kaybolmaz. Legacy dosyayı bir repository'ye koyup ikinci commit'te ölü kodu sildim, sonra geri aradım:

$ git log -S "OldCalcDiscount" --oneline
015f737 Remove dead code: OldCalcDiscount and commented-out ProcessDataV2
3558512 Import legacy OrderService
$ git show HEAD~1:OrderService.cs | grep -n -A4 "OldCalcDiscount"
439:        private decimal OldCalcDiscount(decimal total, int itemCount)
440-        {
441-            if (itemCount > 3) return total * 0.07m;
442-            return 0;
443-        }

git log -S (pickaxe) bir string'in eklendiği ya da silindiği commit'leri bulur. Silinen method iki komutla, satır numarasıyla geri geldi. Commit mesajı da comment'ten daha iyi bir yer: "neden silindi" sorusunun cevabı tam orada duruyor.

Comment'i İsme ve Koda Çevirmek

Telafi eden comment'lerin üçünü koda çevirelim. int Status ve onu açıklayan comment bir enum oluyor; 1.1m ve // %10 vergi bir const oluyor:

namespace Bookstore.Ordering;

public enum OrderStatus
{
    New = 0,
    Paid = 1,
    Approved = 2,
    Shipped = 3,
    Cancelled = 4,
    // Değerler veritabanında int olarak duruyor; eski kayıtlar yüzünden 5-8 boş, 9 değişmemeli
    PaymentFailed = 9
}

public static class Pricing
{
    private const decimal TaxRate = 0.10m;

    // Muhasebe yazılımı yarım kuruşları en yakın çift sayıya yuvarlıyor; fatura ile tahsil edilen
    // tutar birebir tutsun diye aynı kuralı bilerek kullanıyoruz (FIN-112).
    public static decimal TotalWithTax(decimal discountedSubtotal) =>
        Math.Round(discountedSubtotal * (1 + TaxRate), 2, MidpointRounding.ToEven);

    // TODO(#482): Kitaplar için ayrı vergi oranı gelince TaxRate ürün tipine göre seçilecek
    public static decimal TaxOf(decimal discountedSubtotal) => discountedSubtotal * TaxRate;
}
690.52
PaymentFailed = 9

OrderStatus.PaymentFailed artık kendini anlatıyor ve 9'u hatırlamak zorunda değilsin. TaxRate adı // %10 vergi'nin yerini aldı. Ama dikkat et: comment'ler tamamen kaybolmadı, yerlerini değiştirdiler. enum'un içindeki comment, değerlerin neden 4'ten 9'a atladığını söylüyor; bu bilgi koddan çıkarılamaz ve biri "düzenleyeyim" diye PaymentFailed'ı 5 yaparsa veritabanındaki eski kayıtların anlamı değişir.

// hediye paketi için de aynısı geçerli: bir önceki yazıda bool flag parametresini PlaceOrderRequest record'unun GiftWrap property'sine çevirdik ve comment'e gerek kalmadı. // fiyatı hesapla ise bir method adı olmak istiyor: UnitPriceOf(book). Kural basit: comment bir şeyin "ne" olduğunu söylüyorsa, o "ne" bir isim olabilir.

İyi Comment'ler: Neden, Uyarı ve Bağlam

Yukarıdaki TotalWithTax'in üstündeki comment, bu yazıdaki en değerli satırlardan biri. Kod MidpointRounding.ToEven kullandığını zaten söylüyor; comment ise neden kullandığını söylüyor: muhasebe yazılımı aynı kuralı kullanıyor ve fatura ile tahsil edilen tutar birebir tutmalı. (Bu gerekçe örnek için kurgulandı; gerçek projede gerekçe neyse o yazılır.) Bu bilgi koda dönüştürülemez. ToEven yerine AwayFromZero yazan biri derleme hatası almaz, testler de kuruş farkını yakalamayabilir; onu durduracak tek şey bu iki satır.

İyi comment'lerin ortak noktası, kodun taşıyamadığı bilgiyi taşımaları:

  • Neden: bir iş kuralının, bir regülasyonun ya da garip görünen bir kararın gerekçesi.
  • Uyarı: "bu method idempotent değil, retry etme", "bu sıra önemli, değiştirme".
  • Dış bağlam: bir bug'ın workaround'u ve linki, bir RFC (Request for Comments) maddesi, bir ticket numarası.
  • Niyet ve contract: public API'nin (Application Programming Interface) ne garanti ettiği, hangi exception'ı fırlattığı.

TODO comment'i de bu listeye girebilir, ama yalnız takip edilebilir olduğunda. // TODO: düzelt bir dilek; // TODO(#482): ... ise issue tracker'da sahibi ve önceliği olan bir iş. SonarAnalyzer'ın S1135 (TODO) kuralı açıkken aynı dosya şunu verdi:

Pricing.cs(23,8): warning S1135: Complete the task associated to this 'TODO' comment.

Bu uyarıyı her TODO'da görmek istemeyebilirsin; ama bir ekip kuralı olarak "issue numarası olmayan TODO merge edilmez" demek, kodda yıllarca yaşayan dileklerin önüne geçer.

Public API için XML Doc Comment

Comment'lerin bir türü diğerlerinden farklı: /// ile başlayan XML doc comment'leri. Bunlar IntelliSense'te görünür, NuGet paketine XML dosyası olarak girer ve en önemlisi, derleyici onları kısmen doğrular. GenerateDocumentationFile property'sini true yapınca derleyici XML dosyasını üretir ve doc comment'leri kontrol eder. Bilerek dört hata koyduğum bir dosya:

namespace Bookstore.Ordering;

/// <summary>Bir sipariş talebi.</summary>
/// <param name="CustomerId">Siparişi veren müşterinin numarası.</param>
/// <param name="QuantitiesByIsbn">ISBN'e göre adetler.</param>
/// <param name="CouponCode">Kupon kodu; yoksa <see langword="null"/>.</param>
public sealed record PlaceOrderRequest(int CustomerId, IReadOnlyDictionary<string, int> QuantitiesByIsbn, string? CouponCode);

/// <summary>Sipariş bir iş kuralı yüzünden reddedildiğinde fırlatılır.</summary>
public sealed class OrderRejectedException(string reason) : Exception(reason);

/// <summary>Kayıtlı müşteriler için sipariş oluşturur ve ödemeyi alır.</summary>
public sealed class OrderPlacementService
{
    /// <summary>Siparişi oluşturur ve müşterinin kayıtlı kartından tahsil eder.</summary>
    /// <param name="req">Müşteri, sepet ve kupon.</param>
    /// <returns>Yeni siparişin numarası.</returns>
    /// <exception cref="ArgumentNullException"><paramref name="request"/> null ise.</exception>
    /// <exception cref="OrderRejectedExcepton">Sepet boşsa ya da kupon tanınmıyorsa.</exception>
    public int PlaceOrder(PlaceOrderRequest request)
    {
        ArgumentNullException.ThrowIfNull(request);
        if (request.QuantitiesByIsbn.Count == 0) throw new OrderRejectedException("Empty cart");
        return 1000;
    }

    public decimal GetQuote(PlaceOrderRequest request) => 0m;
}
OrderPlacementService.cs(16,22): warning CS1572: XML comment has a param tag for 'req', but there is no parameter by that name
OrderPlacementService.cs(19,26): warning CS1574: XML comment has cref attribute 'OrderRejectedExcepton' that could not be resolved
OrderPlacementService.cs(20,45): warning CS1573: Parameter 'request' has no matching param tag in the XML comment for 'OrderPlacementService.PlaceOrder(PlaceOrderRequest)' (but other parameters do)
OrderPlacementService.cs(27,20): warning CS1591: Missing XML comment for publicly visible type or member 'OrderPlacementService.GetQuote(PlaceOrderRequest)'

Parametrenin adı değişip doc comment'i değişmeyince (CS1572, CS1573) ya da exception class'ının adı yanlış yazılınca (CS1574) derleme seni uyarıyor. Sıradan bir comment için böyle bir güvence yok. Düzeltilmiş PlaceOrder:

/// <summary>Siparişi oluşturur ve müşterinin kayıtlı kartından tahsil eder.</summary>
/// <param name="request">Müşteri, sepet ve kupon.</param>
/// <returns>Yeni siparişin numarası.</returns>
/// <exception cref="ArgumentNullException"><paramref name="request"/> null ise.</exception>
/// <exception cref="OrderRejectedException">Sepet boşsa ya da kupon tanınmıyorsa.</exception>
public int PlaceOrder(PlaceOrderRequest request)

Üretilen XML dosyasında her üye M:Bookstore.Ordering.OrderPlacementService.PlaceOrder(Bookstore.Ordering.PlaceOrderRequest) gibi bir kimlikle duruyor ve cref'ler T:Bookstore.Ordering.OrderRejectedException gibi tam tip adına çözülmüş oluyor; doküman üreten araçlar bu dosyayı okur.

Doc comment'in içeriğinde de aynı kural geçerli: kodu tekrar etme. /// <summary>Gets the quote.</summary> gibi bir summary bir şey eklemez. Değerli olan, imzadan çıkarılamayan bilgi: GetQuote'un sipariş oluşturmadığı ve tahsilat yapmadığı, PlaceOrder'ın hangi durumda hangi exception'ı fırlattığı, bir değerin hangi birimde olduğu. CS1591 her public üye için doc ister ve uygulama kodunda (bir Web API'nin controller'ları gibi) gürültüye dönüşebilir; ekipler genelde bu uyarıyı yalnız dışarıya paket olarak açılan library projelerinde açık tutar.

Karşı Görüş: Ousterhout ve Interface Comment'leri

John Ousterhout (Stanford profesörü, A Philosophy of Software Design kitabının yazarı) comment'lere Martin'den çok daha olumlu bakar. Ona göre comment, abstraction'ın parçası: iyi bir interface comment'i, çağıranın implementasyonu okumadan bilmesi gereken her şeyi söyler; kısıtları, side effect'leri, neden var olduğunu. Kod "ne"yi ve "nasıl"ı anlatır, ama tasarımcının kafasındaki "neden" ve "ne garanti ediyorum" bilgisi koddan çıkmaz. Ousterhout bu yüzden uzun method isimleriyle comment'in yerini doldurma fikrine karşı çıkar.

Martin ile 2024–2025'teki yazışmalarında (ikinci baskıda "The Clean Code Debate" adıyla ek olarak yer alıyor) iki taraf bu konuda tam anlaşmadı, ama ortak noktaları beklenenden genişti: public API'ler comment ister, bazı internal interface'ler de ister; açık olmayan kod açıklanmalıdır; comment'ler de kod gibi review edilmeli ve hatalıysa düzeltilmelidir. Ayrıldıkları yer vurgu: Martin comment'i son çare olarak görür ve önce kodu düzeltmeyi ister, Ousterhout ise comment'i tasarımın ilk adımı olarak görür ve hatta method'u yazmadan önce interface comment'ini yazmayı önerir.

Pratikte ikisini birleştiren kural şu: kodun anlatabildiğini koda anlat, anlatamadığını comment'e yaz. int Status'u enum'a çevirmek Martin'in dersi; ToEven'ın neden seçildiğini yazmak Ousterhout'un.

Sık Düşülen Tuzaklar

Comment'i kodla birlikte güncellememek. En tehlikeli comment, bir zamanlar doğru olan comment'tir. Code review'da diff'teki kodun üstündeki comment'i de oku: hâlâ doğru mu?

Her method'a bir summary yazmayı kural yapmak. "Her public method'un doc comment'i olacak" kuralı, /// <summary>Saves the order.</summary> gibi SaveOrder adını tekrar eden satırlar üretir. Kural "comment'siz anlaşılmayan her public method'un" olmalı.

Commented-out kodu "geçici olarak" bırakmak. Geçici, Git'teki her şey kadar kalıcıdır. Kodu sil, commit mesajına neden sildiğini yaz.

Comment ile kötü ismi örtmek. int d; // gün sayısı yerine int elapsedDays;. Bir değişkenin yanına açıklama yazma ihtiyacı duyduğun an, isim değiştirme zamanıdır.

Kapanış parantezi comment'leri ve bölücü satırlar. } // end foreach ya da // ======== HELPERS ======== method'un çok uzun olduğunun belirtisidir; çözüm comment değil, method'u bölmek.

#region ile büyük class'ı saklamak. Visual Studio'da katlanan bölgeler 462 satırlık bir class'ı 30 satırlık gösterir ama küçültmez. Bölgelere ayırma ihtiyacı, class'ın birden fazla sorumluluğu olduğunu söyler.

Küçük Bir Deneme

★☆☆ Legacy OrderService.cs'te 26, 71, 81, 102, 108, 126 ve 131. satırlardaki comment'leri sil. ProcessData'yı tekrar oku: hangi bilgiyi kaybettin? Kaybettiğin bir bilgi varsa onu bir method adına ya da değişken adına taşı.

★★☆ Bir projede GenerateDocumentationFile'ı aç ve <WarningsAsErrors>CS1572;CS1573;CS1574</WarningsAsErrors> ekle. Sonra bir public method'un parametresini IDE (Integrated Development Environment) ile değil elle yeniden adlandır; derleme ne diyor?

★★★ Legacy dosyayı bir Git repository'sine koy, commented-out ProcessDataV2'yi ve OldCalcDiscount'u ayrı commit'lerde sil ve git log -S ile ikisini de geri bul. Ardından git log -L 31,166:OrderService.cs ile yalnız ProcessData'nın satır aralığının geçmişini izle ve tarihli changelog comment'lerinin sağladığı bilgiyle karşılaştır.

Bir sonraki yazıda bu dosyanın görsel düzenine geçiyoruz: aynı dosyada on iki tip, rastgele sıralanmış member'lar ve 259 karakterlik tek satırlık bir LINQ (Language Integrated Query) zinciri. Newspaper metaphor'ü, member sırasını, girintinin nasıl yalan söyleyebildiğini ve dotnet format'ın bu işin hangi kısmını senin yerine yapabileceğini göreceğiz.