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

Refactoring Nedir, Ne Değildir? Davranışı Korumak, İki Şapka ve Rewrite Kararı

Mert Özen 14 Kas 2026 12 dk 2 görüntülenme
Refactoring Nedir, Ne Değildir? Davranışı Korumak, İki Şapka ve Rewrite Kararı

Refactoring, gözlemlenebilir davranışı değiştirmeden iç yapıyı küçük ve güvenli adımlarla değiştirmektir; davranışı ölçen bir test yoksa yapılan şey refactoring değil, umutla yapılan bir değişikliktir. Yazıda "no behavior change" başlıklı bir PR'ın faturayı bir kuruş değiştirmesi, iki şapka ve ayrı commit disiplini, ne zaman refactor edileceği ve rewrite kararının ölçütleri gerçek çıktılarla anlatılıyor.

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

Önce Tahmin Et: "No Behavior Change" Yazan PR

Bookstore'un fiyat modülünde, sipariş satırlarının toplamını hesaplayan küçük bir class var. Alan bir öğleden sonra bu class'a bakıyor, foreach döngüsünü fazla uzun buluyor ve LINQ (.NET'in koleksiyonlar üzerinde sorgu yazma syntax'ı) ile sadeleştiriyor. PR (pull request; değişikliğin incelenip ana branch'e alınması için açılan istek) başlığı net: davranış değişmiyor. Reviewer'ın önüne gelen diff şu:

-    public static decimal Calculate(IEnumerable<OrderLine> lines)
-    {
-        decimal total = 0m;
-        foreach (OrderLine line in lines)
-        {
-            decimal lineTotal = line.UnitPrice * line.Quantity * (1 - line.DiscountRate);
-            total += Math.Round(lineTotal, 2);
-        }
-        return total;
-    }
+    public static decimal Calculate(IEnumerable<OrderLine> lines) =>
+        Math.Round(lines.Sum(LineTotal), 2);
+
+    private static decimal LineTotal(OrderLine line) =>
+        line.UnitPrice * line.Quantity * (1 - line.DiscountRate);

Repo'daki tek test, Dune'u yüzde 10 indirimle ve iki adet Emma'yı alan bir siparişin 765 tuttuğunu doğruluyor. CI yeşil:

Test run summary: Passed!
  total: 1
  failed: 0
  succeeded: 1
  skipped: 0
  duration: 473ms

Kod daha kısa, test geçiyor, başlık "no behavior change" diyor. Sen reviewer olsan onaylar mısın? Onaylamadan önce bir soru: 39,90'lık ve 49,90'lık iki kitaba yüzde 15 indirim uygulanan bir sipariş, değişiklikten önce ve sonra aynı toplamı mı verir?

Refactoring'in Tanımı: Davranış Kimin Gözünden?

Martin Fowler'ın refactoring.com'daki tanımı kısa: "Refactoring is a disciplined technique for restructuring an existing body of code, altering its internal structure without changing its external behavior." Cümlenin iki ağırlık merkezi var. Birincisi "disciplined": refactoring bir niyet değil, bir yöntem. Kod, her biri küçük ve davranışı koruyan adımlarla değişir; her adımdan sonra sistem çalışır durumdadır. İkincisi "external behavior": değişen şey yalnız iç yapı, dışarıdan görünen her şey aynı kalır.

"Dışarıdan görünen" ifadesini somutlaştırmak gerekiyor, çünkü tartışmaların çoğu burada çıkıyor. Bir method'un gözlemlenebilir davranışı, onu çağıran kodun ve sistemin kullanıcısının fark edebileceği her şeydir: dönen değer, fırlatılan exception tipi, veritabanına yazılan satırlar, gönderilen e-posta, log'a düşen kayıt, bir API'nin JSON çıktısındaki alan adları. Bir kuruşluk yuvarlama farkı da bu listeye girer; müşterinin faturasında görünüyorsa davranıştır.

Listeye genelde girmeyenler de var. Private bir method'un adı, bir class'ın hangi dosyada durduğu, döngünün foreach mi LINQ mi olduğu dışarıdan görünmez. Performans arada kalır: bir refactoring sonrasında kod yüzde 2 yavaşladıysa çoğu zaman bunu davranış değişikliği saymayız, ama bir endpoint'in yanıt süresi bir SLA'ya (müşteriye taahhüt edilen hizmet seviyesi) bağlıysa sayarız. Bu yüzden "davranış değişti mi?" sorusunun cevabı her zaman bir bağlama göredir: kimin, neyi gözlemlediği.

Clean Code serisinin "Code Smell Kataloğu" yazısında kokuların nerede olduğunu, "Uygulamalı Clean Code: Legacy OrderService'i Adım Adım Temizlemek" yazısında da tek bir class'ın commit commit temizlenmesini gördük. Bu seri ise refactoring'in kendisini bir disiplin olarak ele alıyor: güvenlik ağı, adım büyüklüğü, legacy kodda dependency kırmak, büyük değişiklikleri planlamak ve bütün bunları ekibe anlatmak.

Cevap: Bir Kuruşluk Davranış Değişikliği

Açılıştaki soruyu iki sürümü yan yana çalıştırarak cevaplayalım. Aynı iki satırı, eski ve yeni hesapla topluyoruz:

OrderLine[] lines =
[
    new("Dune", 39.90m, 1, 0.15m),
    new("Emma", 49.90m, 1, 0.15m)
];

foreach (OrderLine line in lines)
{
    decimal raw = line.UnitPrice * line.Quantity * (1 - line.DiscountRate);
    Console.WriteLine($"{line.Title,-6} raw={raw}  printed={Math.Round(raw, 2)}");
}

Console.WriteLine($"Before refactor: {Before(lines)}");
Console.WriteLine($"After refactor:  {After(lines)}");
Dune   raw=33.9150  printed=33.92
Emma   raw=42.4150  printed=42.42
Before refactor: 76.34
After refactor:  76.33

Eski kod her satırı ayrı yuvarlayıp topluyordu: 33,92 + 42,42 = 76,34. Faturada da satırlar bu şekilde basılıyor. Yeni kod önce ham değerleri topluyor (76,33) ve sonra yuvarlıyor. Sonuç bir kuruş eksik ve artık faturadaki satırların toplamı, faturanın altındaki toplamla tutmuyor. Muhasebe bunu ay sonunda, binlerce siparişte biriken farkla fark eder.

Değişiklik kötü niyetli değil; Math.Round'un döngünün içinde mi dışında mı olduğu, kodu sadeleştirirken kolayca gözden kaçan bir ayrıntı. Asıl sorun iki yerde. Birincisi, mevcut test bu davranışı hiç sınamıyordu: 450 × 0,90 ve 180 × 2 yuvarlama gerektirmeyen sayılar. İkincisi, PR başlığı reviewer'ın dikkatini kapattı. "No behavior change" yazan bir diff'e insan daha az şüpheyle bakar.

Davranışı sabitleyen testi eklediğimizde, refactor edilmiş kod hemen yakalanıyor:

[Fact]
public void Calculate_rounds_each_line_like_the_invoice()
{
    OrderLine[] lines =
    [
        new("Dune", 39.90m, 1, 0.15m),
        new("Emma", 49.90m, 1, 0.15m)
    ];

    Assert.Equal(76.34m, OrderTotals.Calculate(lines));
}
failed Bookstore.Pricing.Tests.OrderTotalsTests.Calculate_rounds_each_line_like_the_invoice (14ms)
  Assert.Equal() Failure: Values differ
  Expected: 76.34
  Actual:   76.33

Ders şu: refactoring'in güvenliği, kodu değiştirenin dikkatinden değil, davranışı ölçen testlerden gelir. Test yoksa ya da test önemli davranışı sınamıyorsa yapılan şey refactoring değil, umutla yapılan bir değişikliktir. İkinci yazının bütün konusu bu güvenlik ağını kurmak.

"Refactoring" Kelimesi Neden Kayıyor?

Ekiplerde "refactoring" kelimesi zamanla "kod üzerinde yapılan, yeni özellik olmayan her iş" anlamına kayıyor. Framework sürümünü yükseltmek, bir modülü baştan yazmak, veritabanı şemasını değiştirmek, hatta iki hafta boyunca build'in kırık kaldığı bir yeniden yapılanma da "refactoring sprint'i" diye anılıyor. Fowler 2004'te "Refactoring Malapropism" yazısında bu kaymayı not etmişti: sistem birkaç gün çalışmaz durumdaysa yapılan iş refactoring değil, daha genel bir restructuring'dir. Refactoring, restructuring'in özel bir yolu: küçük, davranışı koruyan adımlar ve her adımın sonunda çalışan bir sistem.

Kelimenin kayması üç somut zarar doğuruyor. Birincisi review'da: "refactor" etiketi reviewer'a "davranışa bakmana gerek yok" mesajı verir, açılıştaki PR'da olduğu gibi. İkincisi planlamada: Product Owner "refactoring" kelimesini "iş değeri olmayan, geliştiricilerin kendi keyfi için yaptığı iş" diye duymaya başlar ve haklı olarak sorgular. Oysa iyi bir refactoring çoğu zaman bir sonraki özelliğin maliyetini düşürmek için yapılır; dokuzuncu yazıda bunu ölçülebilir bir dille anlatmayı konuşacağız. Üçüncüsü riskte: iki haftalık bir yeniden yazım "refactoring" diye sunulunca, küçük adımlarla ilerlemenin sağladığı güvenceler varmış gibi davranılır ama yoktur.

Pratik bir öneri: PR başlığında ve commit mesajında "refactor" kelimesini yalnız davranış gerçekten değişmiyorsa kullan. Davranış değişiyorsa ya da emin değilsen, ne yaptığını adıyla söyle: "upgrade", "rewrite", "fix", "change rounding rule".

İki Şapka: Aynı Anda Hem Özellik Hem Refactoring Yok

Kent Beck'in, Fowler'ın Refactoring kitabıyla yaygınlaşan bir metaforu var: programlarken iki şapkadan birini takarsın. Refactoring şapkası takılıyken her değişiklik davranışı korur, testler hep yeşil kalır, yeni test yazmazsın. Özellik şapkası takılıyken davranış değiştirirsin: yeni test eklersin, bazı testler bilerek kırmızıya döner. Şapkalar sık sık, hatta birkaç dakikada bir değişebilir; önemli olan aynı anda ikisini takmamak.

Neden bu kadar önemli? Çünkü iki şapka karışınca bir test kırmızıya döndüğünde nedenini bilemezsin. Yeni özellik mi beklenen şekilde davranışı değiştirdi, yoksa yan tarafta yaptığın "küçük temizlik" mi bir şeyi bozdu? Açılıştaki PR'da Alan yalnız refactoring şapkasını taktığını sanıyordu; aslında davranış da değişti ve bunu söyleyen bir test olmadığı için kimse fark etmedi.

Aynı işi şapkaları ayırarak yapalım. Bookstore'a yeni bir kural geliyor: beş ve üzeri kitaplık siparişlerde yüzde 5 toplu alım indirimi. Önce davranışı sabitleyen test, sonra refactoring, en son özellik; her biri ayrı commit:

$ git log --oneline --stat fbd742a..HEAD
e932f1c feat: 5% bulk discount from five books
 src/Bookstore.Pricing/OrderTotals.cs              | 15 +++++++++++++--
 tests/Bookstore.Pricing.Tests/OrderTotalsTests.cs |  8 ++++++++
 2 files changed, 21 insertions(+), 2 deletions(-)
5e22257 refactor: extract OrderTotals.LineTotal
 src/Bookstore.Pricing/OrderTotals.cs | 16 ++++++----------
 1 file changed, 6 insertions(+), 10 deletions(-)
ad13c13 test: pin per-line rounding of OrderTotals
 tests/Bookstore.Pricing.Tests/OrderTotalsTests.cs | 12 ++++++++++++
 1 file changed, 12 insertions(+)

İlk commit yalnız test ekliyor ve eski kod üzerinde yeşil. İkinci commit yalnız üretim kodunu değiştiriyor, test dosyasına dokunmuyor; aynı testler yeşil kaldığı için davranışın korunduğunu biliyoruz. Bu kez yuvarlama doğru yerde, satır hesabının içinde:

public static decimal Calculate(IEnumerable<OrderLine> lines) =>
    lines.Sum(LineTotal);

// Fatura her satırı ayrı yuvarlıyor; toplam da aynı kuralı izlemeli.
private static decimal LineTotal(OrderLine line) =>
    Math.Round(line.UnitPrice * line.Quantity * (1 - line.DiscountRate), 2);

Üçüncü commit davranışı değiştiriyor ve bunu açıkça söylüyor: yeni test, yeni kod. Reviewer'ın işi kolaylaşıyor: refactor: commit'inde "test dosyası değişmemiş mi, testler yeşil mi?" diye bakıyor, feat: commit'inde ise iş kuralına odaklanıyor. Commit'leri sonradan toparlaman gerekirse Git serisindeki "Git Interactive Rebase ve Squash: PR Öncesi Geçmişi Toparlamak" yazısındaki adımlar burada da geçerli.

Kent Beck'in 2023 tarihli "Tidy First?" kitabı aynı fikri bir adım öteye taşıyor. Beck, davranışı değiştirmeyen küçük düzenlemelere "tidying" diyor ve yapısal değişikliklerle davranış değişikliklerinin aynı PR'a karıştırılmamasını, mümkünse ayrı PR'larda, en azından ayrı commit'lerde gelmesini öneriyor. Fowler'ın 2015 tarihli "An example of preparatory refactoring" yazısında aktardığı bir Beck sözü de bu sıralamayı özetliyor: "make the change easy, then make the easy change". Önce değişikliği kolaylaştıran yapıyı kur, sonra kolaylaşmış değişikliği yap.

Ne Zaman Refactor Edilir?

Refactoring proje planında ayrı bir satır olarak durmak zorunda değil; en verimli hâli günlük işin içine dağılmış olanı. Fowler 2014 tarihli "Workflows of Refactoring" yazısında bu anları ayrı ayrı adlandırıyor. Bookstore'dan örneklerle üç tanesi gündelik iş için yeterli.

Hazırlık için refactoring (preparatory refactoring). Toplu alım indirimi gelmeden önce OrderTotals'ı satır hesabı ayrı bir method'da olacak şekilde düzenlemek tam olarak buydu. Yeni özellik mevcut yapıya zor sığıyorsa önce yapıyı değiştirirsin; özellik sonra birkaç satırla eklenir.

Anlamak için refactoring (comprehension refactoring). Bir method'u okurken üç kez başa dönüyorsan, anladığın şeyi koda yazmanın yolu refactoring'dir: tmp2 değişkenine anlamlı bir ad vermek, bir koşulu adı olan bir method'a çıkarmak. Anladığın şey kafanda kalırsa bir sonraki okuyucu aynı yolu baştan yürür.

Gördüğün yeri temizlemek (litter-pickup refactoring). Bir bug'ı düzeltirken yanındaki ölü kodu ya da yanlış adlandırılmış bir parametreyi fark edersen düzeltirsin; büyükse not alıp sonraya bırakırsın. Ekipte "kamp yeri kuralı" ya da boy scout rule diye bilinen alışkanlık bu.

Planlı ve uzun soluklu refactoring'ler de var: bir modülün baştan sona dönüştürülmesi, aylara yayılan bir altyapı değişikliği. Bunlar istisna olmalı ve altıncı yazıdaki teknikleri gerektiriyor. Refactor etmemenin de makul olduğu durumlar var: bir daha dokunmayacağın, çalışan ve kimsenin okumadığı kod; yakında silinecek bir modül; ya da yeniden yazmanın gerçekten daha ucuz olduğu nadir durumlar.

Rewrite mı Refactor mı?

Legacy bir modülle karşılaşan her ekip er ya da geç "bunu sıfırdan yazsak daha hızlı olmaz mı?" sorusunu sorar. Joel Spolsky'nin 6 Nisan 2000 tarihli "Things You Should Never Do, Part I" yazısı bu sorunun klasik uyarısıdır. Netscape, tarayıcısını sıfırdan yeniden yazmaya karar vermiş; 4.0 ile 6.0 arasında neredeyse üç yıl geçmiş ve bu sürede pazar payı eriyip gitmişti. Spolsky'nin ana argümanı şu: eski kodun çirkin görünen her köşesi, bir zamanlar bulunup düzeltilmiş bir hatanın izini taşır. Sıfırdan yazınca bu birikmiş bilgiyi de çöpe atarsın.

Büyük yeniden yazımların riskleri birbirine bağlı. Yeni sistem eskisinin yaptığı her şeyi yapana kadar kullanıcıya hiçbir şey teslim etmez. Bu sürede eski sisteme gelen bug fix'ler ve yeni özellikler yeni sisteme de taşınmak zorunda kalır, hedef sürekli kayar. Eski sistemin belgelenmemiş davranışları, örneğin açılıştaki satır bazlı yuvarlama, ancak üretimde biri şikâyet edince ortaya çıkar. Ve ekip genelde ikinci sistemi, birincinin yapmadığı her şeyi de yapacak şekilde tasarlamaya meyleder.

Yine de rewrite'ın savunulabildiği durumlar var. Platform ölmüşse, örneğin .NET Framework üzerindeki bir Web Forms arayüzünü .NET 10'a taşımak sayfaları yeniden yazmadan mümkün değilse. Modül küçükse, davranışı iyi tanımlıysa ve onu tarif eden testler varsa. Eski davranışın kendisi zaten istenmiyorsa, yani iş kuralları da değişecekse. Bu durumlarda bile yeniden yazımı tek parça yapmak yerine parça parça yapmak riskin büyük kısmını alır: eski ve yeni yan yana çalışır, trafik yavaş yavaş yeni tarafa geçer. Microservices serisinin "Monolith'ten Microservice'e: Strangler Fig, Branch by Abstraction ve Distributed Monolith Tuzağı" yazısı bunun servis ölçeğindeki hâlini, bu serinin altıncı yazısı da monolith içindeki hâlini anlatıyor.

Karar için kullanabileceğin basit bir soru: "Eski kodun bugün yaptığı her şeyi bir listede sayabiliyor muyum?" Sayamıyorsan yeniden yazmaya hazır değilsin; önce characterization testleriyle o listeyi çıkarman gerekir. Sayabiliyorsan büyük ihtimalle refactoring de artık korkutucu değildir.

Serinin Yolculuk Haritası

01-legacy-map-tr.png

Bu seri, Fowler'ın kataloğunu sırayla gezmek yerine Bookstore'un legacy parçaları üzerinde ilerliyor. Tekniklerin adları, ihtiyaç duyulduğu yerde geçecek. Haritadaki her parça bir sorun türünü temsil ediyor.

InvoiceRenderer, sipariş faturasının metnini üreten 400 satırlık bir class; testi yok ve çıktısı uzun bir metin. İkinci yazıda ona dokunmadan önce mevcut çıktısını approval testing ile kayda alıp güvenlik ağı kuracağız. PricingEngine ise uzun bir Calculate method'una ve müşteri tipi, kampanya ve kuponun iç içe geçtiği koşullara sahip. Üçüncü yazıda onu IDE refactoring'leriyle küçük adımlarla açacak, dördüncüde koşulları okunur bir tasarıma dönüştüreceğiz.

OrderImportJob, CSV (virgülle ayrılmış metin) dosyası okuyan, static bir SmtpHelper'ı çağıran, DateTime.Now ve new SqlConnection kullanan bir background job. Beşinci yazıda onu test edilebilir hâle getirmek için dependency'lerini kıracağız. LegacyShippingClient, kargo firmasıyla senkron ve static çağrılarla konuşan eski bir client; altıncı yazıda yerine modern bir client'ı Mikado Method ve branch by abstraction ile koyacağız. Yedinci yazıda Book.Price alanının API'de, veritabanında ve Angular modelinde birlikte değişmesini, sekizinci yazıda Angular'daki eski CheckoutComponent'in modernleşmesini, dokuzuncu yazıda da bu parçalardan hangisine önce yatırım yapılacağını hotspot analiziyle seçmeyi konuşacağız.

Sık Düşülen Tuzaklar

Testsiz "refactor" PR'ı. Açılıştaki PR'ın sorunu kodun kendisi değil, değişikliğin hiçbir şey tarafından doğrulanmamasıydı. Davranışı ölçen bir test yoksa önce o test yazılır; yazılamıyorsa, kod test edilemeyecek kadar bağımlıysa, işe beşinci yazıdaki dependency kırma teknikleriyle başlanır.

Refactor commit'ine davranış değişikliği gizlemek. "Hazır buradayken" eklenen küçük bir kural, düzeltilen bir null kontrolü ya da değiştirilen bir varsayılan değer, refactor: başlıklı bir commit'in içinde kaybolur. Sonradan bir hata ararken git log sana yanlış yeri gösterir. Davranış değişikliği ne kadar küçük olursa olsun kendi commit'inde gelir.

Refactoring'i ayrı bir proje fazı yapmak. "Önce özellikleri bitirelim, sonra bir refactoring sprint'i yaparız" planı genelde o sprint'in hiç gelmemesiyle biter. Gelirse de biriken iş büyük ve riskli olur. Küçük ve sürekli yapılan refactoring, nadir ve büyük olandan daha ucuzdur.

Kodu "daha güzel" diye değiştirmek. Refactoring'in bir amacı olmalı: bir sonraki değişikliği kolaylaştırmak ya da kodu anlaşılır kılmak. Kimsenin dokunmadığı, çalışan bir modülü kişisel zevke göre yeniden düzenlemek hem review yükü üretir hem de hiçbir şey kazandırmadan risk taşır.

Rewrite kararını tahmine dayandırmak. "Altı ayda yeniden yazarız" tahmini, eski sistemin yaptığı her şeyin bilindiği varsayımına dayanır. Bu varsayım nadiren doğrudur; kararı vermeden önce davranışı kayda almak hem tahmini gerçekçi kılar hem de çoğu zaman refactoring'in yeterli olduğunu gösterir.

Küçük Bir Deneme

★☆☆ Kendi repo'nda son 50 commit'in mesajlarına bak (git log --oneline -50) ve "refactor" geçenleri işaretle. Her birinin diff'ine göz at: test dosyası değişmiş mi, bir iş kuralı eklenmiş mi? Bu sayı, ekibinde kelimenin ne kadar kaydığını gösterir.

★★☆ Açılıştaki OrderTotals örneğini kendin kur. Önce yalnız ilk testle refactor edilmiş sürümü çalıştır ve yeşil olduğunu gör. Sonra yuvarlama farkını yakalayan ikinci testi ekle. İkinci testi bulmak için hangi girdileri denediğini not et; hangi sayılar yuvarlamayı tetikliyor?

★★★ Ekibinde yakın zamanda "sıfırdan yazalım" diye konuşulan bir modül seç. Bu modülün bugün yaptığı her şeyi bir listeye dök: girdiler, çıktılar, yan etkiler, bilinen tuhaflıklar. Liste kaç maddeye ulaştı ve kaç maddeden emin değilsin? Emin olmadıkların, rewrite'ın gizli maliyeti.

Açılıştaki hatayı yakalayan şey tek bir testti; ama gerçek legacy kodda hangi testi yazman gerektiğini bilmezsin. Bir sonraki yazıda 400 satırlık InvoiceRenderer'a dokunmadan önce onun bugünkü davranışını characterization test ve approval testing ile kayda alacak, sonra bu güvenlik ağının gerçekten koruyup korumadığını kasıtlı mutasyonlarla sınayacağız.