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

Uygulamalı Clean Code: Legacy OrderService'i Adım Adım Temizlemek

Mert Özen 7 Kas 2026 14 dk 1 görüntülenme
Uygulamalı Clean Code: Legacy OrderService'i Adım Adım Temizlemek

Legacy kodu temizlemenin güvenli yolu önce küçük bir seam ve characterization test'lerle mevcut davranışı sabitlemek, sonra her adımı ayrı commit'te yapmak ve davranış değişikliklerini ayrı fix commit'lerine ayırmaktır. Yazıda 609 satırlık OrderService 16 commit'te, her commit'te testler yeşilken temizleniyor; en yüksek Cognitive Complexity 92'den 10'a iniyor, bilinçli bırakılan smell'ler de gerekçesiyle listeleniyor.

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

Önce Tahmin Et: İlk Commit Ne Olmalı?

Seri boyunca parça parça baktığımız legacy OrderService.cs önünde duruyor: 609 satır, ProcessData tek başına 127 satır ve Cognitive Complexity değeri 92, hiç test yok. Ürün ekibi yeni kupon tipleri istiyor ve kimse dosyaya dokunmaya cesaret edemiyor. Bir günün var. İlk commit'in hangisi olur?

  • Önce isimleri düzeltmek, çünkü d, tmp2, k okumayı imkânsız kılıyor.
  • Önce ProcessData'yı küçük method'lara bölmek, çünkü en büyük risk orada.
  • Önce test yazmak, ama DateTime.Now'a bağlı bir hafta sonu indirimi var ve sonuçlar güne göre değişiyor.

Bu yazıda dosyayı baştan sona temizledim: 16 commit; testlerin eklendiği üçüncü commit'ten itibaren her commit'te bütün testler koştu ve geçti. Aşağıdaki çıktılar o koşulardan. Cevabın üçüncü şık olduğunu tahmin etmişsindir, ama ilginç kısım "test yazılamıyorsa ne yaparsın?" sorusu.

Strateji: Önce Güvenlik Ağı, Sonra Temizlik

Sıra plan gereği şu: önce characterization test ile güvenlik ağı, sonra naming, fonksiyon bölme, error handling ve class ayrıştırma. Her adım ayrı commit ve iki kural hiç esnemedi:

Refactoring commit'i davranışı değiştirmez. Testler aynı değerleri, aynı mesajları assert etmeye devam eder; değişen yalnızca çağrılan API'nin (Application Programming Interface) şekli olabilir.

Davranış değişikliği ayrı bir fix: commit'idir ve testteki beklenen değeri bilinçli olarak değiştirir. Böylece git log'a bakan biri hangi commit'in ürünün davranışını değiştirdiğini tek satırdan görür. Commit mesajları Conventional Commits biçiminde; formatın kendisini Git serisinin "Git ile Ekip Çalışması: Branch Stratejileri, Rulesets ve Conventional Commits" yazısında anlattık.

Ortaya çıkan geçmiş:

39cbf8e chore: import legacy OrderService
9873de4 refactor: inject TimeProvider instead of DateTime.Now (seam for tests)
adb9544 test: add characterization tests for OrderService
b1be16c refactor: remove dead and unused code
217d695 fix: compare customer type and coupon codes with invariant culture
12140ca fix: parse imported prices with invariant culture
147bf65 refactor: rename to intention-revealing names and drop redundant comments
7810c9d refactor: replace magic numbers and type strings with enums and constants
9eb89ff test: pin that rejected carts consume an order id
e1060a5 refactor: flatten PlaceOrder with guard clauses and extract methods
3c3d015 fix: quote uses the same pricing as placed orders
120231a fix: let unexpected payment exceptions propagate
9bc2cd0 fix: GetOrders returns an empty list instead of null
372968a refactor!: return Result<T> instead of error codes and LastError
4de6003 refactor: split OrderService into focused classes, one type per file
276eb4b fix: rejected carts no longer consume an order id

Adım 0: Seam ve Characterization Test

Characterization test terimi Michael Feathers'ın Working Effectively with Legacy Code (2004) kitabından geliyor: kodun ne yapması gerektiğini değil, şu anda ne yaptığını sabitleyen test. Doğru olup olmadığına karar vermezsin; mevcut davranışı yazıya geçirirsin, bug'ları da dahil.

Burada ilk engel zaman. Hafta sonu indirimi DateTime.Now'a bakıyor; bugün cumartesiyse test bir sonuç, çarşamba ise başka bir sonuç görür. Testsiz bir kodu test edebilmek için önce küçük bir değişiklik gerekiyor ve bu değişikliğin kendisi testsiz yapılıyor. Feathers bu yere seam diyor: davranışı, kodu düzenlemeden dışarıdan değiştirebildiğin nokta. Seam'i olabildiğince küçük ve mekanik tuttum:

@@ -18,8 +18,15 @@ namespace Bookstore.Legacy
         StringBuilder log = new StringBuilder();
         public bool debug = false;
 
-        public OrderService()
+        private readonly TimeProvider clock;
+
+        public OrderService() : this(TimeProvider.System)
+        {
+        }
+
+        public OrderService(TimeProvider clock)
         {
+            this.clock = clock;
             if (Db.Books.Count == 0) Db.Seed();
         }
 
@@ -97,7 +104,7 @@ namespace Bookstore.Legacy
                             tmp2 += tmp * 0.05m;
                         }
                         if (o.Items.Count > 5) tmp2 += 15;
-                        if (DateTime.Now.DayOfWeek == DayOfWeek.Saturday || DateTime.Now.DayOfWeek == DayOfWeek.Sunday)
+                        if (clock.GetLocalNow().DateTime.DayOfWeek == DayOfWeek.Saturday || clock.GetLocalNow().DateTime.DayOfWeek == DayOfWeek.Sunday)
                             tmp2 += tmp * 0.02m;
                         // kargo
                         decimal k = KargoUcretiHesapla(tmp - tmp2, t, c.Address.City.Name);

Parametresiz constructor duruyor, yani üretimdeki çağıranlar için hiçbir şey değişmedi; TimeProvider.System zaten DateTime.Now ile aynı yerel zamanı veriyor. Testler ise Microsoft.Extensions.TimeProvider.Testing paketindeki FakeTimeProvider'ı veriyor. TimeProvider'ın kendisini C# Async serisinin "C# Async ve .NET 8: TimeProvider, ConfigureAwaitOptions, Parallel.ForAsync ve CancelAsync" yazısında tanıttık.

Sonra testler. Feathers'ın önerdiği yöntem basit: assert'e bilerek yanlış bir değer yaz, testi çalıştır, hata mesajındaki gerçek değeri kopyala. Ben aynı işi, sabit saatli küçük bir console programıyla gerçek değerleri yazdırarak yaptım; sonunda 33 test çıktı. Birkaçı:

[Fact]
public void Vip_customer_on_a_weekday_gets_five_percent_and_free_shipping()
{
    int id = Service.ProcessData(1, Cart((Dune, 1), (BriefHistory, 1)), null, false, true, 1);

    Order order = Service.GetOrder(id);
    Assert.Equal(1000, id);
    Assert.Equal(33.75m, order.Discount);
    Assert.Equal(0m, order.Shipping);
    Assert.Equal(705.38m, order.Total);
    Assert.Equal(1, order.Status);
    Assert.Equal("TX0001", order.PaymentRef);
}

[Theory]
[InlineData(99, Emma, 1, null, -7, "Customer not found")]
[InlineData(1, "123", 1, null, -2, "Book not found: 123")]
[InlineData(1, Emma, 4, null, -3, "Not enough stock: Emma")]
[InlineData(2, Emma, 1, "FREE", -4, "Invalid coupon")]
[InlineData(3, Emma, 1, null, -5, "Payment failed: Do not honor")]
public void Failures_return_negative_codes_and_set_last_error(
    int customerId, string isbn, int qty, string? coupon, int expectedCode, string expectedError)
{
    int result = Service.ProcessData(customerId, Cart((isbn, qty)), coupon, false, false, 1);

    Assert.Equal(expectedCode, result);
    Assert.Equal(expectedError, OrderService.LastError);
}

