Ders 09 / 19
Sorma, Söyle
Davranışın veriye taşınması: aynı sevk kuralını dört istemcinin ayrı ayrı kurduğu tasarım ile kararı nesneye söyleyen tasarımın karşılaştırılması, kuralın alanlarını bilen dosya sayısının sayılması, istemcilerin ayrık sonuç kümesinin ölçülmesi ve kural değiştiğinde düzenlenen dosya sayısının bulunması.
İçindekiler
Demeter dersinde zincirler kısaldı, ama ara nesneler hâlâ değer döndürüyordu: ilIlce() bir
dize verir, kararı çağıran verir. Bu, sorunun bir başka biçimini gizler. Bir nesneden alan
alıp o alanlara bakarak karar veren her istemci, kuralın bir kopyasını kendi içinde taşır.
Kopya sayısı arttıkça kuralın tek bir tanımı kalmaz.
Sorma, söyle (tell, don’t ask) bu tekrarı hedefler: veriye sahip olan nesneye ne yapması gerektiği söylenir, verisi alınıp karar dışarıda verilmez. Ölçüsü iki sayıdır — kuralın dayandığı alanları kaç dosya biliyor ve aynı kural kaç dosyada tekrarlanıyor.
Kuralı Dört Kez Yazan Tasarım
Sevk kuralı üç koşuldan oluşuyor: gönderi depoda olmalı, ödemesi yapılmış olmalı ve ağırlığı sınırı aşmamalı. Dört istemci bu kuralı ayrı ayrı kuruyor.
mkdir -p sorma soyle
// sorma/kayit.mjs — sevk edilecek gonderiler export const KAYITLAR = [ { kod: "GN-1", durum: "depoda", odendi: true, agirlik: 12 }, { kod: "GN-2", durum: "depoda", odendi: true, agirlik: 45 }, { kod: "GN-3", durum: "yolda", odendi: true, agirlik: 8 }, { kod: "GN-4", durum: "depoda", odendi: false, agirlik: 5 }, { kod: "GN-5", durum: "depoda", odendi: true, agirlik: 25 }, ];
// sorma/web.mjs — HTTP ucu kurali kendisi kuruyor export function gonder(kayit) { if (kayit.durum === "depoda" && kayit.odendi && kayit.agirlik <= 30) { return { httpKodu: 200, kod: kayit.kod }; } return { httpKodu: 409, kod: kayit.kod }; }
// sorma/toplu.mjs — toplu isleme ayni kurali yeniden yaziyor export const secilenler = (kayitlar) => kayitlar .filter((k) => k.durum === "depoda" && k.odendi && k.agirlik <= 30) .map((k) => k.kod);
// sorma/kurye.mjs — kurye uygulamasi agirlik kosulunu yazmamis export const listele = (kayitlar) => kayitlar.filter((k) => k.durum === "depoda" && k.odendi).map((k) => k.kod);
// sorma/zamanli.mjs — zamanlanmis is kurali ucuncu kez yaziyor export function tarama(kayitlar) { const cikacak = []; for (const k of kayitlar) { if (k.durum === "depoda" && k.odendi && k.agirlik <= 30) cikacak.push(k.kod); } return cikacak; }
Kararı Nesneye Söyleyen Tasarım
İkinci tasarımda kural gönderi nesnesinin içindedir. İstemciler alan okumaz; nesneye yola çıkmasını söyler ve sonucu alır.
// soyle/kayit.mjs — sevk edilecek gonderiler export const KAYITLAR = [ { kod: "GN-1", durum: "depoda", odendi: true, agirlik: 12 }, { kod: "GN-2", durum: "depoda", odendi: true, agirlik: 45 }, { kod: "GN-3", durum: "yolda", odendi: true, agirlik: 8 }, { kod: "GN-4", durum: "depoda", odendi: false, agirlik: 5 }, { kod: "GN-5", durum: "depoda", odendi: true, agirlik: 25 }, ];
// soyle/gonderi.mjs — kural nesnenin icinde, tek yerde const AGIRLIK_SINIRI = 30; export const gonderi = (kayit) => ({ kod: kayit.kod, yolaCikar() { if (kayit.durum !== "depoda") return { ok: false, sebep: "depoda_degil" }; if (kayit.odendi === false) return { ok: false, sebep: "odenmedi" }; if (kayit.agirlik > AGIRLIK_SINIRI) return { ok: false, sebep: "agirlik_siniri" }; return { ok: true, sebep: "yola_cikti" }; }, });
// soyle/web.mjs — karari nesneye soyler export function gonder(g) { return { httpKodu: g.yolaCikar().ok ? 200 : 409, kod: g.kod }; }
// soyle/toplu.mjs — ayni emri verir export const secilenler = (gonderiler) => gonderiler.filter((g) => g.yolaCikar().ok).map((g) => g.kod);
// soyle/kurye.mjs — ayni emri verir; kosul yazma firsati yok export const listele = (gonderiler) => gonderiler.filter((g) => g.yolaCikar().ok).map((g) => g.kod);
// soyle/zamanli.mjs — ayni emri verir export function tarama(gonderiler) { const cikacak = []; for (const g of gonderiler) if (g.yolaCikar().ok) cikacak.push(g.kod); return cikacak; }
Tekrarın Sayılması
İlk ölçü kuralın dayandığı alan adlarını kaç dosyanın bildiğidir. Bir dosya bu alanlardan ikisini birden biliyorsa kuralın bir kopyasını taşıyor sayılır; veri dosyası bu sayımın dışındadır.
// kural-say.mjs — kuralin dayandigi alanlari kac dosyanin bildigini sayar import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; const ALANLAR = ["durum", "odendi", "agirlik"]; const kok = process.argv[2]; let bilenDosya = 0; let tekrar = 0; for (const d of readdirSync(kok).filter((a) => a.endsWith(".mjs")).sort()) { const metin = readFileSync(join(kok, d), "utf8").replace(/^\/\/.*$/gm, ""); const bilinen = ALANLAR.filter((a) => new RegExp(`\\b${a}\\b`).test(metin)); if (bilinen.length > 0) bilenDosya += 1; if (bilinen.length >= 2 && d !== "kayit.mjs") tekrar += 1; console.log(` ${d.padEnd(12)} bilinen alan=${bilinen.length} ${bilinen.join(" ")}`); } console.log(`${kok.padEnd(6)} alani bilen dosya=${bilenDosya}, kurali tekrarlayan dosya=${tekrar}`);
node kural-say.mjs sorma node kural-say.mjs soyle
kayit.mjs bilinen alan=3 durum odendi agirlik kurye.mjs bilinen alan=2 durum odendi toplu.mjs bilinen alan=3 durum odendi agirlik web.mjs bilinen alan=3 durum odendi agirlik zamanli.mjs bilinen alan=3 durum odendi agirlik sorma alani bilen dosya=5, kurali tekrarlayan dosya=4 gonderi.mjs bilinen alan=3 durum odendi agirlik kayit.mjs bilinen alan=3 durum odendi agirlik kurye.mjs bilinen alan=0 toplu.mjs bilinen alan=0 web.mjs bilinen alan=0 zamanli.mjs bilinen alan=0 soyle alani bilen dosya=2, kurali tekrarlayan dosya=1
Birinci tasarımda dört dosya kuralı tekrarlıyor; ikincisinde bir. Tarama aynı zamanda kuralın çatladığı yeri de gösterdi: kurye modülü üç alandan yalnız ikisini biliyor.
Ayrık Sonuç Kümesi
Kopyalar arasındaki sapmanın koşumdaki karşılığı, aynı veriyle çalışan istemcilerin farklı listeler üretmesidir. Sürücü betik dört istemciyi aynı beş gönderiyle çalıştırır ve kaç ayrı sonuç çıktığını sayar.
// kur.mjs — dort istemci ayni veriyle calisir, sonuclari karsilastirilir const kok = process.argv[2]; const { KAYITLAR } = await import(`./${kok}/kayit.mjs`); const { gonder } = await import(`./${kok}/web.mjs`); const { secilenler } = await import(`./${kok}/toplu.mjs`); const { listele } = await import(`./${kok}/kurye.mjs`); const { tarama } = await import(`./${kok}/zamanli.mjs`); const girdi = kok.startsWith("soyle") ? KAYITLAR.map((await import(`./${kok}/gonderi.mjs`)).gonderi) : KAYITLAR; const sonuclar = { web: girdi.filter((g) => gonder(g).httpKodu === 200).map((g) => g.kod), toplu: secilenler(girdi), kurye: listele(girdi), zamanli: tarama(girdi), }; for (const [ad, liste] of Object.entries(sonuclar)) console.log(`${ad.padEnd(9)} ${liste.join(" ")}`); const ayrik = new Set(Object.values(sonuclar).map((l) => l.join(" "))); console.log(`${kok}: farkli sonuc veren istemci kumesi = ${ayrik.size}`);
node kur.mjs sorma node kur.mjs soyle
web GN-1 GN-5 toplu GN-1 GN-5 kurye GN-1 GN-2 GN-5 zamanli GN-1 GN-5 sorma: farkli sonuc veren istemci kumesi = 2 web GN-1 GN-5 toplu GN-1 GN-5 kurye GN-1 GN-5 zamanli GN-1 GN-5 soyle: farkli sonuc veren istemci kumesi = 1
Kırk beş kilogramlık GN-2 kurye listesinde görünüyor, diğer üç istemcide görünmüyor. İkinci tasarımda böyle bir sapma için yer yok, çünkü koşulu yazacak dört ayrı gövde yok.
Kural Değiştiğinde
Ağırlık sınırı 30’dan 20’ye iniyor. İki ağaç kopyalanır ve değişiklik ikisine de uygulanır.
Yerinde düzenlemede yedek uzantısı verilmiştir; GNU ve BSD sed bu biçimde aynı davranır.
cp -r sorma sorma-yeni cp -r soyle soyle-yeni sed -i.y 's/agirlik <= 30/agirlik <= 20/' sorma-yeni/*.mjs sed -i.y 's/AGIRLIK_SINIRI = 30/AGIRLIK_SINIRI = 20/' soyle-yeni/gonderi.mjs rm -f sorma-yeni/*.y soyle-yeni/*.y node kur.mjs sorma-yeni node kur.mjs soyle-yeni for k in sorma soyle; do echo "$k: duzenlenen dosya = $(diff -rq $k $k-yeni | grep -c '^Files')"; done
web GN-1 toplu GN-1 kurye GN-1 GN-2 GN-5 zamanli GN-1 sorma-yeni: farkli sonuc veren istemci kumesi = 2 web GN-1 toplu GN-1 kurye GN-1 zamanli GN-1 soyle-yeni: farkli sonuc veren istemci kumesi = 1 sorma: duzenlenen dosya = 3 soyle: duzenlenen dosya = 1
Üç dosyaya karşı bir dosya. Sapmanın büyüdüğüne de dikkat: kurye listesi eskiden yalnız GN-2 fazlasını içeriyordu, sınır düşünce GN-5 de eklendi. Kural kopyalarının bir kez ayrışması kalıcıdır; ayrışma her kural değişikliğinde büyür.
İlkenin Sınırı
“Sorma, söyle” her alan okumasını yasaklamaz. Bir alanın dışarıya verilmesi, o alanla ilgili bir karar dışarıda veriliyorsa sorun olur. Gönderi kodunu raporda yazdırmak için okumak karar değildir; kodun biçimine bakıp gönderinin yurt içi mi olduğuna karar vermek karardır.
İlkenin aşırı uygulanışı da ölçülebilir bir maliyet üretir. Nesneye her soru için ayrı bir emir yöntemi eklenirse arayüz büyür ve arayüz ayrımı dersinde ölçülen fazlalık geri gelir. Ölçüt, yöntem sayısı değil, kuralı tekrarlayan dosya sayısıdır: bu sayı birden büyükse davranış yanlış yerdedir.
Özet
- Sorma–söyle ilkesi, veriye sahip nesneden alan alıp dışarıda karar vermek yerine karara konu olan işi nesneye söylemeyi ister.
- Kuralın alanlarını bilen dosya sayısı 5’ten 2’ye, kuralı tekrarlayan dosya sayısı 4’ten 1’e indi.
- Tekrar sapmayı üretti: dört istemci aynı veriyle 2 ayrı sonuç kümesi verdi, tek tanımlı sürümde 1 küme çıktı.
- Ağırlık sınırı değiştiğinde tekrarlı tasarımda 3, tek tanımlı tasarımda 1 dosya düzenlendi; sapma kural değişikliğiyle büyüdü.
- İlke alan okumasını değil, alan üzerinde dışarıda karar vermeyi sınırlar; ölçüt kuralı tekrarlayan dosya sayısıdır.
Sonraki Adım
yolaCikar bir emir gibi adlandırıldı ama aslında yalnız bir sonuç döndürüyor: gönderinin
durumu değişmiyor. Gerçek bir sevk işlemi durumu da değiştirmeli. Aynı yöntem hem durumu
değiştirip hem bir değer döndürdüğünde ne olur? Yöntemi iki kez çağırmak aynı sonucu verir
mi, bir günlük satırına yazmak için yapılan fazladan bir çağrı sistemin durumunu değiştirir
mi? Sonraki ders hem durum değiştiren hem değer döndüren bir işlemin ürettiği sürprizi bir
koşumla gösterir ve iki sorumluluğu ayırmanın ölçüsünü kurar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.