İçeriğe geç
academia.sh

Ders 14 / 16

İhtiyacın Olmayacak

Ücret hesabına bugünden eklenen bir eklenti noktasıyla yapılandırma katmanının karar noktası, dal kapsamı ve çalışmayan yol sayısıyla ölçülmesi; gelen gereksinimin bu yapıya uymadığının başarısız koşumla gösterilmesi ve genelleştirmenin atılması.

İçindekiler

Önceki derste her adım gelmiş bir gereksinime karşılık geldi ve dört kural beş adımda karşılandı. Gelmemiş bir gereksinim için bugünden yazılan yapının ne kadara mal olduğu ölçülmedi. Bu ders ücret hesabına bugünden bir eklenti noktası ile bir yapılandırma katmanı ekler, ikisinin ürettiği karar noktalarını ve hiç çalışmayan yolları sayar, sonra gerçekten gelen gereksinimin bu yapıya uymadığını gösterir.

İlkenin adı ihtiyacın olmayacak (you aren’t gonna need it): bir yetenek, ona duyulan gereksinim ortaya çıkana kadar yazılmaz. İlke bir tahmin yasağıdır — yazılım geliştiricinin gelecekteki gereksinimi doğru tahmin edemeyeceğini değil, tahminin yanlış çıkma maliyetinin ölçülebilir olduğunu söyler.

Başlangıç noktası önceki dersin son adımıdır.

// 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 son adimindan devralindi
import { kademe } from './tarife.mjs';

const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 };
const ASGARI_KURUS = 6990;

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);
}
// ucret.test.mjs — onceki dersten devralinan dort test
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { gonderiUcreti } from './ucret.mjs';

const kutu = { en: 20, boy: 15, yukseklik: 10 };

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);
});

Ölçü için bir sayaç gerekiyor: bir dosyadaki karar noktalarını, yani programın iki ayrı yoldan birini seçtiği yerleri sayan bir betik.

// dal.mjs — verilen dosyalardaki karar noktalarini sayar
import { readFileSync } from 'node:fs';

const ORUNTU = /\bif\b|\bcase\b|\?\?|&&|\|\||\?\.|\?(?![?.])/g;

let toplam = 0;
for (const yol of process.argv.slice(2)) {
  const n = (readFileSync(yol, 'utf8').match(ORUNTU) ?? []).length;
  console.log(`${yol}: ${n} karar noktasi`);
  toplam += n;
}
console.log(`toplam: ${toplam}`);
node --test --test-reporter=tap ucret.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
node dal.mjs ucret.mjs
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
# tests 4
# pass 4
# fail 0
ucret.mjs: 0 karar noktasi
toplam: 0

Bugünden Yazılan İki Esneklik

Tarife kuralları zamanla değişecek. Bu doğru bir gözlem ve yanlış bir gerekçe: değişimin geleceğini bilmek, hangi biçimde geleceğini bilmek anlamına gelmez. Yine de iki yapı bugünden yazılıyor. Birincisi, hesabın her ayrıntısını dışarıdan seçilebilir kılan bir yapılandırma katmanı.

// yapilandirma.mjs — bugunden yazilan yapilandirma katmani
export const VARSAYILAN = {
  hacimUygula: true,
  hacimBolen: 3,
  asgariUygula: true,
  asgariKurus: 6990,
  yuvarlama: 'yakin',
  katsayiKaynagi: 'sabit',
};

export function yapilandir(secenek = {}) {
  return { ...VARSAYILAN, ...secenek };
}

İkincisi, ileride gelecek ücret kurallarının hesaba dışarıdan takılmasını sağlayan bir eklenti noktası (extension point).

// eklenti.mjs — ucret kurallari icin eklenti noktasi
const kurallar = [];

export function kuralEkle(ad, oncelik, uygula) {
  kurallar.push({ ad, oncelik, uygula });
  kurallar.sort((a, b) => a.oncelik - b.oncelik);
}

export function kurallariSifirla() {
  kurallar.length = 0;
}

