İçeriğe geç
academia.sh

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_degil gö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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat