Mert Özen Her Satırda Daha İleri
Yazılım Tasarımı Refactoring — #14

Refactoring Kataloğu 5: Koşulları Sadeleştirmek — Decompose/Consolidate Conditional, Guard Clauses, Replace Conditional with Polymorphism ve Introduce Special Case

Mert Özen 15 Kas 2026 15 dk 11 görüntülenme
Refactoring Kataloğu 5: Koşulları Sadeleştirmek — Decompose/Consolidate Conditional, Guard Clauses, Replace Conditional with Polymorphism ve Introduce Special Case

Koşul refactoring'leri karmaşık if'leri adı olan fonksiyonlara, guard clause'lara, switch expression'a ya da polymorphism'e çevirir; null kontrollerini kaldıran bir special case ise kontrolün sessizce koruduğu yan etkiyi de kaldırabilir. Yazıda Bookstore'un CheckoutPolicy class'ı yedi refactoring'le dönüştürülüyor; dotnet format'ın IDE0066 çıktısı, testler yeşilken misafire 12 puan gösteren special case ve Release build'de kaybolan Debug.Assert gerçek çıktılarla gösteriliyor.

Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 (SDK 10.0.404) ile, testler xUnit.net v3 4.0.3 ve Verify.XunitV3 32.0.0 ile derlenip denenmiştir.

Önce Tahmin Et: Misafir İçin null Yerine Bir Nesne Koyunca Testler Ne Der?

Katalog bloğunun beşinci yazısı koşulları sadeleştiren yedi refactoring'i ele alıyor: Decompose Conditional, Consolidate Conditional Expression, Replace Nested Conditional with Guard Clauses, Replace Control Flag with Break, Replace Conditional with Polymorphism, Introduce Special Case ve Introduce Assertion. Adlar refactoring.com/catalog'daki gibi; açıklama ve örnekler bizim. Sahne, sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) Ordering module'ündeki CheckoutPolicy. Sepetin sahibi giriş yapmamış bir misafir olabiliyor ve bu durum kodda null ile ifade ediliyor:

public sealed record Cart(Customer? Customer, IReadOnlyList<CartLine> Lines, PaymentMethod Payment)
{
    public decimal Subtotal => Lines.Sum(l => l.UnitPrice * l.Quantity);
}

public string Greeting(Cart cart)
{
    if (cart.Customer == null)
        return "Welcome, guest!";
    return $"Welcome back, {cart.Customer.Name}!";
}

public int AwardLoyaltyPoints(Cart cart)
{
    int points = (int)(cart.Subtotal / 10);
    if (cart.Customer != null)
        cart.Customer.LoyaltyPoints += points;
    return points;
}

public string LoyaltyBadge(Cart cart)
{
    if (cart.Customer == null)
        return "0 points";
    return $"{cart.Customer.LoyaltyPoints} points";
}

Aynı null kontrolü Discount ve CheckoutError'da da var. Yazının ortasında bu kontrolleri bir special case nesnesiyle kaldıracağız: misafir artık null değil, Customer.CreateGuest() ile oluşturulan ve puanı 0 olan bir Customer. Puan veren method'daki if de gereksiz görünüyor, çünkü artık her sepetin bir müşterisi var:

public bool IsGuest { get; private init; }

public static Customer CreateGuest() => new() { Name = "guest", IsGuest = true };

// CheckoutPolicy
public int AwardLoyaltyPoints(Cart cart)
{
    int points = (int)(cart.Subtotal / 10);
    cart.Customer.LoyaltyPoints += points;
    return points;
}

public string LoyaltyBadge(Cart cart) => $"{cart.Customer.LoyaltyPoints} points";

Güvenlik ağı, her sepet tipi, her ödeme yöntemi ve iki tarih için bütün method'ların çıktısını tutan bir Verify snapshot'ı (36 kombinasyon) ve puan veren method için iki test. Bu değişiklikten sonra testler yeşil kalır mı? Sipariş onay sayfası önce AwardLoyaltyPoints'i, sonra LoyaltyBadge'i çağırıyor. 120 TL'lik sepetle alışveriş yapan bir misafir o sayfada ne görür?

