İçeriğe geç
academia.sh

Ders 20 / 30

Yineleyici ve Ziyaretçi

Rota ağacındaki beş işlemin her birinin kendi gezinme kodunu taşıması ile gezinmenin yineleyiciye, tür ayrımının ziyaretçiye alınmasının karşılaştırılması: gezinme kodu taşıyan yer sayısı, gezinme sırası değiştiğinde düzenlenen dosya sayısı, yeni düğüm türü eklendiğinde sessizce yanlış sonuç veren işlem sayısı ile hatayı bildiren işlem sayısı.

İçindekiler

Zincir isteği bir dizide sırayla dolaştırıyordu: dolaşma düz, tek yönlü ve sıra dizinin kendisindeydi. Rota tarafında dolaşılacak yapı düz değil. Bir rota, aktarma noktalarından oluşan bir ağaçtır: merkez, bölge deposu, dağıtım şubesi, teslim noktası. Bu ağaç üzerinde birbirinden bağımsız beş işlem yapılıyor — toplam mesafe, en uzun bekleme, kapasite denetimi, etiket listesi, maliyet toplamı — ve her işlem ağacı kendi gezinme koduyla dolaşıyor.

İki kalıp bu durumun iki ayrı yarısını hedefler. Yineleyici (iterator) gezinmeyi işlemden ayırır: ağacın nasıl dolaşıldığı tek yerde tanımlanır, işlemler yalnızca gelen düğümlerle uğraşır. Ziyaretçi (visitor) düğüm türüne göre değişen davranışı işlemden ayırır: her işlem her tür için bir yöntem taşır ve türün seçimi tek bir yerde yapılır. Ölçülecek sayılar gezinme kodu taşıyan yer sayısı, gezinme sırası değiştiğinde düzenlenen dosya sayısı ve yeni bir düğüm türü eklendiğinde ortaya çıkan hata sayısıdır.

Ağaç ve Beş İşlem

mkdir -p gezinme yineleyici ziyaretci
// agac.mjs — rota agaci: merkez, bolge depolari, dagitim subeleri, teslim noktalari
const d = (tur, ad, mesafe, bekleme, kapasite, alt = []) => ({ tur, ad, mesafe, bekleme, kapasite, alt });

export const ROTA = d("merkez", "MRK", 0, 0, 5000, [
  d("depo", "DPO-A", 120, 4, 1800, [
    d("sube", "SB-A1", 35, 2, 400, [d("nokta", "NK-A1a", 6, 1, 0), d("nokta", "NK-A1b", 9, 3, 0)]),
    d("sube", "SB-A2", 48, 5, 900, [d("nokta", "NK-A2a", 4, 2, 0)]),
  ]),
  d("depo", "DPO-B", 260, 7, 300, [d("sube", "SB-B1", 52, 6, 650, [d("nokta", "NK-B1a", 11, 4, 0)])]),
]);

Birinci sürümde beş işlem beş ayrı özyineleme taşır. Her gövde hem ne hesaplayacağını hem ağacı nasıl dolaşacağını bilir.

// gezinme/islemler.mjs — bes islem, bes ayri gezinme
export function toplamMesafe(d) {
  let t = d.mesafe;
  for (const c of d.alt) t += toplamMesafe(c);
  return t;
}

export function enUzunBekleme(d) {
  let e = d.bekleme;
  for (const c of d.alt) e = Math.max(e, enUzunBekleme(c));
  return e;
}

export function darKapasite(d) {
  let n = d.tur !== "nokta" && d.kapasite < 500 ? 1 : 0;
  for (const c of d.alt) n += darKapasite(c);
  return n;
}

export function etiketler(d) {
  const bas = d.tur === "merkez" ? "M" : d.tur === "depo" ? "D" : d.tur === "sube" ? "S" : "N";
  let liste = [`${bas}:${d.ad}`];
  for (const c of d.alt) liste = liste.concat(etiketler(c));
  return liste;
}

export function maliyet(d) {
  const kat = d.tur === "depo" ? 8 : d.tur === "sube" ? 12 : d.tur === "nokta" ? 20 : 0;
  let t = d.mesafe * kat;
  for (const c of d.alt) t += maliyet(c);
  return t;
}

Gezinmenin Ayrılması

Yineleyici sürümünde gezinme tek bir üreteçtedir. İşlemler artık ağaç görmez, düğüm dizisi görür.

// yineleyici/gez.mjs — gezinme kodu tek yerde, derine arama sirasiyla dugum verir
export function* gez(dugum) {
  yield dugum;
  for (const c of dugum.alt) yield* gez(c);
}
// yineleyici/islemler.mjs — bes islem, hicbirinde gezinme kodu yok
import { gez } from "./gez.mjs";

export function toplamMesafe(kok) {
  let t = 0;
  for (const d of gez(kok)) t += d.mesafe;
  return t;
}

export function enUzunBekleme(kok) {
  let e = 0;
  for (const d of gez(kok)) e = Math.max(e, d.bekleme);
  return e;
}

export function darKapasite(kok) {
  let n = 0;
  for (const d of gez(kok)) {
    switch (d.tur) {
      case "depo":
      case "sube":
        n += d.kapasite < 500 ? 1 : 0;
        break;
      default:
        break;
    }
  }
  return n;
}

export function etiketler(kok) {
  const liste = [];
  for (const d of gez(kok)) {
    switch (d.tur) {
      case "merkez":
        liste.push(`M:${d.ad}`);
        break;
      case "depo":
        liste.push(`D:${d.ad}`);
        break;
      case "sube":
        liste.push(`S:${d.ad}`);
        break;
      default:
        liste.push(`N:${d.ad}`);
        break;
    }
  }
  return liste;
}

export function maliyet(kok) {
  let t = 0;
  for (const d of gez(kok)) {
    switch (d.tur) {
      case "depo":
        t += d.mesafe * 8;
        break;
      case "sube":
        t += d.mesafe * 12;
        break;
      case "nokta":
        t += d.mesafe * 20;
        break;
      default:
        break;
    }
  }
  return t;
}

Gezinme kayboldu, ama tür ayrımı kalmaya devam ediyor: üç işlem hâlâ d.tur üzerinde dallanıyor. Ziyaretçi bu üçünü hedefler. Türün seçimi tek bir yere alınır ve eksik tür sessizce geçilmez.

// ziyaretci/kabul.mjs — dugum kendi turunun yontemini secer, eksikse hata firlatir
export function kabul(dugum, ziyaretci) {
  const yontem = ziyaretci[dugum.tur];
  if (yontem === undefined) throw new TypeError(`${dugum.tur} icin ziyaretci yontemi yok`);
  return yontem(dugum);
}
// ziyaretci/ziyaretciler.mjs — her islem her dugum turu icin bir yontem tasir
export const maliyet = {
  merkez: () => 0,
  depo: (d) => d.mesafe * 8,
  sube: (d) => d.mesafe * 12,
  nokta: (d) => d.mesafe * 20,
};

export const etiket = {
  merkez: (d) => `M:${d.ad}`,
  depo: (d) => `D:${d.ad}`,
  sube: (d) => `S:${d.ad}`,
  nokta: (d) => `N:${d.ad}`,
};

export const darlik = {
  merkez: () => 0,
  depo: (d) => (d.kapasite < 500 ? 1 : 0),
  sube: (d) => (d.kapasite < 500 ? 1 : 0),
  nokta: () => 0,
};

Üç Sürümün Eşitliği

// kosum.mjs — uc surumun ayni agac uzerinde ayni sonucu verdigini dogrular
import { ROTA } from "./agac.mjs";
import * as gezinme from "./gezinme/islemler.mjs";
import * as yineleyici from "./yineleyici/islemler.mjs";
import { gez } from "./yineleyici/gez.mjs";
import { kabul } from "./ziyaretci/kabul.mjs";
import { darlik, etiket, maliyet } from "./ziyaretci/ziyaretciler.mjs";

const ziyaretle = (z, birlestir, baslangic) => {
  let sonuc = baslangic;
  for (const d of gez(ROTA)) sonuc = birlestir(sonuc, kabul(d, z));
  return sonuc;
};

const sonuclar = {
  gezinme: [
    gezinme.toplamMesafe(ROTA),
    gezinme.enUzunBekleme(ROTA),
    gezinme.darKapasite(ROTA),
    gezinme.etiketler(ROTA).join(""),
    gezinme.maliyet(ROTA),
  ],
  yineleyici: [
    yineleyici.toplamMesafe(ROTA),
    yineleyici.enUzunBekleme(ROTA),
    yineleyici.darKapasite(ROTA),
    yineleyici.etiketler(ROTA).join(""),
    yineleyici.maliyet(ROTA),
  ],
};
sonuclar.ziyaretci = [
  sonuclar.yineleyici[0],
  sonuclar.yineleyici[1],
  ziyaretle(darlik, (a, b) => a + b, 0),
  ziyaretle(etiket, (a, b) => a + b, ""),
  ziyaretle(maliyet, (a, b) => a + b, 0),
];

for (const [ad, s] of Object.entries(sonuclar)) {
  console.log(`${ad.padEnd(11)} mesafe=${s[0]} bekleme=${s[1]} dar=${s[2]} maliyet=${s[4]}`);
}
console.log(`etiketler: ${sonuclar.gezinme[3]}`);
const ayrik = new Set(Object.values(sonuclar).map((s) => s.join("|")));
console.log(`farkli sonuc veren surum kumesi = ${ayrik.size}`);
gezinme     mesafe=545 bekleme=7 dar=2 maliyet=5260
yineleyici  mesafe=545 bekleme=7 dar=2 maliyet=5260
ziyaretci   mesafe=545 bekleme=7 dar=2 maliyet=5260
etiketler: M:MRKD:DPO-AS:SB-A1N:NK-A1aN:NK-A1bS:SB-A2N:NK-A2aD:DPO-BS:SB-B1N:NK-B1a
farkli sonuc veren surum kumesi = 1

Ayrık sonuç kümesi bir: üç sürüm aynı beş değeri veriyor, karşılaştırma geçerli.

Gezinme Kodu ve Tür Ayrımı

// sayim.mjs — gezinme kodu tasiyan govde sayisi ve tur ayirt eden yer sayisi
import { readFileSync, readdirSync } from "node:fs";

const say = (metin, kalip) => (metin.match(kalip) ?? []).length;

for (const dizin of ["gezinme", "yineleyici", "ziyaretci"]) {
  let gezinme = 0;
  let turAyirt = 0;
  for (const d of readdirSync(dizin).sort()) {
    const metin = readFileSync(`${dizin}/${d}`, "utf8").replace(/^\/\/.*$/gm, "");
    gezinme += say(metin, /\.alt\b/g);
    turAyirt += say(metin, /\.tur\b|\bswitch\b/g);
  }
  console.log(`${dizin.padEnd(11)} gezinme kodu tasiyan yer=${gezinme}  tur ayirt eden yer=${turAyirt}`);
}
gezinme     gezinme kodu tasiyan yer=5  tur ayirt eden yer=7
yineleyici  gezinme kodu tasiyan yer=1  tur ayirt eden yer=6
ziyaretci   gezinme kodu tasiyan yer=0  tur ayirt eden yer=2

Beş, bir, sıfır. Yineleyici gezinmeyi beş yerden bire indirdi ama tür ayrımına dokunmadı: yedi yerden altıya. Ziyaretçi tür ayrımını ikiye indirdi ve ikisi de aynı dosyadadır. İki kalıp iki ayrı ekseni temizliyor; birbirinin yerine geçmiyorlar.

Gezinme Sırası Değiştiğinde

Etiket listesinin bölge sırasına göre çıkması isteniyor: aynı düzeydeki düğümler bir arada, yani enine arama. Yineleyici sürümünde bu tek dosyalık bir değişikliktir.

// sira.mjs — gezinme sirasi enine aramaya cevrilir; yalniz gez.mjs duzenlenir
import { cpSync, readdirSync, writeFileSync } from "node:fs";
import { ROTA } from "./agac.mjs";

cpSync("yineleyici", "yineleyici-enine", { recursive: true });
writeFileSync(
  "yineleyici-enine/gez.mjs",
  `// yineleyici/gez.mjs — gezinme kodu tek yerde, enine arama sirasiyla dugum verir
export function* gez(dugum) {
  const kuyruk = [dugum];
  while (kuyruk.length > 0) {
    const d = kuyruk.shift();
    yield d;
    for (const c of d.alt) kuyruk.push(c);
  }
}
`,
);

