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

Clean Code Tartışması: Ousterhout, Muratori ve Kuralların Sınırları

Mert Özen 7 Kas 2026 13 dk 4 görüntülenme
Clean Code Tartışması: Ousterhout, Muratori ve Kuralların Sınırları

Ousterhout ve Martin aynı hedefte, okuyucunun yükünü azaltmakta anlaşıyor; ayrıldıkları yer method'ları ne kadar böleceğin, comment'lerin değeri ve TDD. Yazı 2024–25 tartışmasını, Muratori'nin performans eleştirisini (C#'ta BenchmarkDotNet ile tekrarlanmış hâliyle) ve qntm'nin kitap eleştirisini iki tarafın kendi kaynaklarından özetliyor.

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

Önce Tahmin Et: Hangisi Daha Temiz?

Bookstore'un kargo ücreti kuralı üç cümle: standart kargo 39,90 TL, 500 TL ve üzeri sipariş ücretsiz, İstanbul'a ücretli gönderimde 10 TL indirim; express kargo sabit 89,90 TL. Aynı kuralın iki implementation'ı:

public sealed class ShallowShippingCalculator
{
    private decimal amount;
    private string city = "";
    private decimal fee;

    public decimal Calculate(decimal orderAmount, ShippingMethod method, string destinationCity)
    {
        SetInputs(orderAmount, destinationCity);
        if (method == ShippingMethod.Express) ApplyExpressFee();
        else CalculateStandardFee();
        return fee;
    }

    private void SetInputs(decimal orderAmount, string destinationCity)
    {
        amount = orderAmount;
        city = destinationCity;
    }

    private void ApplyExpressFee() => fee = 89.90m;

    private void CalculateStandardFee()
    {
        ApplyBaseFee();
        ApplyFreeShippingIfEligible();
        ApplyIstanbulReductionIfEligible();
    }

    private void ApplyBaseFee() => fee = 39.90m;

    private void ApplyFreeShippingIfEligible()
    {
        if (IsEligibleForFreeShipping()) fee = 0m;
    }

    private bool IsEligibleForFreeShipping() => amount >= 500m;

    private void ApplyIstanbulReductionIfEligible()
    {
        if (city == "Istanbul" && fee > 0m) fee -= 10m;
    }
}

public static class ShippingFee
{
    private const decimal FreeShippingThreshold = 500m;

    // Standart kargo 39,90; 500 TL ve üzeri ücretsiz; İstanbul'a ücretli gönderimde 10 TL indirim.
    public static decimal For(decimal amount, ShippingMethod method, string city) => method switch
    {
        ShippingMethod.Express => 89.90m,
        _ when amount >= FreeShippingThreshold => 0m,
        _ when city == "Istanbul" => 29.90m,
        _ => 39.90m,
    };
}

İkisini 3 tutar, 2 şehir ve 2 kargo tipiyle karşılaştıran küçük bir program:

All 12 cases agree.

Birinci versiyon "fonksiyonlar küçük olmalı, tek iş yapmalı, iyi isimlendirilmeli" kurallarının harfiyen uygulanmış hâli: en uzun method'u dört satır, her ismi ne yaptığını söylüyor. İkincisi tek bir switch expression ve bir comment. Hangisi daha temiz? Bir soru daha: ApplyIstanbulReductionIfEligible'ın neden fee > 0m diye kontrol ettiğini, başka hiçbir method'u okumadan anlayabiliyor musun?

Bu soru 2024 sonbaharından 2025 baharına kadar süren bir tartışmanın tam ortasında duruyor. Bu yazı o tartışmayı, Casey Muratori'nin performans eleştirisini ve qntm'nin kitap eleştirisini iki tarafın kendi kaynaklarından özetliyor.

Tartışma: Ousterhout ve Martin

John Ousterhout, Stanford'da bilgisayar bilimi profesörü ve APOSD'nin (A Philosophy of Software Design, John Ousterhout, ilk baskı 2018, ikinci baskı 2021) yazarı. Eylül 2024 ile Şubat 2025 arasında Robert C. Martin'le bir kısmı online, bir kısmı yüz yüze bir dizi görüşme yaptı ve sonucu GitHub'daki aposd-vs-clean-code repository'sinde ortak bir metin olarak yayımladılar. Metin üç konuyu ele alıyor: method uzunluğu, comment'ler ve TDD (Test-Driven Development). Clean Code'un 2025'te çıkan ikinci baskısının yayıncı içindekiler listesinde bu metin, aynı bölüm başlıklarıyla "The Clean Code Debate" adlı bir ek olarak yer alıyor.

Tartışmayı adil okumak için başlangıç noktasına bakmak gerekiyor, çünkü orada anlaşıyorlar. Ousterhout'a göre yazılım tasarımının amacı sistemi anlamayı ve değiştirmeyi kolaylaştırmak; karmaşıklığı da iki soruyla ölçüyor: geliştiricinin aklında ne kadar bilgi tutması gerekiyor ve o bilgi ne kadar görünür? Martin bu yaklaşıma katıldığını söylüyor ve bir şey ekliyor: kolaylaştırmak istediğimiz kişi kodu yazan değil, başkasının kodunu okuyan programcı. Yani iki taraf aynı hedefe bakıyor: okuyucunun yükünü azaltmak. Ayrıldıkları yer, oraya hangi kuralların götürdüğü.

Deep Module ve Shallow Method

Ousterhout'un ana kavramı deep module: arkasında çok iş yapan ama interface'i basit olan bir birim. İyi bir method, okuyucunun implementation'ı okumak yerine kısa bir interface öğrenmesini sağlar ve böylece yükü azaltır. Method'lar küçüldükçe her birinin sakladığı iş azalır, interface'ler ise çoğu zaman karmaşıklaşır; Ousterhout bunlara shallow diyor. İkinci problem entanglement: bir method'un nasıl çalıştığını anlamak için diğerini de okumak zorundaysan ikisi birbirine dolanmıştır. Method'lar arasında gidip gelerek okuduğunu fark ediyorsan bu bir uyarıdır. Ousterhout'un Clean Code eleştirisi şu: kitap "fonksiyonlar küçük olmalı, daha da küçük olmalı" derken çok güçlü ve sayısal tavsiyeler veriyor, ama ne zaman çok ileri gittiğini anlamak için neredeyse hiç rehber vermiyor.

Martin'in cevabı da kendi içinde tutarlı. Kitaptaki "iki, üç, dört satırlık fonksiyonlar" örneğinin, Kent Beck'le 1999'da TDD öğrenmek için yazdıkları Sparkle adlı bir applet'ten geldiğini, kitabın da tavsiyelerini kesin doğru diye değil değerlendirilmek üzere sunduğunu söylüyor. Aşırı bölmenin mümkün olduğunu kabul ediyor; ölçütü "one thing" kuralı: bir method'dan anlamlı bir isim verilebilecek ve orijinalinden daha az iş yapan bir parça çıkarılabiliyorsa, method birden fazla iş yapıyordur. Ousterhout bu kuralın belirsiz olduğunu ve her şeyin bir isim alabileceğini söyleyince Martin, her araç gibi bunun da kötüye kullanılabileceğini ve sonunda işin muhakemeye kaldığını savunuyor. 2008'deki kaygısının web'in ilk yıllarındaki dev fonksiyon alışkanlığını kırmak olduğunu, ikinci baskıda daha dengeli yazdığını ekliyor. Hata yapacaksa bölme tarafında hata yapmayı tercih ediyor, çünkü fazla bölünmüş method'lar sonradan inline edilebilir.

Vardıkları ortak sonuç net: ikisi de modüler tasarımı değerli buluyor, aşırı bölmenin mümkün olduğunu ve Clean Code'un ilk baskısının bunu tanımaya pek yardım etmediğini kabul ediyor. Ayrıldıkları nokta ne kadar böleceğin ve entanglement'ın ne kadar sorun olduğu: Martin bölmenin faydasının entanglement'ın bedelini karşılayabileceğini düşünüyor, Ousterhout karşılayamayacağını.

