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.