export function kurallariUygula(kurus, g, yap) {
  let sonuc = kurus;
  for (const k of kurallar) {
    sonuc = k.uygula(sonuc, g, yap);
  }
  return sonuc;
}

Ücret hesabı ikisini de kullanacak biçimde yeniden yazılıyor. Davranış aynı kalıyor: varsayılan yapılandırmayla ve kayıtlı kural olmadan sonuçlar değişmez.

// ucret.mjs — bugunden genellestirilmis surum
import { kademe } from './tarife.mjs';
import { yapilandir } from './yapilandirma.mjs';
import { kurallariUygula } from './eklenti.mjs';

const SABIT_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 };

function katsayi(g, yap) {
  if (yap.katsayiKaynagi === 'dis') return yap.katsayiTablosu?.[g.bolge] ?? 1;
  return SABIT_KATSAYI[g.bolge] ?? 1;
}

function yuvarla(kurus, yap) {
  if (yap.yuvarlama === 'yok') return kurus;
  if (yap.yuvarlama === 'yukari') return Math.ceil(kurus);
  return Math.round(kurus);
}

export function etkinAgirlik(g, yap) {
  if (!yap.hacimUygula) return g.gram;
  return Math.max(g.gram, Math.ceil((g.en * g.boy * g.yukseklik) / yap.hacimBolen));
}

export function gonderiUcreti(g, secenek = {}) {
  const yap = yapilandir(secenek);
  const bolgeli = yuvarla(kademe(etkinAgirlik(g, yap)).kurus * katsayi(g, yap), yap);
  const asgarili = yap.asgariUygula ? Math.max(bolgeli, yap.asgariKurus) : bolgeli;
  return kurallariUygula(asgarili, g, yap);
}

Ödenen Bedelin Ölçülmesi

Testler değişmedi ve yeşil kalıyor. Değişen şey testlerin kapsayabildiği yapının büyüklüğü. Karar noktaları ile birlikte, koşumun gerçekten hangi yolları geçtiğini veren dal kapsamı da ölçülüyor.

node --test --test-reporter=tap ucret.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
node dal.mjs ucret.mjs yapilandirma.mjs eklenti.mjs
node --test --experimental-test-coverage --test-reporter=tap ucret.test.mjs 2>&1 \
  | sed -n '/start of coverage report/,/end of coverage report/p'
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
# tests 4
# pass 4
# fail 0
ucret.mjs: 8 karar noktasi
yapilandirma.mjs: 0 karar noktasi
eklenti.mjs: 0 karar noktasi
toplam: 8
# start of coverage report
# -----------------------------------------------------------------
# file             | line % | branch % | funcs % | uncovered lines
# -----------------------------------------------------------------
# eklenti.mjs      |  63.16 |    66.67 |   33.33 | 5-7 10-11 16-17
# tarife.mjs       | 100.00 |   100.00 |  100.00 | 
# ucret.mjs        | 100.00 |    45.45 |  100.00 | 
# yapilandirma.mjs | 100.00 |   100.00 |  100.00 | 
# -----------------------------------------------------------------
# all files        |  90.41 |    63.16 |   80.00 | 
# -----------------------------------------------------------------
# end of coverage report

Üç sayı okunmaya değer. Karar noktası sayısı sıfırdan sekize çıktı; bunların hiçbiri bir alan kuralından gelmiyor, hepsi bir seçeneğin hangi değerde olduğunu sınıyor. Ücret dosyasının satır kapsamı yüzde 100 ama dal kapsamı yüzde 45,45: her satır çalışıyor, yolların yarısından fazlası çalışmıyor. Eklenti dosyasında kuralEkle ve kurallariSifirla hiç çağrılmadığı için işlev kapsamı yüzde 33,33’te kalıyor ve kapsanmayan satırlar tek tek listeleniyor.

Bu yolların bakımsız kalmasının bir yolu yok: dal kapsamını yüzde 100’e çıkarmak, altı ek karar noktasının her biri için en az bir test daha yazmak demek. Bu testlerin sınayacağı davranışı bugün hiçbir çağrı yeri istemiyor. Yapılandırma katmanı altı alan taşıyor ve çağrı yerlerinin geçtiği seçenek sayısı sıfır; eklenti listesine kayıtlı kural sayısı da sıfır.

Gelen Gereksinim

Sözleşmeli müşteriler için bir kural geliyor: bir fatura döneminde elliden çok gönderisi olan müşterinin o dönemdeki bütün gönderilerine yüzde on iki indirim uygulanacak. Eklenti noktası tam bu tür kurallar için yazılmıştı; kural oraya takılmayı deniyor.

// donem.mjs — donem indirimini eklenti noktasi uzerinden yazma denemesi
import { gonderiUcreti } from './ucret.mjs';
import { kuralEkle, kurallariSifirla } from './eklenti.mjs';

const ESIK = 50;
const ORAN = 0.88;

export function donemUcreti(gonderiler) {
  kurallariSifirla();
  let sayac = 0;
  kuralEkle('donem-indirimi', 10, (kurus) => {
    sayac += 1;
    return sayac > ESIK ? Math.round(kurus * ORAN) : kurus;
  });
  return gonderiler.reduce((t, g) => t + gonderiUcreti(g), 0);
}
// donem.test.mjs — gelen gereksinimin tek testi
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { donemUcreti } from './donem.mjs';

const donem = Array.from({ length: 60 }, () => ({
  en: 20, boy: 15, yukseklik: 10, gram: 3000, bolge: 'B1',
}));

test('esigi asan donemde butun gonderiler indirimli toplanir', () => {
  assert.equal(donemUcreti(donem), Math.round(60 * 7490 * 0.88));
});
node --test --test-reporter=tap donem.test.mjs \
  | grep -E '^ *(ok|not ok|expected:|actual:|# (tests|pass|fail))'
not ok 1 - esigi asan donemde butun gonderiler indirimli toplanir
  expected: 395472
  actual: 440410
# tests 1
# pass 0
# fail 1

Aradaki 44.938 kuruşluk fark, eklenti noktasının yanlış eksende genelleştirdiğini gösteriyor. Kural imzası (kurus, gonderi, yapilandirma) biçimindedir: kural tek bir gönderiyi görür. Gelen gereksinim ise bir gönderinin ücretini kümenin büyüklüğüne göre belirliyor; elli birinci gönderi işlenirken önceki ellisinin ücreti çoktan toplanmış oluyor. Eklenti bunu ancak kümeyi iki kez dolaşarak karşılayabilir, o zaman da toplamayı yapan çağıran taraf işin tamamını üstlenmiş olur; eklenti noktası bir işe yaramaz.

Genelleştirme, gelecekteki değişimin gönderi başına bir kural biçiminde geleceğini varsaymıştı. Gerçekte gelen değişim gönderi başına değil dönem başına. Yapılandırma katmanında da benzeri geçerli: altı alanın hiçbiri indirim eşiğini ya da oranını karşılamıyor.

Genelleştirmeyi Atmak

Atılan yapının büyüklüğü sayılabilir.

printf 'atilan dosyalarin satir sayisi: %s\n' \
  "$(cat yapilandirma.mjs eklenti.mjs donem.mjs | wc -l | tr -d ' ')"
rm yapilandirma.mjs eklenti.mjs donem.mjs donem.test.mjs
atilan dosyalarin satir sayisi: 48

Yerine gelen gereksinim doğrudan karşılanıyor: dönem ücretini hesaplayan bir fonksiyon, tek eşik ve tek oran.