Açılıştaki örneğe bu gözle bak. ShallowShippingCalculator'da her method shallow: ApplyBaseFee tek bir atama, interface'i implementation'ından daha az şey anlatmıyor. Ve method'lar dolanmış: ApplyIstanbulReductionIfEligible'daki fee > 0m kontrolü, ücretsiz kargonun bir önceki method'da fee'yi sıfırlamış olmasına dayanıyor. Bunu bilmek için çağrı sırasını ve üç field'ın kimin tarafından ne zaman yazıldığını okuman gerekiyor; geçen yazıdaki Temporary Field kokusu da tam olarak bu. ShippingFee.For ise deep: tek bir interface, arkasında bütün kural, sırası da switch'in kendisinde görünüyor. Ama bu Martin'in reddettiği bir sonuç değil; onun "anlamlı extract" ölçütüne göre de ApplyBaseFee gibi bir method savunulamaz. Asıl ayrılık daha gri bölgede: 40 satırlık bir method'u üç tane 13 satırlığa bölmeli misin? Orada iki yazar gerçekten farklı cevap veriyor.

PrimeGenerator: Herkesin Hata Yaptığı Örnek

Tartışmanın en öğretici kısmı ortak bir kod üzerinde geçiyor. Clean Code'un ilk baskısındaki PrimeGenerator (Donald Knuth'un literate programming örneğinden uyarlanmış, ilk N asal sayıyı üreten bir Java class'ı) sekiz küçük method'a bölünmüş hâliyle Ousterhout'un hedefi oldu. Ousterhout, isNotMultipleOfAnyPreviousPrimeFactor gibi method'ların gizli side effect'leri olduğunu ve doğru çalışmalarının uzaktaki bir döngünün candidate'i nasıl artırdığına bağlı olduğunu gösterdi. Martin bu iç bölmenin sorunlu olduğunu kabul etti; örneğin amacının method'lara değil class'lara bölmeyi göstermek olduğunu söyledi.

Ousterhout algoritmayı tek bir method ve bol comment'le yeniden yazdı. Martin bu comment'lerin kendisine yardım etmediğini, birinde hata olduğunu ve "karenin neden başlangıç noktası olduğunu" ancak uzun bir bisiklet turundan sonra anladığını yazdı; Ousterhout hatayı düzeltip comment'leri yeniden yazdı. Sonra Martin kendi versiyonunu yazdı ve Ousterhout bu versiyonun okunaklı olduğunu kabul edip ölçtü: döngüyü ikiye bölmek, aday elenir elenmez çıkılan tek döngünün yerine her adımda bütün listeyi dolaşan bir döngü getirmişti. Ousterhout'un ölçümüne göre sonuç 3–4 kat yavaşlamaydı. Martin profilleme yapmadığını kabul edip döngüleri birleştirdi; kendi ölçümüne göre yeni versiyonu bir milyon asal sayı için 440 ms'de, Ousterhout'unki 561 ms'de bitiyordu. Ousterhout düzeltmeyi kabul etti ve method sayısının sekizden dörde indiğini not etti.

Bu hikâyeden çıkan ders iki taraftan birinin haklı olması değil. İkisi de aynı algoritmayı anlamakta zorlandı, ikisinin de ilk yazdığı açıklama karşı tarafa yetmedi ve küçük method üretmeye odaklanan bir revizyon, kimse ölçmediği için performansı sessizce bozdu.

Comment'ler ve TDD

Comment'ler en az uzlaştıkları konu, bunu kendi kapanış notlarında da yazıyorlar. Ousterhout'a göre interface comment'leri olmadan bir abstraction tanımlanamaz, çünkü method imzası sözleşmenin tamamını anlatmaz; eksik comment'lerin, yanlış comment'lerden çok daha fazla zaman kaybettirdiğini düşünüyor ve aynı kod için Martin'den 5–10 kat fazla comment satırı yazacağını tahmin ediyor. Uzun method isimlerini de comment'in beceriksiz bir ikamesi olarak görüyor. Martin ise comment'lerin çoğunu ancak kod doğruladığında güvenilir buluyor, sektördeki comment'lerin büyük kısmını faydasız bulduğunu söylüyor ve bilgiyi mümkünse koda, örneğin uzun bir isme taşımayı tercih ediyor. Anlaştıkları yerler: public API (Application Programming Interface) dokümante edilmeli, implementation comment'i ise yalnızca kod açık değilse gerekir. Bu serinin comment'ler yazısındaki "kodun anlatamadığını yaz" kuralı tam bu ortak zemin.