[Fact]
public void Under_turkish_culture_the_vip_discount_is_lost()
{
    CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("tr-TR");

    int id = Service.ProcessData(1, Cart((Dune, 1), (BriefHistory, 1)), null, false, false, 1);

    Assert.Equal(0m, Service.GetOrder(id).Discount);
    Assert.Equal(742.50m, Service.GetOrder(id).Total);
}

Son teste dikkat et. Türkçe kültürde VIP (Very Important Person) indiriminin kaybolması bir bug, ama characterization test onu da sabitliyor, çünkü amaç şu anki davranışı bilmek. Testin adı da bunu açıkça söylüyor.

Static state yüzünden test base'i her testten önce bütün dünyayı sıfırlamak ve paralel çalışmayı kapatmak zorunda kaldı:

[assembly: CollectionBehavior(DisableTestParallelization = true)]

protected LegacyTestBase()
{
    CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-US");
    Db.Reset();
    Mailer.Sent.Clear();
    OrderService.LastError = null;
    OrderService.cnt = 0;
    Clock = new FakeTimeProvider(Wednesday);
    Service = new OrderService(Clock);
}

Kapsam ölçümü için xUnit template'inde gelen coverlet collector yeterli:

dotnet test --collect:"XPlat Code Coverage"
Passed!  - Failed:     0, Passed:    33, Skipped:     0, Total:    33, Duration: 30 ms - Bookstore.Tests.dll (net10.0)
<coverage line-rate="0.7961" branch-rate="0.7037"

33 test satırların yüzde 79,6'sını, dalların yüzde 70,4'ünü çalıştırıyor. Hiç çalışmayan method'lar IsValid, CheckStock, TryCalc, CalcPoints, UpdateStatus, OldCalcDiscount, SendMail, GetFromCache ve ClearAll. Bu liste bir sonraki adımın girdisi oldu: solution'da çağıranı olmayan ve testle de korunmayan kod, refactor edilmez, silinir. İlk temizlik commit'i OrderService.cs'ten 112 satır sildi; testlerde yalnız artık var olmayan cnt sayacını sıfırlayan satır gitti.

Küçük bir yan not: faturanın tarih satırını önce "Date: 11/4/2026 10:00:00 AM" diye yazdım ve test kırmızı oldu, iki string ekranda aynı görünmesine rağmen. .NET'in macOS'ta kullandığı ICU (International Components for Unicode) verisi en-US saat biçiminde AM (ante meridiem) ibaresinden önce normal boşluk yerine dar bir bölünmez boşluk (U+202F) koyuyor. Beklenen değeri aynı DateTime'ı biçimlendirerek ürettim.

Davranış Değişikliği Ayrı Commit'te

Güvenlik ağı kurulunca bilinen iki bug'ı düzelttim, ama refactoring'e karıştırmadan. Türkçe I düzeltmesi tek satırlık bir fark:

@@ -95,7 +95,7 @@ namespace Bookstore.Legacy
                                 return -4;
                             }
                         }
-                        if (c.Type.ToUpper() == "VIP")
+                        if (c.Type.ToUpperInvariant() == "VIP")
                         {
                             tmp2 += tmp * 0.05m;
                         }

Testleri koşturunca ağ beklendiği gibi yakaladı:

  Failed Bookstore.Tests.Characterization.ProcessDataTests.Under_turkish_culture_the_vip_discount_is_lost [< 1 ms]
Expected: 0
Actual:   33,750
Failed!  - Failed:     1, Passed:    32, Skipped:     0, Total:    33, Duration: 25 ms - Bookstore.Tests.dll (net10.0)