// ucret.mjs — genellestirme atildi, gelen gereksinim dogrudan karsilandi
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 — dort eski test ve donem indiriminin iki testi
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|not ok|# (tests|pass|fail))'
node dal.mjs ucret.mjs
node --test --experimental-test-coverage --test-reporter=tap ucret.test.mjs 2>&1 \
  | sed -n '/start of coverage report/,/end of coverage report/p'
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
ucret.mjs: 1 karar noktasi
toplam: 1
# start of coverage report
# -----------------------------------------------------------
# file       | line % | branch % | funcs % | uncovered lines
# -----------------------------------------------------------
# tarife.mjs | 100.00 |   100.00 |  100.00 | 
# ucret.mjs  | 100.00 |   100.00 |  100.00 | 
# -----------------------------------------------------------
# all files  | 100.00 |   100.00 |  100.00 | 
# -----------------------------------------------------------
# end of coverage report

Tek karar noktası kaldı ve o karar noktası bir alan kuralına karşılık geliyor: dönem eşiğin altında mı, değil mi. Dal kapsamı yüzde 100; kod tabanında koşumun geçmediği yol yok. Ücret dosyası bir modül ithal ediyor, önceki sürüm üçünü ithal ediyordu.

Ölçüt Genelleştirilmiş Doğrudan
Karar noktası 8 1
Ücret dosyasının dal kapsamı %45,45 %100
Bütün dosyaların satır kapsamı %90,41 %100
İthal edilen modül 3 1
Gelen gereksinimi karşılıyor mu hayır evet

İlkenin Ne Yasakladığı

Bu ders ile önceki ders farklı şeyler ölçtü. Basit tut ilkesi gelmiş bir gereksinim için en az yapıyı ister; ihtiyacın olmayacak ilkesi gelmemiş bir gereksinim için yapı yazılmasını yasaklar. Bir tanesi bugünkü işin biçimiyle, diğeri yarınki iş hakkındaki tahminle ilgilidir.

İlkenin sınırı, atmanın maliyetindedir. Burada atılan 48 satır kitaplığın içinde kaldı; hiçbir dış tüketiciyi etkilemedi, git deposundaki tek bir işlemeyle geri alınabilirdi. Aynı hesap, dışarıya yayımlanmış bir arayüzde ya da geriye dönük dönüştürme gerektiren kalıcı bir veri biçiminde başka sonuç verir: orada yanlış tahminin bedeli tahmini yapmamanın bedelinden büyük olabilir. İlkeyi uygulanabilir kılan soru “bu ileride gerekir mi” değil, “gerektiğinde eklemek bugün eklemekten ne kadar pahalı” sorusudur. Ücret kitaplığında bu fark sıfıra yakındı: dönem indirimi, hiçbir eklenti noktası olmadan altı satırda yazıldı.

Özet

  • İhtiyacın olmayacak ilkesi gelmemiş bir gereksinim için bugünden yapı yazılmasını yasaklar; ölçülen büyüklük tahminin yanlış çıkma maliyetidir.
  • Bugünden yazılan yapılandırma katmanı ile eklenti noktası, ücret dosyasının karar noktası sayısını sıfırdan sekize çıkardı ve dal kapsamını yüzde 45,45’e düşürdü.
  • Satır kapsamının yüzde 100 olması yolların çalıştığı anlamına gelmez; çalışmayan yollar dal ve işlev kapsamında görünür.
  • Gerçekten gelen gereksinim gönderi başına değil dönem başınaydı; eklenti noktasının imzası bunu ifade edemedi ve tek testli koşum 44.938 kuruşluk farkla kırmızı bitti.
  • Genelleştirmenin atılması 48 satır sildi; doğrudan çözüm tek karar noktası, yüzde 100 dal kapsamı ve altı testle aynı gereksinimi karşıladı.
  • İlkenin ölçütü “ileride gerekir mi” değil, gerektiğinde eklemenin bugün eklemeye göre ne kadar pahalı olduğudur.

Sonraki Adım

Genelleştirme atıldıktan sonra kalan dosya çalışıyor ama düzenlenmiş değil: 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. Sonraki ders bu üç kusuru üç mekanik teknikle giderir — ayıklama, taşıma ve yeniden adlandırma — her tekniği ara adımlarıyla uygular ve her adımdan sonra test koşumunu 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