TDD'de Ousterhout, kitabındaki TDD tarifinin hatalı olduğunu kabul ediyor ama yaklaşımın tasarımı geri plana ittiğini düşünüyor ve "bundling" öneriyor: birkaç on ila birkaç yüz satır kod, ardından o kodun testleri. Martin'in tercihi kısa döngülü TDD. Unit test'lerin vazgeçilmez olduğunda ve bundling'in TDD kadar iyi sonuç verebileceğinde anlaşıyorlar; ortalama ve en kötü durumda hangisinin daha iyi sonuç verdiğinde anlaşamıyorlar. İlginç bir ayrıntı: ikinci baskının içindekiler listesinde Testing Disciplines bölümünde TDD, TCR (Test && Commit || Revert) ve Small Bundles başlıkları yan yana duruyor.

Muratori: Performansın Faturası

Şubat 2023'te Casey Muratori "Clean Code, Horrible Performance" başlıklı yazı ve videosunda başka bir açıdan saldırdı. Clean Code'un kod yapısını etkileyen beş kuralını seçti: if/switch yerine polymorphism, object'lerin iç yapısını bilmemek, küçük fonksiyonlar, tek iş yapan fonksiyonlar ve DRY (Don't Repeat Yourself). Klasik Shape hiyerarşisini C++'ta yazdı ve alan toplamını ölçtü. Virtual çağrılı versiyonu switch'le yazınca yaklaşık 1,5 kat, şekillerin ortak yapısını bilen bir katsayı tablosuyla yazınca 10 kattan fazla hızlandı; köşe sayısını da hesaba katan genişletilmiş örnekte fark yaklaşık 15 kata çıktı. DRY'ı ise büyük ölçüde kabul edilebilir buldu.

Aynı deneyi C# ve .NET 10'da BenchmarkDotNet ile tekrarladım:

[MemoryDiagnoser]
public class ShapeAreaBenchmarks
{
    private const int Count = 4096;
    private Shape[] shapes = [];
    private ShapeData[] data = [];

    // GlobalSetup: aynı 4096 rastgele şekil iki diziye de yazılıyor

    [Benchmark(Baseline = true)]
    public float VirtualCall()
    {
        float total = 0;
        foreach (Shape shape in shapes) total += shape.Area();
        return total;
    }

    [Benchmark]
    public float SwitchOnKind()
    {
        float total = 0;
        foreach (ShapeData shape in data) total += shape.Area();
        return total;
    }

    [Benchmark]
    public float CoefficientTable()
    {
        float total = 0;
        foreach (ShapeData shape in data) total += ShapeData.Coefficients[(int)shape.Kind] * shape.Width * shape.Height;
        return total;
    }
}

public abstract class Shape
{
    public abstract float Area();
}

public sealed class Circle(float radius) : Shape
{
    public override float Area() => MathF.PI * radius * radius;
}

public readonly record struct ShapeData(ShapeKind Kind, float Width, float Height)
{
    public static readonly float[] Coefficients = [1f, 1f, 0.5f, MathF.PI];

    public float Area() => Kind switch
    {
        ShapeKind.Square or ShapeKind.Rectangle => Width * Height,
        ShapeKind.Triangle => 0.5f * Width * Height,
        _ => MathF.PI * Width * Height,
    };
}
Apple M5, .NET 10.0.13, Arm64 RyuJIT

| Method           | Mean      | Error     | StdDev    | Ratio | RatioSD | Allocated | Alloc Ratio |
|----------------- |----------:|----------:|----------:|------:|--------:|----------:|------------:|
| VirtualCall      | 10.182 us | 0.2029 us | 0.4324 us |  1.00 |    0.06 |         - |          NA |
| SwitchOnKind     |  3.032 us | 0.0233 us | 0.0218 us |  0.30 |    0.01 |         - |          NA |
| CoefficientTable |  2.586 us | 0.0312 us | 0.0243 us |  0.25 |    0.01 |         - |          NA |