Decompose Conditional ve Consolidate Conditional Expression

Önce indirim hesabı. Characterization testinden önceki hâli şöyle:

public decimal Discount(Cart cart, DateOnly today)
{
    if (cart.Customer == null)
        return 0m;
    if (cart.Lines.Count == 0)
        return 0m;
    if (cart.Payment == PaymentMethod.CashOnDelivery)
        return 0m;

    if (today.Month == 11 && today.Day >= 24 && today.Day <= 30 && cart.Subtotal >= 250m)
        return Math.Round(cart.Subtotal * 0.15m, 2, MidpointRounding.AwayFromZero);
    else
        return Math.Round(cart.Subtotal * 0.05m, 2, MidpointRounding.AwayFromZero);
}

İkinci if'in koşulu ne yaptığını değil nasıl yaptığını anlatıyor; okuyan kişi "24 ile 30 Kasım arası" ifadesinden kampanya haftasını kendisi çıkarmak zorunda. Decompose Conditional, koşulu ve her iki dalı ayrı ayrı adı olan fonksiyonlara çıkarır. Aslında onuncu yazıdaki Extract Function'ın koşullara uygulanmış hâli; Clean Code serisinin "Code Smell Kataloğu" yazısındaki Long Function smell'ine cevap. Mekanik adımlar: koşulu bir fonksiyona çıkar, test et; "then" dalını çıkar, test et; "else" dalını çıkar, test et.

Üstteki üç if farklı bir sorun: üçü de aynı cevabı veriyor ve aslında tek bir soruyu soruyor: "bu sepet indirime uygun mu?" Consolidate Conditional Expression, aynı sonucu veren kontrolleri tek bir koşulda birleştirir ve sonra o koşulu adı olan bir fonksiyona çıkarır. Birleştirmeden önce kontrollerin yan etkisi olmadığından emin ol; bir kontrol log yazıyor ya da bir sayaç artırıyorsa birleştirmek o yan etkinin ne zaman çalıştığını değiştirir. C#'ta birleştirme için || zinciri yerine property pattern'ler daha okunur:

public decimal Discount(Cart cart, DateOnly today)
{
    if (!IsEligibleForDiscount(cart))
        return 0m;

    return QualifiesForCampaign(cart, today) ? CampaignDiscount(cart) : RegularDiscount(cart);
}

private static bool IsEligibleForDiscount(Cart cart) =>
    cart is not ({ Customer: null } or { Lines.Count: 0 } or { Payment: PaymentMethod.CashOnDelivery });

private static bool QualifiesForCampaign(Cart cart, DateOnly today) =>
    IsCampaignWeek(today) && cart.Subtotal >= CampaignMinimum;

private static bool IsCampaignWeek(DateOnly today) =>
    today is { Month: 11, Day: >= 24 and <= 30 };

private static decimal CampaignDiscount(Cart cart) => Round(cart.Subtotal * 0.15m);

private static decimal RegularDiscount(Cart cart) => Round(cart.Subtotal * 0.05m);

Decompose ve consolidate iki ayrı commit; ikisinden sonra da snapshot değişmedi. Lines.Count: 0 bir extended property pattern; iç içe property'lere nokta ile inmeyi sağlıyor. Kampanya haftası artık tek bir yerde ve adıyla duruyor; tarih değişince değişecek satır belli.

Replace Nested Conditional with Guard Clauses ve Replace Control Flag with Break

Sepetin ödemeye geçip geçemeyeceğini söyleyen method üç seviye iç içe ve sonucu bir değişkende biriktiriyor:

public string? CheckoutError(Cart cart)
{
    string? error;
    if (cart.Lines.Count > 0)
    {
        if (cart.Customer != null || cart.Payment != PaymentMethod.BankTransfer)
        {
            bool tooMany = false;
            foreach (CartLine line in cart.Lines)
            {
                if (!tooMany && line.Quantity > OrderSettings.MaxQuantityPerLine)
                    tooMany = true;
            }
            if (tooMany)
                error = $"At most {OrderSettings.MaxQuantityPerLine} copies per line.";
            else
                error = null;
        }
        else
        {
            error = "Guests cannot pay by bank transfer.";
        }
    }
    else
    {
        error = "The cart is empty.";
    }
    return error;
}