Beklenen değer 0'dan 33,75'e döndü ve testin adı Turkish_culture_keeps_the_vip_discount oldu. Aynı şekilde import'taki fiyatların kültüre göre parse edilmesi (Türkçe makinede 520.50 fiyatı 52050 olarak okunuyordu) ayrı bir commit'te düzeldi. Bu iki commit'i önce yapmamın nedeni şu: birazdan string Type'ı enum'a çevireceğim ve o refactoring Türkçe I bug'ını sessizce yok edecekti. Bug'ın düzeldiğini gösteren tek kayıt, adı "refactor" olan bir commit'in içinde kaybolacaktı.

Naming ve Fonksiyonlar

İsim değişikliği commit'i saf mekanik: ProcessData → PlaceOrder, KargoUcretiHesapla → CalculateShipping, d → cart, tmp2 → discount, Qty → Quantity. Changelog comment'leri ve kodu tekrar eden comment'ler aynı commit'te gitti. Testlerde yalnızca çağrılan method adları değişti; git diff ile kontrol ettim, tek bir beklenen değer satırı değişmedi. Ardından magic number'lar OrderStatus, ShippingMethod, BookFormat, CustomerTier enum'larına ve PricingRules sabitlerine dönüştü.

Fonksiyonları bölmeden önce bir kör nokta fark ettim: ProcessData sipariş numarasını kitapları kontrol etmeden önce alıyordu, yani reddedilen bir sepet de bir numara harcıyordu. Hiçbir test bunu görmüyordu ve fonksiyonu yeniden düzenlerken bu davranışı farkında olmadan değiştirebilirdim. Önce bunu sabitleyen bir test commit'i ekledim, sonra PlaceOrder'ı guard clause'larla düzleştirdim:

public int PlaceOrder(int customerId, Dictionary<string, int> cart, string couponCode, bool giftWrap, bool sendConfirmation, ShippingMethod shippingMethod)
{
    LastError = null;
    Customer customer = Db.Customers.FirstOrDefault(c => c.Id == customerId);
    if (customer == null) return Fail(-7, "Customer not found");
    if (cart == null || cart.Count == 0) return Fail(-1, "Empty cart");
    if (customer.IsBlocked) return Fail(-6, "Customer blocked");

    Order order = StartOrder(customer);
    foreach (var (isbn, quantity) in cart)
    {
        Book book = Db.Books.FirstOrDefault(b => b.Isbn == isbn);
        if (book == null) return Fail(-2, "Book not found: " + isbn);
        if (book.Stock < quantity) return Fail(-3, "Not enough stock: " + book.Title);
        order.Items.Add(new OrderItem { Book = book, Quantity = quantity, Price = UnitPrice(book) });
    }
    if (!IsValidCoupon(couponCode)) return Fail(-4, "Invalid coupon");

    decimal subtotal = order.Items.Sum(item => item.Price * item.Quantity);
    order.Discount = CalculateDiscount(subtotal, customer, couponCode, order.Items.Count);
    order.Shipping = CalculateShipping(subtotal - order.Discount, shippingMethod, customer.Address.City.Name)
        + (giftWrap ? PricingRules.GiftWrapFee : 0);
    order.Total = Math.Round((subtotal - order.Discount) * (1 + PricingRules.TaxRate) + order.Shipping, 2);

    int paymentError = ChargeCustomer(order);
    if (paymentError != 0) return paymentError;

    CompleteOrder(order);
    if (sendConfirmation) SendConfirmation(order);
    return order.Id;
}

Dört seviyelik if piramidi gitti, her guard tek satır. Cognitive Complexity 88'den 10'a indi. Hâlâ dönüş kodları ve static LastError var; error handling sırası henüz gelmedi. Bir şey bilerek aynı: StartOrder numarayı hâlâ doğrulamadan önce alıyor, çünkü test öyle olduğunu söylüyor.