Square, Rectangle ve Triangle da Circle ile aynı kalıpta. 4096 şeklin alanını toplamak virtual çağrıyla 10,2 µs, switch'le 3,0 µs, tabloyla 2,6 µs sürdü; oranlar Muratori'nin C++ sonuçlarından farklı ama yön aynı. Bir uyarı: iki versiyon yalnız çağrı mekanizmasında değil bellek düzeninde de farklı. Biri heap'e dağılmış object'lere referans dizisi, diğeri yan yana duran struct'lar; bu ölçüm farkın ne kadarının hangisinden geldiğini ayırmıyor. Muratori'nin asıl noktası da zaten bu: veri düzenini bilen kod, bilmeyen koddan hızlıdır.

Martin, Muratori'yle GitHub'daki unclebob/cmuratori-discussion repository'sinde yazılı bir tartışma yaptı ve analizin özünü kabul etti: bu yapılar nanosaniye seviyesinde verimli değil. İtirazı bağlamla ilgiliydi: yazılımların büyük çoğunluğunun işlemcinin küçük bir kısmını kullandığını, aynı uygulamanın içinde bile nanosaniye, mikrosaniye ve milisaniye seviyesinde çalışan modüllerin yan yana durduğunu, kendi işinde programcı zamanını makine zamanına tercih ettiğini söyledi. Muratori'nin kapanış özeti ise ayrılığı dürüstçe koyuyor: implementation detaylarını gizlemenin ne sıklıkla doğru olduğunda ve bilgisayarın ne kadar önemli olduğunda anlaşamıyorlar. Muratori ise her gün kullandığımız birçok yazılımın (derleyiciler, tarayıcılar, ofis uygulamaları) performansın önemli olduğu kategoriye girdiğini savunuyor.

Bookstore açısından ölçüye bak: şekil başına fark yaklaşık 1,7 nanosaniye. Bir sipariş isteği veritabanına ve ödeme sağlayıcısına gidiyor, yani milisaniyelerle ölçülüyor; OrderPricing'i switch'ten polymorphism'e çevirmek ya da tersi bu isteğin süresinde ölçülebilir bir şey değiştirmez. Ama milyonlarca satırı işleyen bir import, bir rapor ya da bir parser'ın iç döngüsü başka bir hikâye. Kural basit: önce ölç, sonra kuralı bilinçli esnet. Ölçmenin yolunu C# İleri serisinin "C#'ta Performans: Allocation, Struct'lar ve BenchmarkDotNet ile Ölçmek" yazısında anlattık. İkinci baskının içindekiler listesinde Objects and Data Structures bölümünün altında "But What About Performance?" başlıklı bir kısım var; Martin konuyu kitaba da taşımış.

qntm ve İkinci Baskının Cevabı

2020'de qntm takma adlı yazar "It's probably time to stop recommending Clean Code" başlıklı yazısında kitaba daha temel bir yerden itiraz etti: örnek kodların kalitesi. Fonksiyonlar bölümünün sonundaki SetupTeardownIncluder örneğinde, on beş private method'un neredeyse hepsinin parametre almak yerine member variable'lar üzerinden birbirine veri aktardığını gösterdi; yani kitabın aynı bölümde "yalan" diye tanımladığı gizli side effect'lerle çalıştıklarını. Tek satırlık ve tek yerden çağrılan method'ların, kitabın kendi "ismi implementation'ını tekrar etmeyen bir şey çıkar" ölçütüne uymadığını da ekledi. Adil olmak için şunu da söylemek gerek: qntm aynı yazıda side effect'lerden kaçınma, command-query ayrımı ve açıklayıcı isimler gibi tavsiyeleri sağlam buluyor. İtirazı, iyi tavsiyelerle sorgulanabilir ya da eskimiş olanların aynı kitapta, aynı tonda karışması. Ayrıca kitabın yalnız Java ve yalnız nesne yönelimli programlama (OOP) üzerine kurulu olduğunu, formatting bölümünün de bugün "bir formatter seç, otomatikleştir, unut" cümlesine indiğini söylüyor.