İç içe koşul, bütün dalların eşit derecede önemli olduğunu söyler. Burada öyle değil: boş sepet ve misafirin havaleyle ödemesi istisnai durumlar, asıl iş satırları kontrol etmek. Replace Nested Conditional with Guard Clauses istisnai durumu en üste alır ve hemen döner. Mekanik adımlar: en dıştaki koşulu bir guard clause'a çevir (koşulu ters çevir, dalın sonucunu hemen döndür), test et, bir sonraki seviyeye geç. Koşulu ters çevirirken De Morgan kuralına dikkat: cart.Customer != null || cart.Payment != PaymentMethod.BankTransfer'ın tersi cart.Customer == null && cart.Payment == PaymentMethod.BankTransfer. Visual Studio ve Visual Studio Code'daki "Invert if" refactoring'i bu ters çevirmeyi elle yapmanın riskini alıyor. Bu yazının dördüncü yazıdan farkı mekanik adımlar; guard clause fikrinin kendisini orada kupon kodunda görmüştük.

Guard clause'lardan sonra geriye tooMany kaldı. Bu bir control flag: döngünün ne zaman bitmesi gerektiğini söyleyen bir değişken. Döngü ilk uzun satırı bulduktan sonra da bütün listeyi dolaşıyor ve !tooMany kontrolü her turda tekrar ediliyor. Replace Control Flag with Break, bayrağın değiştiği yere break, continue ya da return koyar ve bayrağı siler. Burada aradığımız şey zaten method'un sonucu olduğu için return en kısası:

public string? CheckoutError(Cart cart)
{
    if (cart.Lines.Count == 0)
        return "The cart is empty.";
    if (cart.Customer == null && cart.Payment == PaymentMethod.BankTransfer)
        return "Guests cannot pay by bank transfer.";

    foreach (CartLine line in cart.Lines)
    {
        if (line.Quantity > OrderSettings.MaxQuantityPerLine)
            return $"At most {OrderSettings.MaxQuantityPerLine} copies per line.";
    }
    return null;
}

Bir arama döngüsünün kalanı genelde LINQ'e (.NET'in collection'lar üzerinde sorgu yazma API'si) çevrilebilir; bir sonraki commit döngüyü cart.Lines.Any(l => l.Quantity > OrderSettings.MaxQuantityPerLine)'a çevirdi. Bu, on üçüncü yazıdaki Replace Loop with Pipeline. Üç commit, üç kez yeşil snapshot.

Replace Conditional with Polymorphism: Önce Switch Expression

Ödeme yöntemine göre karar veren üç method vardı: ücret, ekranda görünen ad ve onay notu. Üçü de aynı PaymentMethod enum'u üzerinde break'li klasik switch statement'lardı. Polymorphism'e geçmeden önce C#'ın kendi aracına baktım: IDE0066 kuralı, switch statement'ın bir switch expression'a çevrilebildiği yerleri bildirir. Kuralı .editorconfig'te warning yapıp build aldım, sonra düzeltmeyi komut satırından uyguladım:

$ dotnet build --no-incremental src/Bookstore.Ordering
src/Bookstore.Ordering/CheckoutPolicy.cs(79,9): warning IDE0066: Use 'switch' expression
src/Bookstore.Ordering/CheckoutPolicy.cs(98,9): warning IDE0066: Use 'switch' expression
src/Bookstore.Ordering/CheckoutPolicy.cs(113,9): warning IDE0066: Use 'switch' expression

$ dotnet format style src/Bookstore.Ordering --diagnostics IDE0066 -v n
Formatting code files in workspace 'src/Bookstore.Ordering/Bookstore.Ordering.csproj'.
Formatted code file 'src/Bookstore.Ordering/CheckoutPolicy.cs'.

Araç üç switch'i de çevirdi. Ücret method'unun sonucu:

public decimal PaymentFee(Cart cart)
{
    var fee = cart.Payment switch
    {
        PaymentMethod.Card => 0m,
        PaymentMethod.BankTransfer => 0m,
        PaymentMethod.CashOnDelivery => cart.Subtotal >= 1000m ? 0m : 15m,
        _ => throw new ArgumentOutOfRangeException(nameof(cart), cart.Payment, "Unknown payment method."),
    };
    return fee;
}

Davranış aynı, ama araç ekip kuralına uymayan bir var bıraktı ve ara değişkeni korudu. Ayrı bir commit'te method'ları expression-bodied yaptım ve aynı sonucu veren iki kolu PaymentMethod.Card or PaymentMethod.BankTransfer ile birleştirdim. Plan bu tür sadeleştirmeyi "polymorphism'e gitmeden" diye tarif ediyor ve çoğu kod için durulacak yer de burası.

Peki ne zaman bir adım daha atılır? Elimizde aynı enum üzerinde üç switch var; Clean Code serisindeki adıyla Repeated Switches smell'i. Yeni bir ödeme yöntemi (örneğin kapıda kartla ödeme) gelince üç yere birden dokunmak gerekecek. Dördüncü yazının "yeni tip mi, yeni işlem mi" tablosu burada da geçerli; onu tekrar etmiyorum. Bookstore'da ödeme yöntemlerine yenisinin eklenmesi, ödeme yöntemlerine yeni bir soru sorulmasından daha olası. Replace Conditional with Polymorphism'in mekanik adımları: her tip için bir class aç (yoksa) ve doğru instance'ı döndüren tek bir fabrika yaz; switch'li method'u ortak tipe taşı; her kolu ilgili class'taki override'a ya da implementation'a kopyala ve test et; switch'te geriye yalnız fabrika kalınca kolları sil.

public interface IPaymentOption
{
    string Label { get; }
    decimal Fee(decimal subtotal);
    string ConfirmationNote(decimal subtotal);
}

public sealed class CashOnDeliveryPayment : IPaymentOption
{
    private const decimal FreeAbove = 1000m;
    private const decimal CourierFee = 15m;

    public string Label => "Cash on delivery";
    public decimal Fee(decimal subtotal) => subtotal >= FreeAbove ? 0m : CourierFee;
    public string ConfirmationNote(decimal subtotal) =>
        FormattableString.Invariant($"Please have {subtotal + Fee(subtotal):0.00} TRY ready for the courier.");
}

public static class PaymentOptions
{
    public static IPaymentOption For(PaymentMethod method) => method switch
    {
        PaymentMethod.Card => new CardPayment(),
        PaymentMethod.BankTransfer => new BankTransferPayment(),
        PaymentMethod.CashOnDelivery => new CashOnDeliveryPayment(),
        _ => throw new ArgumentOutOfRangeException(nameof(method), method, "Unknown payment method."),
    };
}

// CheckoutPolicy
public decimal PaymentFee(Cart cart) => PaymentOptions.For(cart.Payment).Fee(cart.Subtotal);

CardPayment ve BankTransferPayment aynı şekilde. Ortak tip olarak bir interface seçtim; bir base class da olabilirdi ama ortak davranış yok, yalnız ortak soru var. Enum hâlâ duruyor, çünkü sepetle birlikte saklanan ve API'den gelen değer o; switch de yok olmadı, sistemin sınırındaki fabrikaya indi. Enum'un yerine doğrudan subclass'lar koymak ayrı bir refactoring, Replace Type Code with Subclasses; onu serinin son yazısında inheritance ailesiyle birlikte ele alacağız. Bu yapıların kendisi Design Patterns serisinin "Strategy Pattern Derinlemesine" ve "State Pattern" yazılarında; burada yalnız oraya giden adımlar var.

Introduce Special Case: Misafir Bir Müşteridir

