Ders 15 / 16
Yeniden Düzenleme Teknikleri
Ayıklama, taşıma ve yeniden adlandırma tekniklerinin ücret kitaplığına ara adımlarıyla uygulanması; her adımdan sonra test koşumunun alınması ve adımların kapsam, dosya ve ad geçiş sayısıyla ölçülmesi.
İçindekiler
Önceki ders genelleştirmeyi atıp gelen gereksinimi doğrudan karşıladı. Kalan dosya
çalışıyor ama üç kusuru var: bölge katsayısı tablosu tarifeye ait bilgiyi ücret dosyasında
tutuyor, dönem hesabındaki eşik karşılaştırması adsız duruyor ve g adı bir gönderiyi
temsil ettiğini söylemiyor. Bu ders üç kusuru üç mekanik teknikle giderir — ayıklama
(extract), taşıma (move) ve yeniden adlandırma (rename) — her tekniği ara
adımlarıyla uygular ve her adımdan sonra koşum çıktısını gösterir.
Test odaklı geliştirme konusunda kırmızı–yeşil–yeniden düzenle döngüsü kurulmuştu: orada testler tasarımı sürüklüyor, üçüncü aşama düzenlemeye ayrılıyordu. Burada konu döngü değil, o üçüncü aşamada uygulanan tekniklerin kendisi. Bu derste tek bir test yazılmayacak, tek bir test beklentisi değişmeyecek ve hiçbir koşum kırmızıya dönmeyecek; testler yalnız davranışın korunduğunu bildiren ölçü aracıdır.
Başlangıç noktası önceki dersin bitiş durumudur.
// tarife.mjs — kademe bilgisinin tek kaynagi export const KADEME = [ { ustGram: 1000, kurus: 4990, ad: 'kucuk' }, { ustGram: 5000, kurus: 7490, ad: 'orta' }, { ustGram: 10000, kurus: 9900, ad: 'buyuk' }, { ustGram: 20000, kurus: 12900, ad: 'cok-buyuk' }, { ustGram: Infinity, kurus: 24900, ad: 'agir' }, ]; export function kademe(gram) { return KADEME.find((k) => gram <= k.ustGram); }
// ucret.mjs — onceki dersin bitis hali import { kademe } from './tarife.mjs'; const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 }; const ASGARI_KURUS = 6990; const INDIRIM_ESIGI = 50; const INDIRIM_ORANI = 0.88; export function etkinAgirlik(g) { return Math.max(g.gram, Math.ceil((g.en * g.boy * g.yukseklik) / 3)); } export function gonderiUcreti(g) { const bolgeli = Math.round(kademe(etkinAgirlik(g)).kurus * BOLGE_KATSAYI[g.bolge]); return Math.max(bolgeli, ASGARI_KURUS); } export function donemUcreti(gonderiler) { const toplam = gonderiler.reduce((t, g) => t + gonderiUcreti(g), 0); if (gonderiler.length <= INDIRIM_ESIGI) return toplam; return Math.round(toplam * INDIRIM_ORANI); }
// ucret.test.mjs — onceki dersten devralinan alti test import { test } from 'node:test'; import assert from 'node:assert/strict'; import { gonderiUcreti, donemUcreti } from './ucret.mjs'; const kutu = { en: 20, boy: 15, yukseklik: 10 }; const donem = (adet) => Array.from({ length: adet }, () => ({ ...kutu, gram: 3000, bolge: 'B1' })); test('kademe ucreti agirliktan okunur', () => { assert.equal(gonderiUcreti({ ...kutu, gram: 3000, bolge: 'B1' }), 7490); }); test('bolge katsayisi taban ucreti carpar', () => { assert.equal(gonderiUcreti({ ...kutu, gram: 3000, bolge: 'B3' }), 11984); }); test('asgari ucret bolge carpimindan sonra uygulanir', () => { assert.equal(gonderiUcreti({ ...kutu, gram: 500, bolge: 'B2' }), 6990); }); test('hacim agirligi gercek agirliktan buyukse o kullanilir', () => { assert.equal(gonderiUcreti({ en: 40, boy: 30, yukseklik: 30, gram: 500, bolge: 'B2' }), 16125); }); test('esigi asan donemde butun gonderiler indirimli toplanir', () => { assert.equal(donemUcreti(donem(60)), Math.round(60 * 7490 * 0.88)); }); test('esigin altindaki donemde indirim uygulanmaz', () => { assert.equal(donemUcreti(donem(10)), 10 * 7490); });
node --test --test-reporter=tap ucret.test.mjs | grep -E '^ *(ok|# (tests|pass|fail))'
ok 1 - kademe ucreti agirliktan okunur ok 2 - bolge katsayisi taban ucreti carpar ok 3 - asgari ucret bolge carpimindan sonra uygulanir ok 4 - hacim agirligi gercek agirliktan buyukse o kullanilir ok 5 - esigi asan donemde butun gonderiler indirimli toplanir ok 6 - esigin altindaki donemde indirim uygulanmaz # tests 6 # pass 6 # fail 0
Üç Tekniğin Ortak Biçimi
Üç teknik de aynı üç parçadan oluşur. Önkoşul: düzenlemenin davranışı değiştirmediğini gösterecek bir koşum vardır ve yeşildir. Ara durum: eski ile yeni bir süre birlikte durur; bu aralıkta kod çalışır, testler yeşildir, ama yapı iki yerlidir. Kaldırma: eskisi silinir ve ara durum kapanır.
Ara durumun varlığı tekniklerin can alıcı yanıdır. Bir düzenleme tek hamlede yapıldığında, koşum kırmızıya döndüğünde hatanın yeni yapıda mı yoksa çağrı yerlerinin geçirilmesinde mi olduğu ayırt edilemez. Adımlara bölündüğünde her koşum tek bir soruyu yanıtlar.
Ayıklama
Dönem hesabındaki eşik karşılaştırması adsız duruyor: dönem uzunluğunu INDIRIM_ESIGI
ile karşılaştıran satır bir kuralı uyguluyor ama kuralın adını söylemiyor. Ayıklama, bir
ifadeyi ya da deyim öbeğini kendi adı olan bir birime çıkarmaktır.
Birinci adım yeni fonksiyonu yazar ve hiçbir çağrı yerine dokunmaz.
// ucret.mjs — ayiklama adim 1: yeni fonksiyon yazildi, cagri yeri degismedi import { kademe } from './tarife.mjs'; const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 }; const ASGARI_KURUS = 6990; const INDIRIM_ESIGI = 50; const INDIRIM_ORANI = 0.88; export function etkinAgirlik(g) { return Math.max(g.gram, Math.ceil((g.en * g.boy * g.yukseklik) / 3)); } export function indirimOrani(adet) { return adet > INDIRIM_ESIGI ? INDIRIM_ORANI : 1; } export function gonderiUcreti(g) { const bolgeli = Math.round(kademe(etkinAgirlik(g)).kurus * BOLGE_KATSAYI[g.bolge]); return Math.max(bolgeli, ASGARI_KURUS); } export function donemUcreti(gonderiler) { const toplam = gonderiler.reduce((t, g) => t + gonderiUcreti(g), 0); if (gonderiler.length <= INDIRIM_ESIGI) return toplam; return Math.round(toplam * INDIRIM_ORANI); }
node --test --experimental-test-coverage --test-reporter=tap ucret.test.mjs 2>&1 \ | grep -E '^# ((tests|pass|fail) |file |ucret\.mjs)'
# tests 6 # pass 6 # fail 0 # file | line % | branch % | funcs % | uncovered lines # ucret.mjs | 92.31 | 100.00 | 80.00 | 14-15
Koşum yeşil, ama ara durumun bedeli kapsamda görünüyor: satır kapsamı yüzde 100’den 92,31’e, işlev kapsamı yüzde 80’e düştü ve 14–15. satırlar kapsanmayan olarak listelendi. Yeni fonksiyon henüz çağrılmıyor. Bu, ayıklamanın yarıda bırakıldığında bıraktığı izdir: kimsenin kullanmadığı bir isim.
İkinci adım çağrı yerini geçirir ve eski ifadeyi siler.
// ucret.mjs — ayiklama adim 2: cagri yeri yeni fonksiyona gecirildi import { kademe } from './tarife.mjs'; const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 }; const ASGARI_KURUS = 6990; const INDIRIM_ESIGI = 50; const INDIRIM_ORANI = 0.88; export function etkinAgirlik(g) { return Math.max(g.gram, Math.ceil((g.en * g.boy * g.yukseklik) / 3)); } export function indirimOrani(adet) { return adet > INDIRIM_ESIGI ? INDIRIM_ORANI : 1; } export function gonderiUcreti(g) { const bolgeli = Math.round(kademe(etkinAgirlik(g)).kurus * BOLGE_KATSAYI[g.bolge]); return Math.max(bolgeli, ASGARI_KURUS); } export function donemUcreti(gonderiler) { const toplam = gonderiler.reduce((t, g) => t + gonderiUcreti(g), 0); return Math.round(toplam * indirimOrani(gonderiler.length)); }
node --test --experimental-test-coverage --test-reporter=tap ucret.test.mjs 2>&1 \ | grep -E '^# ((tests|pass|fail) |file |ucret\.mjs)'
# tests 6 # pass 6 # fail 0 # file | line % | branch % | funcs % | uncovered lines # ucret.mjs | 100.00 | 100.00 | 100.00 |
Kapsam geri geldi. Dönem fonksiyonunun gövdesi dört satırdan üç satıra indi ve indirim kuralı artık kendi adıyla ayrıca sınanabilir durumda. Ayıklamanın ölçülebilir kazancı budur: sınanabilir birim sayısı arttı, davranış aynı kaldı.
Taşıma
Bölge katsayısı tablosu ücret dosyasında duruyor ama tarifeye ait bir bilgi: ağırlık kademeleri gibi ticari bir karar ve aynı merci tarafından değiştiriliyor. Taşıma üç adımda yapılır, çünkü ithal ilişkisi iki dosyayı birden ilgilendirir.
Birinci adım hedef modülde yeni işlevi oluşturur; kaynak dosyaya dokunulmaz.
// tarife.mjs — tasima adim 1: islev hedef modulde olusturuldu export const KADEME = [ { ustGram: 1000, kurus: 4990, ad: 'kucuk' }, { ustGram: 5000, kurus: 7490, ad: 'orta' }, { ustGram: 10000, kurus: 9900, ad: 'buyuk' }, { ustGram: 20000, kurus: 12900, ad: 'cok-buyuk' }, { ustGram: Infinity, kurus: 24900, ad: 'agir' }, ]; const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 }; export function kademe(gram) { return KADEME.find((k) => gram <= k.ustGram); } export function bolgeKatsayisi(bolge) { return BOLGE_KATSAYI[bolge]; }
node --test --test-reporter=tap ucret.test.mjs | grep -E '^# (tests|pass|fail)' printf 'katsayi tablosu tarife.mjs: %s ucret.mjs: %s\n' \ "$(grep -c 'B2: 1.25' tarife.mjs)" "$(grep -c 'B2: 1.25' ucret.mjs)"
# tests 6 # pass 6 # fail 0 katsayi tablosu tarife.mjs: 1 ucret.mjs: 1
Tablo şu anda iki dosyada. Bu, birinci dersteki bilgi yinelemesinin ta kendisi ve bilerek üretilen geçici bir durumdur; taşımanın sonraki iki adımı bu yinelemeyi kapatmak içindir.
İkinci adım çağrı yerini yeni işleve geçirir. Eski sabit dosyada kalır, artık kullanılmaz.
// ucret.mjs — tasima adim 2: cagri yeri tarifedeki isleve gecirildi import { kademe, bolgeKatsayisi } from './tarife.mjs'; const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 }; const ASGARI_KURUS = 6990; const INDIRIM_ESIGI = 50; const INDIRIM_ORANI = 0.88; export function etkinAgirlik(g) { return Math.max(g.gram, Math.ceil((g.en * g.boy * g.yukseklik) / 3)); } export function indirimOrani(adet) { return adet > INDIRIM_ESIGI ? INDIRIM_ORANI : 1; } export function gonderiUcreti(g) { const bolgeli = Math.round(kademe(etkinAgirlik(g)).kurus * bolgeKatsayisi(g.bolge)); return Math.max(bolgeli, ASGARI_KURUS); } export function donemUcreti(gonderiler) { const toplam = gonderiler.reduce((t, g) => t + gonderiUcreti(g), 0); return Math.round(toplam * indirimOrani(gonderiler.length)); }
node --test --test-reporter=tap ucret.test.mjs | grep -E '^# (tests|pass|fail)' printf 'ucret.mjs icinde BOLGE_KATSAYI gecisi: %s\n' "$(grep -c BOLGE_KATSAYI ucret.mjs)"
# tests 6 # pass 6 # fail 0 ucret.mjs icinde BOLGE_KATSAYI gecisi: 1
Geçiş sayısının bire inmesi kaldırma adımının önkoşuludur: ad yalnız kendi tanımında geçiyorsa hiçbir yerde okunmuyor demektir. Üçüncü adım tanımı siler.
grep -v 'BOLGE_KATSAYI' ucret.mjs > gecici.mjs && mv gecici.mjs ucret.mjs node --test --test-reporter=tap ucret.test.mjs | grep -E '^# (tests|pass|fail)' printf 'katsayi tablosu tarife.mjs: %s ucret.mjs: %s\n' \ "$(grep -c 'B2: 1.25' tarife.mjs)" "$(grep -c 'B2: 1.25' ucret.mjs)"
# tests 6 # pass 6 # fail 0 katsayi tablosu tarife.mjs: 1 ucret.mjs: 0
Katsayı tablosu artık tek dosyada. Taşımanın ölçüsü satır sayısı değil, bir bilginin bulunduğu dosya sayısıdır: ikiden bire indi ve tarife kararlarını değiştiren kişi bundan sonra tek dosyaya bakacak.
Yeniden Adlandırma
Taşıma yeni bir kusur üretti. Tarife modülünde şimdi iki arama işlevi var: biri ağırlığa
göre, diğeri bölgeye göre. kademe adı, modülde tek arama varken yeterliydi; ikincisi
gelince hangi boyutta arama yaptığını söylemez oldu. Yeniden adlandırmanın gerekçesi çoğu
zaman böyle doğar — ad kötüleşmez, adın içinde durduğu bağlam değişir.
Dışa verilen bir ad, onu ithal eden her dosyayı ilgilendirir. Bu yüzden adlandırma da üç adımdır ve birinci adımda eski ad yaşatılır.
// tarife.mjs — ad adim 1: yeni ad eskisinin yanina eklendi export const KADEME = [ { ustGram: 1000, kurus: 4990, ad: 'kucuk' }, { ustGram: 5000, kurus: 7490, ad: 'orta' }, { ustGram: 10000, kurus: 9900, ad: 'buyuk' }, { ustGram: 20000, kurus: 12900, ad: 'cok-buyuk' }, { ustGram: Infinity, kurus: 24900, ad: 'agir' }, ]; const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 }; export function agirlikKademesi(gram) { return KADEME.find((k) => gram <= k.ustGram); } export const kademe = agirlikKademesi; export function bolgeKatsayisi(bolge) { return BOLGE_KATSAYI[bolge]; }
node --test --test-reporter=tap ucret.test.mjs | grep -E '^# (tests|pass|fail)' printf 'eski ad gecisi: %s\n' "$(cat tarife.mjs ucret.mjs | grep -o 'kademe' | wc -l | tr -d ' ')"
# tests 6 # pass 6 # fail 0 eski ad gecisi: 3
İkinci adım çağrı yerlerini yeni ada geçirir. Aynı adımda ikinci bir yeniden adlandırma
yapılıyor: g parametresi gonderi oluyor. İki adlandırmanın aynı adıma sığması bir
gevşeklik değil, kapsam farkının sonucudur; g adının kapsamı tek fonksiyondur, dışarıdan
kimse onu görmez, dolayısıyla ara adıma ihtiyaç duymaz.
// ucret.mjs — ad adim 2: cagri yeri yeni ada gecirildi import { agirlikKademesi, bolgeKatsayisi } from './tarife.mjs'; const ASGARI_KURUS = 6990; const INDIRIM_ESIGI = 50; const INDIRIM_ORANI = 0.88; export function etkinAgirlik(gonderi) { const hacim = Math.ceil((gonderi.en * gonderi.boy * gonderi.yukseklik) / 3); return Math.max(gonderi.gram, hacim); } export function indirimOrani(adet) { return adet > INDIRIM_ESIGI ? INDIRIM_ORANI : 1; } export function gonderiUcreti(gonderi) { const taban = agirlikKademesi(etkinAgirlik(gonderi)).kurus; return Math.max(Math.round(taban * bolgeKatsayisi(gonderi.bolge)), ASGARI_KURUS); } export function donemUcreti(gonderiler) { const toplam = gonderiler.reduce((t, g) => t + gonderiUcreti(g), 0); return Math.round(toplam * indirimOrani(gonderiler.length)); }
node --test --test-reporter=tap ucret.test.mjs | grep -E '^# (tests|pass|fail)' printf 'eski ad gecisi: %s\n' "$(cat tarife.mjs ucret.mjs | grep -o 'kademe' | wc -l | tr -d ' ')"
# tests 6 # pass 6 # fail 0 eski ad gecisi: 1
Üçte birden bire indi; kalan tek geçiş, eski adı yaşatan bağdır. Üçüncü adım onu siler.
grep -v 'export const kademe = agirlikKademesi;' tarife.mjs > gecici.mjs mv gecici.mjs tarife.mjs node --test --test-reporter=tap ucret.test.mjs | grep -E '^ *(ok|# (tests|pass|fail))' printf 'eski ad gecisi: %s\n' "$(cat tarife.mjs ucret.mjs | grep -o 'kademe' | wc -l | tr -d ' ')"
ok 1 - kademe ucreti agirliktan okunur ok 2 - bolge katsayisi taban ucreti carpar ok 3 - asgari ucret bolge carpimindan sonra uygulanir ok 4 - hacim agirligi gercek agirliktan buyukse o kullanilir ok 5 - esigi asan donemde butun gonderiler indirimli toplanir ok 6 - esigin altindaki donemde indirim uygulanmaz # tests 6 # pass 6 # fail 0 eski ad gecisi: 0
Test adlarında “kademe” sözcüğü duruyor ve durması gerekiyor: orada geçen şey bir kod adı değil, alanın sözcüğü. Yeniden adlandırma sembolü değiştirir, alanın sözlüğünü değiştirmez.
Adımların Bilançosu
| Adım | Teknik | Koşum | Ölçülen |
|---|---|---|---|
| 1 | ayıklama | yeşil | işlev kapsamı %100 → %80 |
| 2 | ayıklama | yeşil | işlev kapsamı %80 → %100 |
| 3 | taşıma | yeşil | katsayı tablosu 1 dosya → 2 dosya |
| 4 | taşıma | yeşil | eski sabitin geçiş sayısı 2 → 1 |
| 5 | taşıma | yeşil | katsayı tablosu 2 dosya → 1 dosya |
| 6 | adlandırma | yeşil | eski adın geçişi 3 |
| 7 | adlandırma | yeşil | eski adın geçişi 3 → 1 |
| 8 | adlandırma | yeşil | eski adın geçişi 1 → 0 |
Sekiz adımın sekizinde koşum yeşil bitti ve test dosyası hiç değişmedi: altı test, altı beklenti, sıfır düzenleme. Ölçülerin ikisi bir adımda geriye gitti — üçüncü adımda tablo iki dosyaya çıktı, birinci adımda kapsam düştü. İkisi de ara durumun izidir ve bir sonraki adımda kapandı. Yeniden düzenlemenin adım adım yapılmasının anlamı burada görünür: ara durumlar kaçınılmazdır, tek çare onları kısa tutmaktır.
Özet
- Ayıklama, taşıma ve yeniden adlandırma aynı üç parçadan oluşur: yeşil bir önkoşul koşumu, eski ile yeninin birlikte durduğu ara durum ve kaldırma adımı.
- Ayıklamanın ara durumu kapsamda görünür; çağrı yeri geçirilmeden önce işlev kapsamı yüzde 80’e, satır kapsamı yüzde 92,31’e düştü ve iki satır kapsanmayan olarak listelendi.
- Taşımanın ara durumu bilerek üretilmiş bir bilgi yinelemesidir; katsayı tablosu bir dosyadan ikiye çıktı, çağrı yeri geçirildikten sonra bire indi.
- Kaldırma adımının önkoşulu ölçülebilir: eski adın geçiş sayısı bire indiyse ad yalnız kendi tanımında duruyor demektir.
- Dışa verilen adlar üç adımda, kapsamı tek fonksiyon olan adlar tek adımda değiştirilir; fark adın kapsamından gelir.
- Sekiz adımın sekizi yeşil bitti ve altı testin hiçbiri değişmedi; teknikler davranışı değil yalnız yapıyı değiştirdi.
Sonraki Adım
Bu derste her adımın ardından alınan yeşil koşum, davranışın korunduğunun kanıtı sayıldı. Bu sayışın kendisi sınanmadı: altı test, ücret hesabının bütün davranışını gerçekten kapsıyor mu? Sonraki ders testsiz yapılan bir ayıklama adımında sessizce değişen bir davranışı gösterir, aynı adımın mevcut altı testle de yakalanmadığını ölçer, testlerin yakalama gücünü sayıya döker ve güçlendirilmiş kümenin aynı adımı kırmızıya çevirdiği anı gösterir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.