İkinci baskı bu eleştirilerin bir kısmına doğrudan cevap veriyor gibi görünüyor, ama sınırını bilmek gerek: ben kitabın tamamını değil, yayıncının açıklamasını ve içindekiler listesini temel alıyorum. Yayıncıya göre ikinci baskı baştan yazılmış bir kitap ve örnekler Java'nın yanında JavaScript, Go, Python, Clojure, C# ve C ile veriliyor. "What Are Large Functions Anyway?" gibi başlıklar ve Ousterhout tartışmasının bütünüyle ek olarak konması, Martin'in tartışmada söylediği "daha dengeli yazdım" cümlesiyle örtüşüyor. SetupTeardownIncluder örneğinin ikinci baskıda nasıl ele alındığını ise içindekiler listesinden çıkarmak mümkün değil; bunu kendim doğrulamadan bir şey söylemiyorum.

Kuralların Altındaki İlke

Üç eleştiriyi yan yana koyunca ortak bir payda çıkıyor ve bu payda Clean Code'un kendi hedefiyle aynı: okuyucuya yük bindirme. Kurallar bu hedefe giden kısayollar; kısayol hedefle çatıştığında hedef kazanır. Bunu günlük karara çevirmek için kullandığım sorular:

  • Çıkardığım method, okuyucuyu implementation'ı okumaktan kurtarıyor mu, yoksa onu iki yer arasında gidip gelmeye mi zorluyor?
  • Bu method'u anlamak için hangi field'ların hangi sırayla yazıldığını bilmek gerekiyor mu? Gerekiyorsa method'lar dolanmıştır.
  • Kod "neden"i anlatabiliyor mu? Anlatamıyorsa kısa ve doğru bir comment, uzun bir isimden iyidir.
  • Bu kod gerçekten sıcak bir yolda mı? Değilse okunaklılık kazanır; öyleyse önce ölç, sonra veri düzenini bilen kodu yaz ve nedenini comment'te bırak.

Sık Düşülen Tuzaklar

Tartışmayı bir kazanan ilan etmek için okumak. İki yazar da bazı noktalarda geri adım attı, ikisi de PrimeGenerator'da hata yaptı. Değerli olan sonuç değil, argümanlar.

"Clean Code yavaştır" diye genellemek. Muratori'nin ölçümü gerçek, ama bir sipariş servisinin süresini veritabanı ve ağ belirler. Hot path'i ölçmeden tasarımı performans için bozmak, ölçmeden kuralı uygulamak kadar hatalı.

Ousterhout'u "uzun method yaz" diye okumak. Deep module, uzun method demek değil; basit bir interface arkasında çok iş saklayan bir birim demek. 300 satırlık bir method'un interface'i basit olabilir ama içi okunmuyorsa o da başarısız.

Comment'i her derde deva sanmak. Ousterhout'un savunduğu interface ve "neden" comment'leri; kodu satır satır tekrar eden comment'ler iki tarafın da karşı çıktığı şey.

Kitabın baskısını karıştırmak. Eleştirilerin çoğu 2008 baskısına yönelik. Bir alıntıyı tartışırken hangi baskıdan olduğunu söyle.

Küçük Bir Deneme

★☆☆ Açılıştaki ShallowShippingCalculator'ı al ve ApplyFreeShippingIfEligible ile ApplyIstanbulReductionIfEligible'ın çağrı sırasını değiştir. Hangi girdide sonuç değişiyor? Bunu bulmak için kaç method okudun?

★★☆ ShapeAreaBenchmarks'a dördüncü bir benchmark ekle: aynı Shape[] dizisi ama Shape'ten Area'yı virtual olmayan bir property olarak okuyan bir versiyon. Fark virtual çağrıdan mı, bellek düzeninden mi geliyor?

★★★ Kendi projende en uzun beş method'u bul. Her biri için Ousterhout'un sorusunu (interface implementation'dan basit mi?) ve Martin'in sorusunu (anlamlı bir isimle bir parça çıkarılabiliyor mu?) ayrı ayrı cevapla. Cevapların kaç method'da çatışıyor?

Bu yazıda kuralların sınırlarını konuştuk. Serinin son yazısında hepsini aynı yerde uyguluyoruz: legacy OrderService.cs'i characterization test'lerle koruyarak, her adımı ayrı commit olarak ve her commit'te testleri koşturarak temizliyoruz.