cart.Customer == null kontrolü dört method'da tekrar ediyordu ve her biri misafir için aynı türden bir cevap veriyordu: selamlama, sıfır puan, indirim yok, havale yok. Introduce Special Case, aynı özel değere verilen bu dağınık cevapları o değeri temsil eden bir nesnede toplar. null için kullanıldığında daha yaygın adı Null Object; Design Patterns serisinin "GoF Dışında Bilmen Gereken Kod Seviyesi Pattern'ler: Null Object, Specification, Result ve Pipeline" yazısı pattern'in kendisini anlatıyor. Mekanik adımlar: asıl tipe "özel durum mu?" diyen bir property ekle (IsGuest); özel durumu temsil eden nesneyi oluşturan bir fonksiyon yaz (CreateGuest); null üreten yerleri bu fonksiyonu çağıracak hâle getir; her null kontrolünü ya kaldır (nesnenin kendi değeri yetiyorsa) ya da IsGuest'e çevir.

C# burada nullable reference types sayesinde yardımcı oluyor. Cart'taki Customer?'ı Customer yapınca derleyici null geçen bütün yerleri listeledi:

tests/Bookstore.Ordering.Tests/CheckoutPolicyTests.cs(13,36): warning CS8625: Cannot convert null literal to non-nullable reference type.
tests/Bookstore.Ordering.Tests/CheckoutPolicyTests.cs(14,35): warning CS8625: Cannot convert null literal to non-nullable reference type.

İkisi de testlerdeydi; üretim kodunda sepet oluşturan yer olsaydı listede o da çıkacaktı. Selamlama ve havale kontrolünde misafir gerçekten farklı davranıyor; oralarda null kontrolü IsGuest'e döndü ve IsEligibleForDiscount'taki pattern { Customer.IsGuest: true } oldu. Rozet ve puan method'larındaki kontrolleri ise yukarıdaki gibi kaldırdım, çünkü misafirin puanı zaten 0. Snapshot yeşil, iki test yeşil; 36 testin tamamı geçti ve commit atıldı.

Cevap: Testler Yeşil, Misafir 12 Puan Görüyor

Testler yeşil kaldı, ama misafir onay sayfasında "12 points" görüyor. Eski kodda null kontrolü iki işi birden yapıyordu: misafire puanı göstermek yerine 0 göstermek ve misafire puan yazmamak. İkinci iş görünmezdi, çünkü null'a bir şey yazılamaz. Special case nesnesi ise yazılabilir bir nesne; kontrol kalkınca puan ona yazıldı. Bunu fark etmem için onay sayfasının yaptığını yapan bir test yazmam gerekti:

failed Bookstore.Ordering.Tests.CheckoutPolicyTests.Guest_badge_stays_at_zero_after_checkout (0ms)
  Assert.Equal() Failure: Strings differ
             ↓ (pos 0)
  Expected: "0 points"
  Actual:   "12 points"
             ↑ (pos 0)

Characterization testleri her method'u ayrı ayrı sabitliyordu; iki method'un sırayla çağrılmasını değil. Düzeltme, kuralı special case'in kendisine taşımak: puanı artık dışarıdan kimse yazamıyor, müşteri kendisi kazanıyor.

private int _loyaltyPoints;

public int LoyaltyPoints { get => _loyaltyPoints; init => _loyaltyPoints = value; }

public void EarnPoints(int points)
{
    // Misafir kazanacağı puanı görür ama puan biriktirmez.
    if (IsGuest)
        return;
    _loyaltyPoints += points;
}

Bu commit davranış değiştirdiği için fix: ile başlıyor: refactoring commit'inin bozduğu davranışı geri getiriyor. Setter'ın kalkması on birinci yazıdaki Encapsulate Variable'ın bir tekrarı ve asıl korumayı o sağlıyor. Bir not daha: CreateGuest her çağrıda yeni bir nesne döndürüyor. Special case çoğu zaman static ve tek bir instance olarak yazılır; öyle yazılsaydı ve setter dururken, 12 puan bütün misafirlerin ortak nesnesine yazılır ve bir sonraki misafire de görünürdü. Tek instance kullanacaksan nesneyi immutable yap.

Introduce Assertion: Debug.Assert Doğrulama Değildir