const eski = await import("./yineleyici/islemler.mjs");
const yeni = await import("./yineleyici-enine/islemler.mjs");
console.log(`duzenlenen dosya: yineleyici=1 (${readdirSync("yineleyici").length} dosyadan)  gezinme=5 govde`);
for (const [ad, m] of [["derine", eski], ["enine", yeni]]) {
  console.log(`${ad.padEnd(7)} mesafe=${m.toplamMesafe(ROTA)} bekleme=${m.enUzunBekleme(ROTA)} dar=${m.darKapasite(ROTA)} maliyet=${m.maliyet(ROTA)}`);
  console.log(`${ad.padEnd(7)} etiketler=${m.etiketler(ROTA).join("")}`);
}
duzenlenen dosya: yineleyici=1 (2 dosyadan)  gezinme=5 govde
derine  mesafe=545 bekleme=7 dar=2 maliyet=5260
derine  etiketler=M:MRKD:DPO-AS:SB-A1N:NK-A1aN:NK-A1bS:SB-A2N:NK-A2aD:DPO-BS:SB-B1N:NK-B1a
enine   mesafe=545 bekleme=7 dar=2 maliyet=5260
enine   etiketler=M:MRKD:DPO-AD:DPO-BS:SB-A1S:SB-A2S:SB-B1N:NK-A1aN:NK-A1bN:NK-A2aN:NK-B1a

Bir dosyaya karşı beş gövde. Sıradan bağımsız dört işlemin sonucu değişmedi; sıraya bağlı olan etiket listesi yeni düzeni verdi. Aynı değişikliği gezinme sürümünde yapmak beş özyinelemeyi birer birer kuyruk döngüsüne çevirmek anlamına gelir, çünkü sırayı bilen beş gövde vardır.

Yeni Düğüm Türü Eklendiğinde

Ağaca gümrük aktarma noktası ekleniyor. Beklenen davranış belli: gümrük düğümünün maliyet katsayısı 15, etiket ön eki G, kapasite denetimi diğer aktarma noktaları gibi.

// yenitur.mjs — agaca gumruk dugumu eklenir; iki surumun tepkisi sayilir
import { ROTA } from "./agac.mjs";
import * as yineleyici from "./yineleyici/islemler.mjs";
import { gez } from "./yineleyici/gez.mjs";
import { kabul } from "./ziyaretci/kabul.mjs";
import { darlik, etiket, maliyet } from "./ziyaretci/ziyaretciler.mjs";

const GUMRUK = { tur: "gumruk", ad: "GMR", mesafe: 40, bekleme: 8, kapasite: 200, alt: [] };
ROTA.alt[1].alt.push(GUMRUK);

const BEKLENEN = { dar: 3, maliyet: 5860, etiketGumruk: "G:GMR" };

const ziyaretle = (z, birlestir, baslangic) => {
  let sonuc = baslangic;
  for (const d of gez(ROTA)) sonuc = birlestir(sonuc, kabul(d, z));
  return sonuc;
};

let sessizYanlis = 0;
const y = { dar: yineleyici.darKapasite(ROTA), maliyet: yineleyici.maliyet(ROTA), etiket: yineleyici.etiketler(ROTA).join("") };
if (y.dar !== BEKLENEN.dar) sessizYanlis += 1;
if (y.maliyet !== BEKLENEN.maliyet) sessizYanlis += 1;
if (y.etiket.includes(BEKLENEN.etiketGumruk) === false) sessizYanlis += 1;
console.log(`yineleyici+switch  dar=${y.dar} (beklenen ${BEKLENEN.dar})  maliyet=${y.maliyet} (beklenen ${BEKLENEN.maliyet})  gumruk etiketi=${y.etiket.match(/[A-Z]:GMR/)[0]}`);

let firlatan = 0;
for (const [ad, z, birlestir, bas] of [["darlik", darlik, (a, b) => a + b, 0], ["maliyet", maliyet, (a, b) => a + b, 0], ["etiket", etiket, (a, b) => a + b, ""]]) {
  try {
    ziyaretle(z, birlestir, bas);
    console.log(`ziyaretci/${ad} sessizce tamamlandi`);
  } catch (e) {
    firlatan += 1;
    console.log(`ziyaretci/${ad.padEnd(7)} ${e.constructor.name}: ${e.message}`);
  }
}

