İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat