İçeriğe geç
academia.sh

Ders 02 / 19

Her Yerde Aynı Dil

Kod ile konuşma dilinin tek sözlükte birleşmesi: beş alan kavramına düşen ad sayısının iki sürümde sayılması, aynı kavramın iki adı arasında çeviri yapan nokta ve birim çevrimi sayısının ölçülmesi, aynı kuralın kaç gövdede yazıldığının ve tavanı kaç gövdenin uyguladığının çıkarılması, otuz gönderide teklif ile ücretin ayrılma sayısının ölçülmesi.

İçindekiler

Önceki ders hizayı tek dosyada ölçtü: uzmanın on iki adı bir kez yazıldı ve on ikisi de kodda bulundu. Gerçek bir kod tabanında aynı kavram birden çok yerde adlandırılır. Girdiyi alan modül bir ad kullanır, ücret hesaplayan modül ikincisini, teklif üreten modül üçüncüsünü. O zaman soru adların alandan gelip gelmediği değil, kaç tanesinin aynı şeyi adlandırdığıdır.

Bu dersin konusu her yerde aynı dil (ubiquitous language): bir alan kavramının, alanın konuşulduğu her yerde ve kodun her katmanında tek bir adla anılması. “Her yerde” sözü bir üslup önerisi değil, ölçülebilir bir kısıttır — kavram başına ad sayısı bir olacaktır.

Beş Kavram, On Üç Ad

Kitaplığın ücretlendirme tarafında beş kavram var: gönderi, ağırlık, bölge, indirim, asgari ücret. Aşağıdaki sürüm üç dosyaya yayılmış ve her dosya kendi adlarını seçmiş.

mkdir -p dagnik birlesik
// dagnik/giris.mjs — kaydin girisi: kargo
export const kargoKaydi = (agirlikGram, zonKodu, iskontolar) =>
  ({ agirlikGram, zonKodu, iskontolar });
// dagnik/ucretlendirme.mjs — paket adiyla calisir, tavani uygular
const KADEME = [[1, 3900], [5, 6400], [Infinity, 11800]];
const KATSAYI = { "34": 1, "06": 1.35, "65": 1.8 };
const TABAN_FIYAT = 3990;
const TAVAN = 0.4;

export function ucret(kargo) {
  const paket = {
    agirlik: kargo.agirlikGram / 1000,
    bolge: kargo.zonKodu,
    indirimler: kargo.iskontolar,
  };
  const ham = KADEME.find(([u]) => paket.agirlik <= u)[1] * (KATSAYI[paket.bolge] ?? 1.8);
  const oran = Math.min(paket.indirimler.reduce((t, i) => t + i.oran, 0), TAVAN);
  return Math.max(Math.round(ham * (1 - oran)), TABAN_FIYAT);
}
// dagnik/teklif.mjs — gonderi adiyla calisir, tavani bilmiyor
const KADEME = [[1, 3900], [5, 6400], [Infinity, 11800]];
const KATSAYI = { "34": 1, "06": 1.35, "65": 1.8 };
const ASGARI_UCRET = 3990;

export function teklif(kargo) {
  const gonderi = {
    kg: kargo.agirlikGram / 1000,
    bolgeKodu: kargo.zonKodu,
    iskontolar: kargo.iskontolar,
  };
  const ham = KADEME.find(([u]) => gonderi.kg <= u)[1] * (KATSAYI[gonderi.bolgeKodu] ?? 1.8);
  const oran = gonderi.iskontolar.reduce((t, i) => t + i.oran, 0);
  return Math.max(Math.round(ham * (1 - oran)), ASGARI_UCRET);
}

Üç dosyanın her biri kendi içinde tutarlıdır ve her adı alandan gelmiştir: kargo, paket, gonderi sözcüklerinin üçü de kitaplığın konuşulduğu odada duyulur. Önceki dersin ad hizası ölçüsü bu sürümde yüksek çıkar. Sorun tek tek adlarda değil, aynı kavramın birden çok adı olmasındadır.

Tek Sözlük

Birleşik sürüm her kavram için bir ad seçer ve o adı girişten hesaba kadar taşır. Ağırlık kilogram cinsinden girer, dolayısıyla dosya içinde bir birim çevrimi kalmaz.

// birlesik/giris.mjs — kaydin girisi: gonderi, agirlik kilogram cinsinden
export const gonderi = (agirlik, bolge, indirimler) => ({ agirlik, bolge, indirimler });
// birlesik/ucretlendirme.mjs — tek ad kumesi, tek kural govdesi
const KADEMELER = [[1, 3900], [5, 6400], [Infinity, 11800]];
const BOLGE_KATSAYISI = { "34": 1, "06": 1.35, "65": 1.8 };
const ASGARI_UCRET = 3990;
const INDIRIM_TAVANI = 0.4;

const tabanUcret = (agirlik) => KADEMELER.find(([ust]) => agirlik <= ust)[1];
const indirimOrani = (indirimler) =>
  Math.min(indirimler.reduce((t, i) => t + i.oran, 0), INDIRIM_TAVANI);

function netUcret(gonderi) {
  const ham = tabanUcret(gonderi.agirlik) * (BOLGE_KATSAYISI[gonderi.bolge] ?? 1.8);
  return Math.max(Math.round(ham * (1 - indirimOrani(gonderi.indirimler))), ASGARI_UCRET);
}

export const ucret = (gonderi) => netUcret(gonderi);
export const teklif = (gonderi) => netUcret(gonderi);

Alanda teklif ile fatura ücretinin aynı kurala tabi olduğu söyleniyor; birleşik sürümde bu cümlenin karşılığı iki dışa açılan adın aynı gövdeyi çağırmasıdır.

Ölçüm

Dört ölçü alınır: kavram başına ad sayısı, aynı kavramın iki adı arasında çeviri yapan nokta sayısı, kuralın kaç gövdede yazıldığı ve tavanı kaç gövdenin uyguladığı. Sonuna otuz gönderide teklif ile ücretin ayrılma sayısı eklenir.

// dil-olc.mjs — kavram basina ad, ceviri noktasi, kural govdesi ve iki yolun ayrilmasi
import { readFileSync } from "node:fs";
import { ucret as dUcret } from "./dagnik/ucretlendirme.mjs";
import { teklif as dTeklif } from "./dagnik/teklif.mjs";
import { kargoKaydi } from "./dagnik/giris.mjs";
import { ucret as bUcret, teklif as bTeklif } from "./birlesik/ucretlendirme.mjs";
import { gonderi as bGonderi } from "./birlesik/giris.mjs";

const KAVRAM = {
  gonderi: ["kargo", "paket", "gonderi"],
  agirlik: ["agirlikGram", "kg", "agirlik"],
  bolge: ["zonKodu", "bolgeKodu", "bolge"],
  indirim: ["iskontolar", "indirimler"],
  asgariUcret: ["TABAN_FIYAT", "ASGARI_UCRET"],
};
const SURUM = {
  dagnik: ["dagnik/giris.mjs", "dagnik/ucretlendirme.mjs", "dagnik/teklif.mjs"],
  birlesik: ["birlesik/giris.mjs", "birlesik/ucretlendirme.mjs"],
};
const kavramiBul = (ad) => Object.keys(KAVRAM).find((k) => KAVRAM[k].includes(ad)) ?? null;

