Testsiz refactor aslında rewrite'tır
“Refactor ediyorum” yazılımda en çok esnetilen cümlelerden biri. Bir değişkenin adını değiştirmeye de deniyor, biri ortalığı toparlarken bir hafta boyunca ayağa kalkmayan bir servise de.
Bu konunun kitabını yazan Martin Fowler ise kelimeyi daha net ifade ediyor. Ona göre refactoring, yazılımın “gözlemlenebilir davranışını değiştirmeden, iç yapısında onu daha kolay anlaşılır ve daha ucuza değiştirilebilir kılmak için yapılan değişiklik.”
Bence işin can alıcı kısmı o tanımın içinde saklı: davranışı değiştirmeden. Bu da insanların sormayı ıskaladığı bir soruyu getiriyor: bunu nereden biliyorsun?
Hiçbir şeyin değişmediğini nereden biliyorsun?
Diff’i okuyabilirsin ya da uygulamayı kullanarak bir şeyler deneyebilirsin (artık PR diff’lerini okumuyoruz da çoğu zaman, code yazan da AI agent, review yapan da :) Değişiklik birkaç satırı geçince ikisi de yetmiyor.
Cevabın “testler” ise haklısın ama bu basit bir cevap, altını doldurmak gerekiyor. Testler kodun bugün ne yaptığını sabitliyor ve bir adım onu yerinden oynattığında saniyeler içinde haber veriyor. Test yoksa “davranış değişmedi” lafı bir fact değil aslında, temenni. Güvenli olduğunu ancak umut edebildiğin bir değişiklik de, ticket’ta adı ne yazarsa yazsın, bir rewrite’ın riskini taşıyor.
Fowler’ın bunun için tek cümlelik bir testi var: “Biri sistemin refactor yüzünden birkaç gündür bozuk olduğunu söylüyorsa, emin olabilirsin ki refactor yapmıyordur.”
Fowler testsiz olanına rewrite değil restructuring der. Benim kuralım daha katı: davranışın yerinde durduğunu gösteremiyorsam, o değişikliği bir rewrite’a göstereceğim özenle planlarım.
Küçük adımlar, her biri kontrollü
Fowler’ın tarif ettiği refactoring, adı konmuş küçük hamlelerden oluşan bir zincir: Rename Variable, Extract Function, Move Function. Her hamleden sonra testleri koşuyorsun.
Rename Variable, Extract Function, Move Function aslında bunlar eskiden alışık olmaya çalıştığımız ve AI’ın olmadığı dönemde uygulamaya çalışırken kafa patlattığımız konulardı. Ancak şu an Claude ve Codex ile de geliştirme yaparken bir şekilde onu da bu yolda yönlendirmemiz gerekebiliyor. Doğrudan çok gelişmiş modeller aslında bu konuları bilerek de geliyorlar ve testler geçene kadar kodu çalıştırıp düzeltecek şekilde eğitilmiş olmaları1 OpenAI, Codex’in arkasındaki codex-1 modelini gerçek kodlama görevlerinde reinforcement learning ile eğittiğini ve modelin “testler geçene kadar tekrar tekrar koştuğunu” yazıyor. Anthropic’in Claude Code rehberi de aynı döngüyü anlatıyor: Claude işi yapıyor, kontrolü koşuyor, sonucu okuyor ve kontrol geçene kadar devam ediyor. aslında bunu doğal akışta yapmalarını sağlıyor, ama yine de kendi yolumuzu agent’lara vermeye özen göstermemiz gerekiyor.
Aşağıdaki anahtarı kapatıp aynı beş adımın testli ve testsiz hâline bir bak.
Testler varken bozuk adım hep son attığın adım oluyor, neyi geri alacağını biliyorsun. Test yoksa beşi birden yayına çıkıyor. Bug geldiğinde soru “az önce ne yaptım?” olmaktan çıkıyor, “Ekim’de ne yapmıştık?” oluyor.
Legacy kod kapanı
Michael Feathers Working Effectively with Legacy Code’da bunu çok net söylüyor: legacy kod, testi olmayan koddur. Kodun yaşının bununla hiçbir ilgisi yok. Geçen hafta yazılmış ama testi olmayan kod da legacy.
Bunun bir de kapanı var. Kodu güvenle değiştirmek için teste ihtiyacın var. Test yazabilmek için de çoğu zaman önce kodu değiştirmen gerekiyor: constructor’ında veritabanına bağlanan bir struct ya da saati kendisi okuyan bir fonksiyon mesela. Feathers’ın çıkış yolu bir döngü ve asıl yapmak istediğin değişiklik o döngünün son adımı.
3. adım küçük ve dikkatli değişikliklerin yeri: saati parametre olarak içeri ver, veritabanını bir interface’in arkasına çek. Feathers, kodu tam orada düzenlemeden davranışını değiştirebildiğin bu noktalara seam diyor.
4. adımda en sevdiğim numara var. Kodun ne yapması gerektiğini test etmiyorsun, şu an ne yaptığını kaydediyorsun. Feathers buna characterization test diyor. Yanlış olduğunu bildiğin bir assertion yaz, testi koş, gerçek cevabı hata mesajından oku. Sonra o gerçek cevabı teste koy.
// Characterization test: Price'ın ne yapması gerektiğini değil, bugün ne yaptığını kaydediyor.
// Bugünkü cevap yanlış görünse bile sabitle, düzeltmesini ayrı bir değişiklikte yap.
func TestPrice_CurrentBehaviour(t *testing.T) {
for cents, want := range map[int]string{
0: "0.00",
199: "1.99",
-5: "-0.05",
100000: "1,000.00",
} {
if got := Price(cents); got != want {
t.Errorf("Price(%d) = %q, want %q", cents, got, want)
}
}
}
Artık refactor’ın altında bir ağ var.
Refactor mı, rewrite mı?
Gerçekten rewrite olduğunda
Rewrite günah değil. Bazen eski kodun gitmesi gerekiyor. Sadece farklı bir plan istiyor ve o planın ilk adımında adını çok daha net bir çerçevede tanımlamak ve daha yüksek sesle koymak gerekiyor.
Adı konmuş bir rewrite’ı ona göre planlayabilirsin: eskiyle yeniyi yan yana çalıştırıp döndürdükleri sonuçları karşılaştır, trafiği bir flag’in arkasından yavaş yavaş kaydır, geri dönüş yolunu açık tut. Fowler’ın strangler fig dediği şey bunun sabırlı hâli: yeni kodu eskinin etrafında parça parça büyütüyorsun, sonunda eski kısmı kapatabilecek hâle geliyorsun.
Bir dahaki sefere biri “refactor ediyorum” dediğinde şu soruyu sor: hiçbir şeyin değişmediğini nereden bileceksin? Cevap testlerse, bırak küçük adımlarla istediği kadar refactor etsin. Cevap “biraz tıklayıp bakarım”sa, o aslında adı güzelleştirilmiş bir rewrite, rewrite gibi de planlanmayı hak ediyor.