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:
kabulbir ç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.