for (const [surum, dosyalar] of Object.entries(SURUM)) {
  const metin = dosyalar.map((d) => readFileSync(d, "utf8")).join("\n");
  const jeton = new Set(metin.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? []);
  const dokum = [];
  let toplamAd = 0;
  for (const [kavram, adlar] of Object.entries(KAVRAM)) {
    const varOlan = adlar.filter((a) => jeton.has(a));
    toplamAd += varOlan.length;
    dokum.push(`${kavram}=${varOlan.length}`);
  }
  const tek = metin.replace(/\s+/g, " ");
  const ceviri = [...tek.matchAll(/(\w+):\s*\w+\.(\w+)/g)].filter(
    ([, sol, sag]) => sol !== sag && kavramiBul(sol) && kavramiBul(sol) === kavramiBul(sag));
  const birim = [...tek.matchAll(/\/ 1000/g)].length;
  const govdeli = dosyalar.map((d) => readFileSync(d, "utf8"))
    .filter((m) => /Math\.max\(Math\.round/.test(m));
  const tavanli = govdeli.filter((m) => /TAVAN/.test(m)).length;
  console.log(`${surum}: dosya ${dosyalar.length}, 5 kavrama ${toplamAd} ad`);
  console.log(`  ad dagilimi: ${dokum.join("  ")}`);
  console.log(`  ceviri noktasi ${ceviri.length}, birim cevrimi ${birim}`);
  console.log(`  kural govdesi ${govdeli.length}, tavani uygulayan govde ${tavanli}`);
}

const ORNEK = [];
for (let i = 0; i < 30; i += 1) {
  const hacimOrani = [0.1, 0.2, 0.3][i % 3] + (i % 5) * 0.05;
  ORNEK.push([600 + i * 400, ["34", "06", "65"][i % 3],
    [{ ad: "sozlesme", oran: 0.2 }, { ad: "hacim", oran: Math.round(hacimOrani * 100) / 100 }]]);
}
const ayrilan = (a, b) => ORNEK.filter((o) => a(o) !== b(o)).length;

console.log(`dagnik   ucret != teklif : ${ayrilan(
  (o) => dUcret(kargoKaydi(...o)), (o) => dTeklif(kargoKaydi(...o)))} / ${ORNEK.length}`);
console.log(`birlesik ucret != teklif : ${ayrilan(
  (o) => bUcret(bGonderi(o[0] / 1000, o[1], o[2])),
  (o) => bTeklif(bGonderi(o[0] / 1000, o[1], o[2])))} / ${ORNEK.length}`);
node dil-olc.mjs
dagnik: dosya 3, 5 kavrama 13 ad
  ad dagilimi: gonderi=3  agirlik=3  bolge=3  indirim=2  asgariUcret=2
  ceviri noktasi 5, birim cevrimi 2
  kural govdesi 2, tavani uygulayan govde 1
birlesik: dosya 2, 5 kavrama 5 ad
  ad dagilimi: gonderi=1  agirlik=1  bolge=1  indirim=1  asgariUcret=1
  ceviri noktasi 0, birim cevrimi 0
  kural govdesi 1, tavani uygulayan govde 1
dagnik   ucret != teklif : 19 / 30
birlesik ucret != teklif : 0 / 30

Beş kavram dağınık sürümde on üç ad taşıyor, birleşik sürümde beş. Aradaki sekiz fazla adın bedeli ikinci satırda: aynı kavramın iki adı arasında beş çeviri noktası ve iki birim çevrimi. Çeviri noktası, bir dosyanın öteki dosyanın sözlüğünü bilmek zorunda olduğu yerdir; her biri bir eşleme satırı olarak yazılır ve bir ad değiştiğinde tek tek gözden geçirilir.

Ayrılan Sonuçların Kaynağı

Son iki satır hizasızlığın bedelini davranış cinsinden veriyor: otuz gönderinin on dokuzunda teklif ile ücret farklı çıkıyor, oysa alan uzmanı ikisinin aynı olduğunu söylüyor. Ayrılmanın doğrudan yapısal nedeni dördüncü satırdadır: dağınık sürümde iki kural gövdesi var ve tavanı yalnız biri uyguluyor.

Buradaki asıl gözlem, iki gövdenin aynı kural olduğunun kod üzerinden görünmemesidir. paket.indirimler ile gonderi.iskontolar iki ayrı ad olduğu için tavanı arayan bir okuma ikinci gövdede aranacak sözcüğü bulamaz. Adlar ayrıldığında kuralların ayrılması bir dikkat kusuru olmaktan çıkar, yapının izin verdiği bir sonuç hâline gelir. Birleşik sürümde aynı ayrılma yazılamaz: tek gövde vardır, iki dışa açılan ad onu çağırır ve ayrılan sonuç 0’a iner.

Sözlüğün Bakımı

Tek sözlük bir kereye mahsus bir seçim değildir; alan uzmanının dili değiştikçe kodun da değişmesi gerekir. Bunun iki yönü vardır. Birincisi, kodda bir ad değiştiğinde alanda karşılığının da değişmiş olması: iskonto sözcüğü artık kullanılmıyorsa kaynakta da kalmamalıdır. İkincisi ve daha önemlisi, alanda yeni bir ayrım doğduğunda kodda yeni bir ad açılmasıdır. Sözleşmeli müşterinin indirimi ile kampanya indirimi ayrı kurallara tabi olmaya başladığı anda, ikisini indirim adı altında toplamak tek sözlük olmaktan çıkar — alanda iki kavram olan şeyi kodda bir kavramla anmak, aynı hizasızlığın ters yöndeki biçimidir.

Bu yüzden ölçü çift taraflıdır: kavram başına ad sayısı bir olacak, ad başına kavram sayısı da bir olacaktır. İkinci kısmın ihlali dersin sonunda görünmeye başlar; aynı gonderi sözcüğünün iki farklı şeyi adlandırdığı durum bu konunun ölçtüğü şeyin dışındadır.

Ne Zaman Karşılığını Vermez

Tek sözlüğün bedeli yeniden adlandırma işidir ve bu bedel dokunulan dosya sayısıyla ölçülür. Yukarıdaki üç dosyada sekiz fazla adı gidermek küçük bir iştir; sözlüğün yüz dosyaya yayıldığı bir kod tabanında aynı iş bir dizi mekanik değişiklik gerektirir ve Yazılım Kalitesi ve Test kursunda kurulan davranış koruması olmadan yapılamaz.

İkinci durum ölçünün girdisiyle ilgilidir. Ad sayısı ancak kavram listesi sabitse sayılabilir. Alanın kendisi henüz oturmamışsa — hangi ayrımların kalıcı olduğu bilinmiyorsa — adlar da oturmaz ve erken yapılan bir birleştirme, alanda gerçekten iki olan şeyi tek ada sıkıştırabilir. Bu durumda ölçü sıfıra iner ama hiza bozulur.

Özet

  • Her yerde aynı dil, bir alan kavramının kodun her yerinde tek adla anılmasıdır; ölçüsü kavram başına ad sayısıdır.
  • Dağınık sürümde beş kavram on üç ad taşıyordu; birleşik sürümde beş ad kaldı.
  • Fazla adların bedeli sayıldı: aynı kavramın iki adı arasında 5 çeviri noktası ve 2 birim çevrimi, birleşik sürümde 0 ve 0.
  • Dağınık sürümde kural iki gövdeye bölünmüştü ve tavanı yalnız biri uyguluyordu; otuz gönderinin 19’unda teklif ile ücret ayrıldı, birleşik sürümde 0’ında.
  • Ölçü çift taraflıdır: kavram başına bir ad, ad başına bir kavram. İkincisinin ihlali alanda iki olan şeyi kodda bir adla anmaktır.

Sonraki Adım

Birleşik sürümde gönderi üç alan taşıyan düz bir kayıttı ve iki gönderi ancak alanları eşitse aynı sayılabilirdi. Alanda bu doğru değildir: aynı gönderinin ağırlığı tartım düzeltmesiyle değişir ve gönderi hâlâ aynı gönderidir; iki ayrı gönderinin bütün alanları tesadüfen eşit olabilir ve ikisi hâlâ iki gönderidir. Sonraki ders bu ayrımı ölçüye çevirir: aynı kimliğin kaç ayrı örnekle temsil edilebildiğini ve alanları eşitleyince kaç ayrı gönderinin tek örneğe çöktüğünü sayar, kimliği açıkça taşıyan bir modelle karşılaştırır.

İ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