console.log(`sessizce yanlis sonuc veren islem: yineleyici+switch=${sessizYanlis}  ziyaretci=0`);
console.log(`eksik turu bildiren islem: yineleyici+switch=0  ziyaretci=${firlatan}`);
console.log(`yeni tur icin duzenlenmesi gereken yer: yineleyici+switch=3 switch govdesi  ziyaretci=3 ziyaretci nesnesi`);
yineleyici+switch  dar=2 (beklenen 3)  maliyet=5260 (beklenen 5860)  gumruk etiketi=N:GMR
ziyaretci/darlik  TypeError: gumruk icin ziyaretci yontemi yok
ziyaretci/maliyet TypeError: gumruk icin ziyaretci yontemi yok
ziyaretci/etiket  TypeError: gumruk icin ziyaretci yontemi yok
sessizce yanlis sonuc veren islem: yineleyici+switch=3  ziyaretci=0
eksik turu bildiren islem: yineleyici+switch=0  ziyaretci=3
yeni tur icin duzenlenmesi gereken yer: yineleyici+switch=3 switch govdesi  ziyaretci=3 ziyaretci nesnesi

Üçe karşı sıfır ve sıfıra karşı üç. Düzenlenmesi gereken yer sayısı ikisinde de üçtür — bu, Programlama Paradigmaları kursunda kurulan ifade problemidir ve ziyaretçi onu çözmez, yalnızca yönünü belirler: yeni işlem ucuz, yeni tür pahalıdır. Kalıbın verdiği şey pahalılığın görünürlüğüdür. Anahtar gövdesindeki default dalı gümrüğü teslim noktası saydı, maliyeti 600 kuruş eksik hesapladı, kapasite denetiminden düşürdü ve hiçbir uyarı üretmedi. Ziyaretçi sürümü üç işlemde de eksik yöntemi adıyla bildirdi.

Bedel iki kalemdir. Birincisi çağrı basamağıdır: bir düğümün maliyetine ulaşmak için kabul üzerinden geçmek gerekir, yani gövdeye giden yol bir çağrı uzar. İkincisi yeni işlemin bedeli: ziyaretçi sürümünde yeni bir işlem, kaç düğüm türü varsa o kadar yöntem yazmayı gerektirir — burada dört. Anahtar gövdesinde default dalı bu zorunluluğu kaldırır, karşılığında sessizliği getirir.

Özet

  • Yineleyici gezinmeyi işlemden ayırır, ziyaretçi tür ayrımını işlemden ayırır; ikisi ayrı ekseni temizler ve birbirinin yerine geçmez.
  • Üç sürüm aynı ağaçta aynı beş değeri üretti; ayrık sonuç kümesi 1 çıktı.
  • Gezinme kodu taşıyan yer sayısı 5’ten 1’e, tür ayırt eden yer sayısı 7’den 2’ye indi.
  • Gezinme sırası enine aramaya çevrilirken yineleyici sürümünde 1 dosya düzenlendi; aynı değişiklik gezinme sürümünde 5 gövdeyi düzenlemeyi gerektirir.
  • Yeni düğüm türü eklendiğinde anahtar gövdeli sürüm 3 işlemde sessizce yanlış sonuç verdi, ziyaretçi sürümü 3 işlemde eksik yöntemi bildirdi; düzenlenmesi gereken yer sayısı ikisinde de 3’tür.
  • Bedel: kabul bir çağrı basamağı ekler ve yeni bir işlem, düğüm türü sayısı kadar yöntem yazmayı gerektirir.

Sonraki Adım

Ziyaretçi bir işlemi ağacın her düğümüne uyguluyor; düğümler birbirini tanımıyor, hepsi gezinmenin verdiği sırayla ele alınıyor. Kitaplığın operatör ekranında ise nesneler birbirini doğrudan tanıyor. Taşıyıcı seçimi, tarife özeti, indirim kutusu ve teslim tarihi alanı birbirini haberdar ediyor: taşıyıcı değişince tarife yenileniyor, tarife değişince indirim yeniden hesaplanıyor, indirim değişince teslim tarihi güncellenebiliyor. Dört alanın her biri diğer üçünü ithal ediyor. Sonraki ders bu karşılıklı bağların sayısını ölçer, bağları tek bir nesnede toplayıp aynı sayıyı yeniden hesaplar ve düzenleme oturumunun durumunu kapsüllemeyi bozmadan saklamanın maliyetini ayrıca sayar.

İ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