Fiyatlama tek bir Price method'una toplanınca GetQuote da onu kullanmaya başladı ve sepet sayfasındaki fiyat ödemedeki fiyata eşitlendi. Bu da bir davranış değişikliği, yani ayrı bir fix: commit'i: test 731,50 yerine 705,38 bekliyor.

Error Handling ve Class Ayrıştırma

Error handling commit'inden önce iki küçük fix: daha geldi: ödeme SDK'sından (Software Development Kit) gelen beklenmeyen exception artık yutulup -99'a çevrilmiyor, GetOrders de null yerine boş liste dönüyor. Sonra dönüş kodları ve LastError, error handling yazısındaki Result<T> ve tipli Error record'larıyla değişti. Bu commit refactor!: ile işaretli, çünkü public API kırılıyor ve CancelOrder'daki iki flag argument kalkıyor: iptal artık her zaman iade ve bildirim demek. Testlerde negatif sayılar error tiplerine dönüştü, mesajlar ise birebir aynı kaldı: "Book not found: 123" hâlâ assert ediliyor.

Son büyük adım class ayrıştırma. OrderService 462 satırlık tek class'tan, constructor'dan beş bağımlılık alan ve dosyası 115 satır olan bir class'a dönüştü:

public sealed class OrderService(
    BookstoreStore store,
    OrderPricing pricing,
    IPaymentGateway payments,
    IOrderNotifier notifier,
    TimeProvider clock)
{
    public Result<Order> PlaceOrder(PlaceOrderRequest request)
    {
        Customer? customer = store.FindCustomer(request.CustomerId);
        if (customer is null) return new CustomerNotFound(request.CustomerId);
        if (request.Cart.Count == 0) return new EmptyCart();
        if (customer.IsBlocked) return new CustomerBlocked(customer.Id);

        Result<IReadOnlyList<OrderItem>> items = PriceItems(request.Cart);
        if (!items.IsSuccess) return items.Error!;
        if (!OrderPricing.IsKnownCoupon(request.CouponCode)) return new InvalidCoupon(request.CouponCode!);

        Order order = new()
        {
            Id = store.NextOrderId(),
            Customer = customer,
            Date = clock.GetLocalNow().DateTime,
            Items = items.Value,
            Price = pricing.Price(items.Value, customer, request.CouponCode, request.ShippingMethod, request.GiftWrap),
        };
        return Charge(order, request.SendConfirmation);
    }

    // ... GetQuote, CancelOrder, ShipOrder ve PriceItems burada

    private Result<Order> Charge(Order order, bool sendConfirmation)
    {
        PaymentOutcome outcome = payments.Charge(order.Customer.CardToken, order.Total);
        if (outcome is PaymentOutcome.Declined declined)
        {
            order.Status = OrderStatus.PaymentFailed;
            store.Add(order);
            return new PaymentDeclined(declined.Reason);
        }

        order.PaymentRef = ((PaymentOutcome.Approved)outcome).TransactionId;
        foreach (OrderItem item in order.Items) item.Book.Stock -= item.Quantity;
        order.Status = OrderStatus.Paid;
        store.Add(order);
        order.Customer.EarnPointsFor(order);
        if (sendConfirmation) notifier.OrderPlaced(order);
        return order;
    }
}

Fiyatlama OrderPricing'e, ödeme SDK'sı boundary'ler yazısındaki gibi IPaymentGateway arkasına (orada interface async'ti; legacy kod senkron olduğu için bu adımda senkron bıraktım), e-posta IOrderNotifier arkasına, fatura InvoiceFormatter'a, rapor SalesReport'a, müşteri kaydı CustomerRegistry'ye, CSV (Comma-Separated Values) import'u BookCsvImporter'a taşındı. Static Db ve Mailer instance oldu ve test base'i artık bir composition root gibi duruyor:

protected LegacyTestBase()
{
    CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-US");
    Clock = new FakeTimeProvider(Wednesday);
    Store = BookstoreStore.CreateSeeded();
    Mailer = new Mailer();
    Service = new OrderService(
        Store,
        new OrderPricing(Clock),
        new VendorPaymentGateway(new PaymentClient("merchant-001", 30)),
        new EmailOrderNotifier(Mailer),
        Clock);
    Customers = new CustomerRegistry(Store);
    Importer = new BookCsvImporter(Store);
    Invoices = new InvoiceFormatter(Clock);
    Reports = new SalesReport(Store);
}

DisableTestParallelization satırı silindi; testler artık birbirinin static state'ine dokunmadığı için paralel koşuyor. Global Data kokusunun en somut bedeli buydu. #nullable disable da kalktı; yalnız Vendor/PaymentClient.cs'te duruyor, çünkü o dosya third-party kodu taklit ediyor ve onu değiştirmiyoruz. Son commit, en başta sabitlediğim sipariş numarası davranışını düzeltti: numara artık sepet doğrulandıktan sonra alınıyor.

Önce ve Sonra: Metrikler

14-before-after-tr.png

Her commit'i ayrı bir worktree'de checkout edip testleri ve aynı metrik derlemesini (SonarAnalyzer'ın S3776 (Cognitive Complexity) ve S138 (method uzunluğu) kuralları eşik düşürülmüş hâlde, bir de CA1502) yeniden çalıştırdım:

CommitAdımTestEn uzun methodEn yüksek Cognitive
39cbf8elegacy kodyok12792
9873de4TimeProvider seamyok12792
adb9544characterization test'ler33/3312792
b1be16cölü kod silindi33/3312488
217d695fix: Türkçe I33/3312488
12140cafix: import kültürü33/3312488
147bf65naming33/3312488
7810c9denum ve sabitler33/3312488
9eb89fftest: sipariş numarası34/3412488
e1060a5guard clause, extract34/342713
3c3d015fix: teklif fiyatı35/352711
120231afix: exception yutma36/362711
9bc2cd0fix: boş liste36/362711
372968aResult<T>36/362810
4de6003class ayrıştırma36/362210
276eb4bfix: sipariş numarası36/362210

Metriklerin tamamı aynı ölçümle alındı ve vendor SDK dosyası iki tarafta da dahil. Toplam Cognitive Complexity 182'den 59'a, en yüksek cyclomatic complexity 27'den 7'ye indi. Line coverage, characterization test'lerden sonraki yüzde 79,6'dan yüzde 94,6'ya çıktı; bunun büyük kısmı yeni testten değil, hiç çalışmayan kodun silinmesinden geliyor. Bir sayı ise arttı: toplam satır 609'dan 678'e çıktı ve tek dosya 28 dosyaya bölündü. Simple design yazısının dördüncü kuralını hatırla: amaç element sayısını azaltmak değil, okuyucunun bir seferde aklında tutması gerekeni azaltmak. Fiyatlama kuralını değiştirecek kişi artık 54 satırlık OrderPricing'i okuyor, 609 satırı değil.

Tablonun en önemli sütunu ise ortadaki: hiçbir satırda kırmızı test yok. En büyük metrik sıçraması (e1060a5'te 88'den 13'e) testlerin tek bir beklenen değeri değişmeden geldi.

Bilinçli Bırakılan Smell'ler

Temizlik bitmedi, durduruldu. Bunlar bilerek kalanlar ve nedenleri:

  • ShipOrder(orderId, bool express) hâlâ bir flag argument alıyor. Doğrusu siparişin kargo tipini saklaması, ama bu bir veri modeli değişikliği ve migration ister; ayrı bir iş.
  • OrderService.ShipOrder etiketi doğrudan File.WriteAllText ile diske yazıyor. Etiket metni ShippingLabel'a çıktı, yazıcı entegrasyonu gelene kadar dosya yeterli.
  • İptal edilen VIP siparişinde 50 bonus puan geri alınmıyor. Bu bir bug olabilir ya da bir iş kararı; test bunu sabitliyor ve Customer.ReturnPointsFor'daki comment iş biriminin cevabını bekliyor.
  • PricingRules'taki oranlar ve "Istanbul", "Turkey" string'leri kodda sabit. Kampanyaları bir panelden yönetme isteği gelmeden configuration'a taşımak, simple design yazısındaki spekülatif genellik olurdu.
  • Order.Status servis tarafından doğrudan değiştiriliyor. Geçişleri bir state machine'e almak Çözüm Desenleri serisinin "Status Alanı Her Yerden Güncelleniyorsa" yazısının konusu.
  • Para hâlâ çıplak decimal. Money value object'i Çözüm Desenleri serisinin "Para ve Yuvarlama" yazısında; burada yuvarlama kuralları değişmediği için ertelendi.

Temizliği nerede durduracağını bilmek de bir karar. Her kalan smell, değişme ihtimali ve maliyetiyle birlikte not edildi; kod bir sonraki değişiklikte yine bu sırayla ele alınabilir.

Sık Düşülen Tuzaklar

Test yazmadan refactor etmeye başlamak. ProcessData'yı düzleştirirken sipariş numarası davranışını değiştirmek çok kolaydı ve hiçbir derleyici uyarısı bunu söylemezdi.

Bug'ı refactoring commit'inde sessizce düzeltmek. Enum dönüşümü Türkçe I bug'ını kendiliğinden yok edecekti. Davranış değişikliği ayrı commit'te ve değişen testle birlikte gelmezse, bir yıl sonra kimse neyin neden değiştiğini bilemez.

Characterization test'i "doğru davranış" testi sanmak. Bu testler bugünkü davranışı kaydeder. Bir bug'ı sabitleyen testin adını bunu söyleyecek şekilde koy, sonra bilinçli olarak değiştir.

Hepsini tek seferde yapmak. 16 commit gereksiz görünebilir, ama git bisect ile bir regresyonu ararken her commit'in tek bir şey yapması bütün farkı yaratır; aracın kendisini Git serisinin "Git bisect ve git worktree: Hatayı Bulan Commit ve Aynı Anda İki Branch" yazısında anlattık.

Metrikleri hedef yapmak. Cognitive Complexity'yi düşürmek için BookCsvImporter.Import'u (Cognitive 10, şu an en yüksek) beş method'a bölmek mümkün. Ama okunaklı, düz bir if/else if zinciri; Ousterhout tartışmasındaki entangled method'ları üretmenin kolay bir yolu olurdu.

Küçük Bir Deneme

★☆☆ LEGACY_ORDERSERVICE.cs'i bir projeye koy, seam'i ekle ve açılıştaki üç characterization test'ini kendin yaz. Feathers'ın yöntemini kullan: önce yanlış bir değer assert et, gerçek değeri hata mesajından al.

★★☆ Bilinçli bırakılan smell'lerden ShipOrder'daki flag'i kaldır: Order'a ShippingMethod ekle, etiketin express olup olmadığını oradan oku. Hangi characterization test'in beklentisi değişmeli ve bu commit refactor: mu olmalı, fix: mi?

★★★ CancelOrder'daki VIP bonus puanı davranışını değiştirmek istediğini varsay. Önce mevcut davranışı sabitleyen testi bul, sonra yeni davranışı yazan bir fix: commit'i hazırla ve commit mesajında iş kararına referans ver.

Bu yazıyla Clean Code serisi bitiyor. On dört yazı boyunca naming'den error handling'e, class'lardan test koduna, smell kataloğundan kuralların eleştirisine kadar aynı dosyayı temizledik. Kuralları ezberlemek yerine her birinin hangi maliyeti düşürdüğünü sormak, serinin asıl önerisi. İlerideki SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation ve Dependency Inversion prensipleri) ve Design Principles serisinde, burada class seviyesinde kalan coupling ve cohesion sorularını prensip seviyesinde ele alacağız.