Ders 10 / 19
Komut–Sorgu Ayrımı
Durum değiştiren ve okuyan işlemlerin ayrılması: hem yazan hem döndüren yöntemleri bulan bir taramanın yazılması, yalnız rapor üreten bir yolun kaç kaydı değiştirdiğinin sayılması ve günlük satırının gerçekte olanın tersini yazdığının koşumla gösterilmesi.
İçindekiler
Önceki dersin yolaCikar yöntemi bir emir gibi adlandırılmıştı ama yalnız bir sonuç
döndürüyordu; gönderinin durumu değişmiyordu. Gerçek bir sevk işlemi durumu da değiştirmeli.
Yöntem bu iki işi birlikte üstlendiğinde yeni bir soru doğar: aynı yöntemi ikinci kez
çağırmak neyi değiştirir?
Komut–sorgu ayrımı (command–query separation) Programlama Temelleri kursunda tanıtılmıştı: bir işlem ya durumu değiştirir ve değer döndürmez (komut), ya da değer döndürür ve durumu değiştirmez (sorgu). Bu ders ilkeyi nesne tasarımına taşır ve ihlalinin bedelini iki sayıyla ölçer: hem yazan hem döndüren yöntem sayısı ve yalnız okumak isteyen bir yolun değiştirdiği kayıt sayısı.
Karışık Yöntem
Birinci tasarımda yolaCikar hem durum alanını yazar hem sonucu döndürür.
mkdir -p karisik ayrik
// karisik/kayit.mjs — her deney taze kayitlarla baslasin diye uretici export const yeniKayitlar = () => [ { 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 }, ];
// karisik/gonderi.mjs — tek yontem hem durumu degistiriyor hem deger donduruyor 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" }; kayit.durum = "yolda"; return { ok: true, sebep: "yola_cikti" }; }, });
// karisik/sevk.mjs — once sevk eder, sonra gunluge yazmak icin sebebi yeniden sorar export function sevkEt(gonderiler) { const cikan = gonderiler.filter((g) => g.yolaCikar().ok).map((g) => g.kod); const gunluk = gonderiler.map((g) => `${g.kod}=${g.yolaCikar().sebep}`); return { cikan, gunluk }; }
// karisik/rapor.mjs — yalniz sebepleri yazmak isteyen rapor export const rapor = (gonderiler) => gonderiler.map((g) => `${g.kod}=${g.yolaCikar().sebep}`);
Rapor modülü hiçbir şeyi sevk etmek istemiyor; yalnız her gönderinin durumunu yazacak. Elinde soracak tek yöntem olduğu için onu çağırıyor.
Ayrık Yöntemler
İkinci tasarımda aynı bilgi iki yönteme bölünür. sevkEngeli bir sorgudur: durumu okur,
değiştirmez. yolaCikar bir komuttur: durumu değiştirir, değer döndürmez; sevk edilemeyen
bir gönderi için hata verir.
// ayrik/kayit.mjs — her deney taze kayitlarla baslasin diye uretici export const yeniKayitlar = () => [ { 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 }, ];
// ayrik/gonderi.mjs — sorgu durum degistirmez, komut deger dondurmez const AGIRLIK_SINIRI = 30; export const gonderi = (kayit) => ({ kod: kayit.kod, sevkEngeli() { if (kayit.durum !== "depoda") return "depoda_degil"; if (kayit.odendi === false) return "odenmedi"; if (kayit.agirlik > AGIRLIK_SINIRI) return "agirlik_siniri"; return null; }, yolaCikar() { const engel = this.sevkEngeli(); if (engel !== null) throw new RangeError(`${kayit.kod} sevk edilemez: ${engel}`); kayit.durum = "yolda"; }, });
// ayrik/sevk.mjs — once sorar, gunluge yazar, sonra komutu verir export function sevkEt(gonderiler) { const cikan = []; const gunluk = []; for (const g of gonderiler) { const engel = g.sevkEngeli(); gunluk.push(`${g.kod}=${engel ?? "yola_cikti"}`); if (engel === null) { g.yolaCikar(); cikan.push(g.kod); } } return { cikan, gunluk }; }
// ayrik/rapor.mjs — yalniz sebepleri yazmak isteyen rapor export const rapor = (gonderiler) => gonderiler.map((g) => `${g.kod}=${g.sevkEngeli() ?? "yola_cikti"}`);
Sürprizin Koşumla Gösterilmesi
Sürücü betik iki deney yapar. Birincisinde yalnız rapor çağrılır ve raporun kaç kaydın durumunu değiştirdiği sayılır. İkincisinde sevk işlemi çalıştırılır, günlük satırları yazılır ve aynı işlem ikinci kez çağrılır.
// kur.mjs — uc deney: rapor, sevk gunlugu ve ayni islemin ikinci kez cagrilmasi const kok = process.argv[2]; const { yeniKayitlar } = await import(`./${kok}/kayit.mjs`); const { gonderi } = await import(`./${kok}/gonderi.mjs`); const { rapor } = await import(`./${kok}/rapor.mjs`); const { sevkEt } = await import(`./${kok}/sevk.mjs`); const kayitlar = yeniKayitlar(); const once = kayitlar.map((k) => k.durum); console.log("rapor ", rapor(kayitlar.map(gonderi)).join(" ")); const sonra = kayitlar.map((k) => k.durum); console.log(`raporun degistirdigi kayit = ${once.filter((d, i) => d !== sonra[i]).length}`); const ikinciKayitlar = yeniKayitlar(); const gonderiler = ikinciKayitlar.map(gonderi); const birinci = sevkEt(gonderiler); console.log("sevk 1 cikan", birinci.cikan.join(" ") || "(bos)"); console.log("sevk 1 gunluk", birinci.gunluk.join(" ")); const ikinci = sevkEt(gonderiler); console.log("sevk 2 cikan", ikinci.cikan.join(" ") || "(bos)");
node kur.mjs karisik
rapor GN-1=yola_cikti GN-2=agirlik_siniri GN-3=depoda_degil GN-4=odenmedi GN-5=yola_cikti raporun degistirdigi kayit = 2 sevk 1 cikan GN-1 GN-5 sevk 1 gunluk GN-1=depoda_degil GN-2=agirlik_siniri GN-3=depoda_degil GN-4=odenmedi GN-5=depoda_degil sevk 2 cikan (bos)
İki sürpriz birden. Rapor iki kaydın durumunu “yolda” yaptı: hiçbir şey sevk etmek istemeyen
bir kod yolu iki gönderiyi yola çıkardı. Günlük satırı ise gerçekte olanın tersini yazdı —
sevk edilen GN-1 ve GN-5 için depoda_degil görünüyor, çünkü sebep sorulduğunda durum artık
değişmişti.
node kur.mjs ayrik
rapor GN-1=yola_cikti GN-2=agirlik_siniri GN-3=depoda_degil GN-4=odenmedi GN-5=yola_cikti raporun degistirdigi kayit = 0 sevk 1 cikan GN-1 GN-5 sevk 1 gunluk GN-1=yola_cikti GN-2=agirlik_siniri GN-3=depoda_degil GN-4=odenmedi GN-5=yola_cikti sevk 2 cikan (bos)
Rapor satırı aynı, ama değiştirdiği kayıt sayısı sıfır. Günlük satırı da gerçeği yazıyor. Sevk sonucu iki sürümde de aynı; ayrım davranışı değiştirmedi, yalnız gözlemi güvenli hâle getirdi.
Taramayla Bulunması
İhlal koşum beklemeden de bulunabilir. Aşağıdaki betik nesne yöntemlerinin gövdesine bakar:
gövdede kayda yazma varsa yöntem komuttur, değer döndüren return varsa sorgudur, ikisi de
varsa bulgudur.
// komut-sorgu-tara.mjs — hem durum degistiren hem deger donduren yontemleri bulur import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; const YONTEM = /^ {2}(\w+)\(/gm; function govde(metin, acilis) { let i = acilis; let derinlik = 0; do { if (metin[i] === "{") derinlik += 1; else if (metin[i] === "}") derinlik -= 1; i += 1; } while (i < metin.length && derinlik > 0); return metin.slice(acilis, i); } const kok = process.argv[2]; let bulgu = 0; for (const d of readdirSync(kok).filter((a) => a.endsWith(".mjs")).sort()) { const metin = readFileSync(join(kok, d), "utf8"); for (const m of metin.matchAll(YONTEM)) { const g = govde(metin, metin.indexOf("{", m.index + m[0].length)); const yazar = /\bkayit\.\w+\s*=[^=]/.test(g); const doner = /\breturn\s+[^;\s]/.test(g); const tur = yazar && doner ? "komut+sorgu" : yazar ? "komut" : doner ? "sorgu" : "bos"; if (yazar && doner) bulgu += 1; console.log(` ${d.padEnd(12)} ${m[1].padEnd(12)} yazar=${yazar} doner=${doner} ${tur}`); } } console.log(`${kok.padEnd(8)} hem yazan hem donduren yontem = ${bulgu}`);
node komut-sorgu-tara.mjs karisik node komut-sorgu-tara.mjs ayrik
gonderi.mjs yolaCikar yazar=true doner=true komut+sorgu karisik hem yazan hem donduren yontem = 1 gonderi.mjs sevkEngeli yazar=false doner=true sorgu gonderi.mjs yolaCikar yazar=true doner=false komut ayrik hem yazan hem donduren yontem = 0
Ayrımın Kabul Edilen İstisnaları
Ayrım mutlak uygulanamaz. Bir yığıttan öge çıkaran işlem hem durumu değiştirir hem çıkardığı ögeyi döndürür; ayrıştırıldığında iki çağrı arasında yarış oluşabilir. Aynı biçimde bir kayıt ekleyip üretilen tanıtıcıyı döndüren işlem de karışıktır ve bölünmesi tanıtıcıyı ikinci bir sorguyla aramayı gerektirir.
İstisnayı kabul edilebilir yapan iki koşul vardır. Birincisi, döndürülen değerin işlemin sonucu olmasıdır — çıkarılan öge, üretilen tanıtıcı — yeniden hesaplanan bir durum değil. İkincisi, çağrının okuma amacıyla tekrarlanmamasıdır. Yukarıdaki karışık sürümde iki koşul da ihlal edilmişti: dönen değer sevkin sonucu değil koşulların yeniden değerlendirmesiydi ve günlük satırı için yeniden çağrıldı.
Adlandırma bu yüzden ölçünün bir parçasıdır. sevkEngeli bir soru sorar, yolaCikar bir emir
verir; ikisinin karışması genellikle adın da karışmasıyla başlar.
Özet
- Komut–sorgu ayrımı bir işlemin ya durumu değiştirmesini ve değer döndürmemesini, ya da değer döndürüp durumu değiştirmemesini ister.
- Tarama, gövdesinde hem kayda yazma hem değer döndürme bulunan yöntemleri sayar: karışık tasarımda 1, ayrık tasarımda 0 bulgu çıktı.
- Yalnız rapor üreten kod yolu karışık tasarımda 2 kaydın durumunu değiştirdi, ayrık tasarımda 0 kayıt değiştirdi.
- Günlük satırı karışık tasarımda gerçeğin tersini yazdı: sevk edilen iki gönderi için
depoda_degilgöründü, çünkü sebep sorulduğunda durum çoktan değişmişti. - İstisna, dönen değerin işlemin sonucu olduğu ve çağrının okuma amacıyla tekrarlanmadığı durumlarda kabul edilir; adlandırma ayrımın görünür parçasıdır.
Sonraki Adım
Ayrık tasarımda AGIRLIK_SINIRI sabiti gönderi modülünün içinde duruyor ve sevk kuralı da
orada. Bu iki karar aynı dosyada, ama aynı nedenle değişmiyorlar: sınır fiyat listesi
yenilendiğinde, kural kurum politikası değiştiğinde oynar. Bir modülde birden çok değişim
ekseni bulunduğunda, iki ayrı eksenden gelen iki değişiklik aynı dosyalara dokunur ve
birbirinin sınamasını kırar. Sonraki ders iki eksende gelen değişikliğin dokunduğu dosya
kümelerini çıkarır, kesişimlerini ölçer ve eksenleri yalıtmanın bu kesişimi nasıl boşalttığını
gösterir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.