Bazı koşullar kodun kendi varsayımıdır: doğru yazılmış bir program için hep doğru olmaları gerekir. İndirim hiçbir zaman negatif olmamalı ve sepet tutarını geçmemeli. Introduce Assertion bu varsayımı koda yazar; okuyan kişi yorum satırı aramak zorunda kalmaz ve varsayım bozulursa geliştirme sırasında hemen fark edilir:

decimal discount = QualifiesForCampaign(cart, today) ? CampaignDiscount(cart) : RegularDiscount(cart);
Debug.Assert(discount >= 0 && discount <= cart.Subtotal, "A discount is never negative and never exceeds the subtotal.");
return discount;

Önemli ayrıntı: Debug.Assert, [Conditional("DEBUG")] ile işaretli. Release build'de çağrı derlenmez, yani koşul hiç değerlendirilmez. Negatif fiyatlı bir sepetle küçük bir file-based app denedim:

$ dotnet run assert.cs
Process terminated.
Assertion failed.
A discount is never negative and never exceeds the subtotal.
   at Bookstore.Ordering.CheckoutPolicy.Discount(Cart cart, DateOnly today) in src/Bookstore.Ordering/CheckoutPolicy.cs:line 39
   at Program.<Main>$(String[] args) in samples/assert.cs:line 9

$ dotnet run -c Release assert.cs
Discount: -6.00

Debug'da process sonlandı, Release'te negatif indirim sessizce hesaplandı. Testlerde ise xUnit.net v3 başarısız assertion'ı process'i kapatmadan test hatasına çeviriyor ("Trace/Debug.Assert() Failure"). Buradaki asıl sorun assertion değil: negatif fiyat kodun bir varsayımı değil, dışarıdan gelen bir girdi. Girdi sistemin sınırında, her build'de çalışan bir kontrolle doğrulanır. Bunu ayrı bir fix: commit'i olarak CartLine'a ekledim:

public sealed record CartLine(string Isbn, string Title, decimal UnitPrice, int Quantity, bool IsEbook)
{
    public decimal UnitPrice { get; } = UnitPrice >= 0
        ? UnitPrice
        : throw new ArgumentOutOfRangeException(nameof(UnitPrice), UnitPrice, "A price cannot be negative.");
}

Artık aynı örnek iki build'de de ArgumentOutOfRangeException ile "A price cannot be negative." diyor ve assertion yalnız gerçekten bizim hatamız olabilecek durumu bekliyor. Kural kısa: kodun bozulmasını yakalamak için assertion, kullanıcının ya da başka bir sistemin yanlış verisini yakalamak için guard ve exception. SOLID (beş temel tasarım ilkesinin baş harflerinden oluşan kısaltma) ve Design Principles serisinin "Daha Az Bilinen Prensipler" yazısındaki fail fast bölümü bu ayrımın genişletilmiş hâli.

IDE Desteği ve Bütün Adımlar

IDE sütunu Microsoft Learn, Visual Studio Code ve JetBrains belgelerine dayanıyor; IDE'leri bu yazı için çalıştırmadım. IDE0066 ve dotnet format satırı yukarıdaki gerçek çıktılardan.

RefactoringSmellIDE / analyzerTersi
Decompose ConditionalLong FunctionExtract Method (Visual Studio, Rider)Inline Function
Consolidate Conditional ExpressionDuplicated Codekısmen: Merge with outer if statement (Visual Studio)yok
Replace Nested Conditional with Guard ClausesLong FunctionInvert if (Visual Studio, Visual Studio Code)yok
Replace Control Flag with BreakMutable Dataelleyok
Replace Conditional with PolymorphismRepeated Switcheselle; öncesinde IDE0066 (dotnet format ile)yok
Introduce Special CaseDuplicated Codeelle; nullable uyarıları (CS8625) yerleri listeleryok
Introduce Assertionyok (varsayımı görünür kılmak)elleyok
$ git log --oneline
fc506d6 chore: samples/assert.cs (Debug vs Release assertion)
11970a0 fix: CartLine rejects negative prices
b6e725d refactor: introduce assertion (discount within 0..subtotal)
988435d fix: guests no longer collect loyalty points (Customer.EarnPoints)
97fbd79 refactor: introduce special case Customer.CreateGuest instead of null
abf022b refactor: replace the payment switches with IPaymentOption classes
98f0ed7 refactor: expression-bodied switches, explicit types
9107b59 refactor: switch statements to switch expressions (dotnet format, IDE0066)
d398dba refactor: replace the search loop with Any
8addeb3 refactor: replace control flag tooMany with return
5ce072e refactor: replace nested conditional with guard clauses in CheckoutError
00bf99c refactor: consolidate the no-discount checks into IsEligibleForDiscount
789f5c1 refactor: decompose the campaign condition in Discount
9ca78a1 test: characterize CheckoutPolicy (legacy)

Katalogda tersi olmayan refactoring'lerin çoğu bu ailede. Koşulu adı olan bir fonksiyona çıkarmanın tersi var (Inline Function), ama guard clause'u tekrar iç içe if'e çevirmeyi isteyen bir durum pek yok.

Sık Düşülen Tuzaklar

Special case'in sessizce yanlış veri vermesi. null bir şey yapamaz, special case nesnesi ise her şeyi yapabilir: puan biriktirir, raporda "guest" adlı bir müşteri olarak görünür, Id'si 0 olan bir kayıt olarak sayılır. null kontrolünü kaldırırken kontrolün koruduğu yan etkiyi de bul; kuralı nesnenin kendisine taşı ve onu mümkünse immutable yap.

Assertion'ı input validation için kullanmak. Debug.Assert Release build'de yok; kullanıcıdan, veritabanından ya da başka bir servisten gelen değeri onunla kontrol etmek, üretimde hiç kontrol etmemek demek. Sınırda guard ve exception kullan.

Guard clause'a çevirirken koşulu yanlış ters çevirmek. || ve && karışık bir koşulu elle ters çevirmek klasik bir hata kaynağı. IDE'nin "Invert if" refactoring'ini kullan ya da koşulu önce adı olan bir fonksiyona çıkar, sonra yalnız ! ekle.

Her switch'i polymorphism'e çevirmek. Tek bir yerde duran, kolları kısa bir switch expression çoğu zaman en okunur hâl. Polymorphism'in bedeli fazladan class'lar ve kodun birkaç dosyaya dağılması; aynı switch birden fazla yerde tekrar etmiyorsa bu bedeli ödeme.

Consolidate ederken yan etkileri birleştirmek. Ayrı ayrı if'lerin biri log yazıyor ya da bir metrik artırıyorsa, onları tek bir || zincirine almak hangi kontrolün çalıştığını değiştirir; kısa devre yüzünden bazıları hiç çalışmaz.

Küçük Bir Deneme

★☆☆ Kendi projende .editorconfig'e dotnet_diagnostic.IDE0066.severity = warning ekle ve build al. Kaç switch statement switch expression'a çevrilebiliyor? dotnet format style --diagnostics IDE0066'nın ürettiği koda bak: ekip kurallarına uymayan bir şey bıraktı mı?

★★☆ Codebase'te aynı nesne için null kontrolünün en çok tekrar ettiği yeri bul. Special case nesnesini tasarlamadan önce her kontrolün iki şeyi yapıp yapmadığına bak: bir değer mi döndürüyor, yoksa bir yan etkiyi mi engelliyor?

★★★ Projendeki Debug.Assert çağrılarını ara. Her biri için şu soruyu sor: bu koşul kodun kendi hatası olmadan bozulabilir mi? Bozulabiliyorsa onu sınırda bir guard'a çevir ve Release build'de bir test ile doğrula.

Koşullar da sadeleşti. Katalogda iki aile kaldı. Bir sonraki yazı bir class'ın dışarıya açtığı API'yi refactor eden on iki refactoring'le devam ediyor: Separate Query from Modifier, Remove Flag Argument, Parameterize Function, factory function ve command'larla ilgili dönüşümler, ve hata kodu ile exception arasındaki iki yönlü geçiş.