Ders 09 / 30
Bileşik
Ağaç yapılarının tek biçim ele alınması: konsolide gönderi ağacında dört hesabın tür sınamasını her gövdede tekrarladığı düzen ile yaprak ve düğümü aynı arayüzün arkasına koyan düzenin sınama sayısıyla karşılaştırılması, yeni düğüm türü ve yeni işlem eklendiğinde dokunulan dosyanın ölçülmesi, şeffaflık ile güvenlik arasındaki seçim.
İçindekiler
Köprü iki bağımsız ekseni ayırdı, ama raporladığı gönderi listesi düzdü: altı gönderi, hepsi aynı düzeyde. Alan bunu bozar. Bir gönderi, içinde başka gönderiler taşıyan konsolide bir gönderi olabilir; onun parçalarından biri yine konsolide olabilir. Ağırlık toplamı, ücret, teslim süresi ve parça sayısı artık bir ağaç üzerinde hesaplanır.
Sorun, her hesabın “bu tek parça mı, konsolide mi” sorusunu kendi gövdesinde sormasıdır. Dört hesap varsa aynı tür sınaması dört kez yazılır ve beşinci hesap beşinci kopyayı getirir. Bileşik (composite) kalıbı yaprağı ve düğümü aynı arayüzün arkasına koyarak sınamayı hesaplardan çıkarır. Ölçü tür sınaması sayısıdır; kalıp iki genişleme yönünde ayrı ayrı sınanacaktır — yeni bir düğüm türü ve yeni bir işlem.
Problem: Ağacın Şekli Her Hesaba Sızıyor
Ağaç ve tarife iki düzende de ortaktır. Konsolide düğümün kendi ambalaj ağırlığı, yaprağın posta koduna göre kademe ücreti ve bölgeye göre teslim günü vardır.
// agac.mjs — iki duzenin de hesapladigi gonderi agaci ve ortak tarife const KADEME = [[1, 4990], [5, 8490], [15, 14990], [30, 24990]]; const KATSAYI = { 34: 100, "06": 115, 35: 120, 65: 145 }; const GUN = { 34: 1, "06": 2, 35: 2, 65: 4 }; export const kademeUcret = (agirlik, postaKodu) => { const k = KADEME.find(([ust]) => agirlik <= ust) ?? [0, 24990]; return Math.round((k[1] * (KATSAYI[postaKodu.slice(0, 2)] ?? 165)) / 100); }; export const bolgeGun = (postaKodu) => GUN[postaKodu.slice(0, 2)] ?? 5; export const AGAC = { tip: "konsolide", kod: "K-1", ambalaj: 0.5, parcalar: [ { tip: "koli", kod: "G-1", agirlik: 0.8, postaKodu: "34100" }, { tip: "konsolide", kod: "K-2", ambalaj: 0.3, parcalar: [ { tip: "koli", kod: "G-2", agirlik: 12, postaKodu: "06500" }, { tip: "koli", kod: "G-3", agirlik: 3, postaKodu: "35400" }, ] }, { tip: "koli", kod: "G-4", agirlik: 18, postaKodu: "06800" }, ], };
İlk düzende dört hesap dört ayrı dosyadır ve dördü de aynı sınamayla başlar.
// sinama/agirlik.mjs — her islem tur sinamasini kendi govdesinde tekrarlar export function agirlik(g) { if (g.tip === "konsolide") return g.ambalaj + g.parcalar.reduce((t, p) => t + agirlik(p), 0); return g.agirlik; }
// sinama/ucret.mjs — konsolide gonderide uc parcadan itibaren yuzde bes hacim indirimi import { kademeUcret } from "../agac.mjs"; export function ucret(g) { if (g.tip === "konsolide") { const toplam = g.parcalar.reduce((t, p) => t + ucret(p), 0); return g.parcalar.length >= 3 ? Math.round(toplam * 0.95) : toplam; } return kademeUcret(g.agirlik, g.postaKodu); }
// sinama/sure.mjs — konsolide gonderi en yavas parcasi kadar, arti bir aktarma gunu import { bolgeGun } from "../agac.mjs"; export function sure(g) { if (g.tip === "konsolide") return Math.max(...g.parcalar.map(sure)) + 1; return bolgeGun(g.postaKodu); }
// sinama/parca.mjs — agactaki yaprak sayisi export function parcaSayisi(g) { if (g.tip === "konsolide") return g.parcalar.reduce((t, p) => t + parcaSayisi(p), 0); return 1; }
Dört dosyanın dördü de iki şeyi birden biliyor: hesabın kendisini ve ağacın nasıl gezildiğini. İkinci bilgi dördünde de aynıdır.
Çözüm: Yaprak ile Düğüm Aynı Arayüzde
Bileşiğin çözümü, ağacı gezme bilgisini düğümün kendisine vermektir. Ortak arayüz dört yöntemden oluşur ve yaprak da düğüm de bu dördünü karşılar.
// bilesik/koli.mjs — yaprak: dort islemi de kendisi yanitlar import { kademeUcret, bolgeGun } from "../agac.mjs"; export class Koli { constructor({ kod, agirlik, postaKodu }) { this.kod = kod; this.kilo = agirlik; this.postaKodu = postaKodu; } agirlik() { return this.kilo; } ucret() { return kademeUcret(this.kilo, this.postaKodu); } sure() { return bolgeGun(this.postaKodu); } parcaSayisi() { return 1; } }
// bilesik/konsolide.mjs — dugum: parcalarina ayni arayuzle sorar, turlerini bilmez export class Konsolide { constructor({ kod, ambalaj, parcalar }) { this.kod = kod; this.ambalaj = ambalaj; this.parcalar = parcalar; } agirlik() { return this.ambalaj + this.parcalar.reduce((t, p) => t + p.agirlik(), 0); } ucret() { const toplam = this.parcalar.reduce((t, p) => t + p.ucret(), 0); return this.parcalar.length >= 3 ? Math.round(toplam * 0.95) : toplam; } sure() { return Math.max(...this.parcalar.map((p) => p.sure())) + 1; } parcaSayisi() { return this.parcalar.reduce((t, p) => t + p.parcaSayisi(), 0); } }
Düğümün gövdesinde p için hiçbir tür sınaması yok: parça yaprak da olabilir, başka bir
konsolide de. Sınama tek bir yere çekilmiştir — veriden nesne ağacını kuran yere.
// bilesik/kur.mjs — tur sinamasinin kaldigi tek yer: veriden nesne agacini kurar import { Koli } from "./koli.mjs"; import { Konsolide } from "./konsolide.mjs"; const DUGUM = { konsolide: Konsolide }; export const kur = (veri) => veri.tip === "koli" ? new Koli(veri) : new DUGUM[veri.tip]({ ...veri, parcalar: veri.parcalar.map(kur) });
// kosum.mjs — iki duzen ayni agac icin ayni dort sonucu veriyor mu import { AGAC } from "./agac.mjs"; import { agirlik } from "./sinama/agirlik.mjs"; import { ucret } from "./sinama/ucret.mjs"; import { sure } from "./sinama/sure.mjs"; import { parcaSayisi } from "./sinama/parca.mjs"; import { kur } from "./bilesik/kur.mjs"; const n = kur(AGAC); const a = [agirlik(AGAC).toFixed(2), ucret(AGAC), sure(AGAC), parcaSayisi(AGAC)]; const b = [n.agirlik().toFixed(2), n.ucret(), n.sure(), n.parcaSayisi()]; console.log(`sinama : agirlik=${a[0]} kg ucret=${a[1]} sure=${a[2]} gun parca=${a[3]}`); console.log(`bilesik: agirlik=${b[0]} kg ucret=${b[1]} sure=${b[2]} gun parca=${b[3]}`); console.log(`ayrilan sonuc = ${a.filter((v, i) => String(v) !== String(b[i])).length} / 4`);
sinama : agirlik=34.60 kg ucret=58098 sure=4 gun parca=4 bilesik: agirlik=34.60 kg ucret=58098 sure=4 gun parca=4 ayrilan sonuc = 0 / 4
İki düzey derinlikte, üç yaprak ve bir iç düğümle dört hesabın dördü aynı. Fark sınamaların sayısında.
// olc.mjs — tur sinamasi sayisini ve agacin ic seklini bilen dosya sayisini olcer import { readdirSync, readFileSync } from "node:fs"; for (const dizin of ["sinama", "bilesik"]) { const dosyalar = readdirSync(dizin).filter((d) => d.endsWith(".mjs")).sort(); let sinama = 0, sekil = 0, satir = 0; for (const d of dosyalar) { const metin = readFileSync(`${dizin}/${d}`, "utf8"); sinama += (metin.match(/tip === /g) ?? []).length; if (/\bparcalar\b/.test(metin)) sekil += 1; satir += metin.split("\n").filter((s) => s.trim() !== "" && !s.trim().startsWith("//")).length; } console.log(`${dizin.padEnd(8)} dosya=${dosyalar.length} tur sinamasi=${sinama} ` + `agac seklini bilen dosya=${sekil} satir=${satir}`); }
sinama dosya=4 tur sinamasi=4 agac seklini bilen dosya=4 satir=21 bilesik dosya=3 tur sinamasi=1 agac seklini bilen dosya=2 satir=32
Tür sınaması 4’ten 1’e indi; ağacın iç şeklini (parcalar alanını) bilen dosya sayısı 4’ten
2’ye. Bileşik düzen 11 satır daha uzun: kalıbın peşin bedeli bu.
Yeni Bir Düğüm Türü Eklenince
İlk genişleme yönü yeni bir düğüm türüdür. Palet, konsolideden üç noktada ayrılır: 25 kg dara ağırlığı, sabit 3500 kuruş palet ücreti ve iki günlük aktarma. Ağaca bir palet düğümü giriyor.
mkdir -p yeni-tur && cp -r agac.mjs sinama bilesik kosum.mjs olc.mjs yeni-tur/ ls yeni-tur
agac.mjs bilesik kosum.mjs olc.mjs sinama
// yeni-tur/bilesik/palet.mjs — ikinci dugum turu: dara agirligi, sabit palet ucreti, iki gun aktarma export class Palet { constructor({ kod, parcalar }) { this.kod = kod; this.parcalar = parcalar; } agirlik() { return 25 + this.parcalar.reduce((t, p) => t + p.agirlik(), 0); } ucret() { return 3500 + this.parcalar.reduce((t, p) => t + p.ucret(), 0); } sure() { return Math.max(...this.parcalar.map((p) => p.sure())) + 2; } parcaSayisi() { return this.parcalar.reduce((t, p) => t + p.parcaSayisi(), 0); } }
Sınama düzeninde ise dört hesabın dördüne birer dal girer. Aşağıdaki betik ağaca paleti koyar, dört dosyayı da düzenler, düğüm kaydına yeni türü yazar ve iki düzeni sayar.
cd yeni-tur sed -i.y 's# { tip: "koli", kod: "G-4", agirlik: 18, postaKodu: "06800" },# { tip: "palet", kod: "P-1", parcalar: [\n { tip: "koli", kod: "G-4", agirlik: 18, postaKodu: "06800" },\n { tip: "koli", kod: "G-5", agirlik: 6, postaKodu: "65100" },\n ] },#' agac.mjs sed -i.y 's#^ if (g.tip === "konsolide") return g.ambalaj# if (g.tip === "palet") return 25 + g.parcalar.reduce((t, p) => t + agirlik(p), 0);\n&#' sinama/agirlik.mjs sed -i.y 's#^ if (g.tip === "konsolide") {# if (g.tip === "palet") return 3500 + g.parcalar.reduce((t, p) => t + ucret(p), 0);\n&#' sinama/ucret.mjs sed -i.y 's#^ if (g.tip === "konsolide") return Math.max# if (g.tip === "palet") return Math.max(...g.parcalar.map(sure)) + 2;\n&#' sinama/sure.mjs sed -i.y 's#^ if (g.tip === "konsolide") return g.parcalar.reduce# if (g.tip === "palet") return g.parcalar.reduce((t, p) => t + parcaSayisi(p), 0);\n&#' sinama/parca.mjs sed -i.y -e 's#^import { Konsolide } from "./konsolide.mjs";#&\nimport { Palet } from "./palet.mjs";#' \ -e 's#^const DUGUM = { konsolide: Konsolide };#const DUGUM = { konsolide: Konsolide, palet: Palet };#' bilesik/kur.mjs rm -f *.y sinama/*.y bilesik/*.y node kosum.mjs node olc.mjs for d in sinama bilesik; do echo "$d: duzenlenen dosya=$(diff -rq "../$d" "$d" | grep -c '^Files ') yeni dosya=$(diff -rq "../$d" "$d" | grep -c '^Only in ') eklenen satir=$(diff -rN "../$d" "$d" | grep '^>' | grep -cvE '^> *(//|$)')" done
sinama : agirlik=65.60 kg ucret=82072 sure=7 gun parca=5 bilesik: agirlik=65.60 kg ucret=82072 sure=7 gun parca=5 ayrilan sonuc = 0 / 4 sinama dosya=4 tur sinamasi=8 agac seklini bilen dosya=4 satir=25 bilesik dosya=4 tur sinamasi=1 agac seklini bilen dosya=3 satir=43 sinama: duzenlenen dosya=4 yeni dosya=0 eklenen satir=4 bilesik: duzenlenen dosya=1 yeni dosya=1 eklenen satir=12
Sınama düzeninde dört var olan dosya düzenlendi ve tür sınaması 4’ten 8’e çıktı; bileşik düzende bir dosya düzenlendi (düğüm kaydı) ve sınama sayısı 1’de kaldı. Düzenlenen dört gövde daha önce doğru çalışan iki türün mantığını da taşıyordu; eklenen 12 satır ise yeni bir dosyada duruyor. Sınama düzeninin satır maliyeti düşük (4’e 12) çünkü her dosyaya tek satır girdi — fakat o tek satırların dördü de var olan bir koşul zincirinin içine girdi.
Bir ayrıntı sınama düzeninin gizli riskini gösteriyor: parca.mjs’nin palet dalı,
konsolide dalıyla birebir aynıdır. Bu dosyada sınamayı g.tip !== "koli" biçimine çevirmek
daha kısa görünür, ama üçüncü bir düğüm türü geldiğinde o dosya sessizce yanlış yanıt verir.
Bileşik düzende böyle bir kısayol yoktur, çünkü yanıtı veren şey türün kendisidir.
Yeni Bir İşlem Eklenince
İkinci genişleme yönü tersidir: yeni bir işlem, en ağır yaprağın kodu. Ağaç değişmediği için temel sürüm yeniden kopyalanıyor.
mkdir -p yeni-islem && cp -r agac.mjs sinama bilesik olc.mjs yeni-islem/ ls yeni-islem/sinama
agirlik.mjs parca.mjs sure.mjs ucret.mjs
// yeni-islem/sinama/en-agir.mjs — besinci islem: en agir yaprak; tur sinamasi bir kez daha export function enAgirParca(g) { if (g.tip === "konsolide") { return g.parcalar.map(enAgirParca).reduce((a, b) => (b[1] > a[1] ? b : a)); } return [g.kod, g.agirlik]; }
cd yeni-islem sed -i.y 's#^ parcaSayisi() { return 1; }#&\n enAgirParca() { return [this.kod, this.kilo]; }#' bilesik/koli.mjs sed -i.y 's#^ parcaSayisi() { return this.parcalar.reduce((t, p) => t + p.parcaSayisi(), 0); }#&\n enAgirParca() {\n return this.parcalar.map((p) => p.enAgirParca()).reduce((a, b) => (b[1] > a[1] ? b : a));\n }#' bilesik/konsolide.mjs rm -f bilesik/*.y cat > kosum2.mjs <<'KOSUM' // yeni-islem/kosum2.mjs — besinci islem iki duzende ayni yapragi buluyor mu import { AGAC } from "./agac.mjs"; import { enAgirParca } from "./sinama/en-agir.mjs"; import { kur } from "./bilesik/kur.mjs"; console.log(`sinama : en agir = ${enAgirParca(AGAC).join(" ")}`); console.log(`bilesik: en agir = ${kur(AGAC).enAgirParca().join(" ")}`); KOSUM node kosum2.mjs for d in sinama bilesik; do echo "$d: duzenlenen dosya=$(diff -rq "../$d" "$d" | grep -c '^Files ') yeni dosya=$(diff -rq "../$d" "$d" | grep -c '^Only in ') eklenen satir=$(diff -rN "../$d" "$d" | grep '^>' | grep -cvE '^> *(//|$)')" done
sinama : en agir = G-4 18 bilesik: en agir = G-4 18 sinama: duzenlenen dosya=0 yeni dosya=1 eklenen satir=6 bilesik: duzenlenen dosya=2 yeni dosya=0 eklenen satir=4
Yön tersine döndü: yeni işlem sınama düzeninde hiçbir var olan dosyaya dokunmadı, bileşik düzende iki sınıfın ikisi de düzenlendi. Üç düğüm türü olsa üç sınıf düzenlenecekti. Bu, Programlama Paradigmaları kursundaki Paradigma Karışımı dersinde tanımlanan ifade problemidir: bileşik yeni tür eklemeyi ucuzlatır (4 düzenlenen dosya → 1), yeni işlem eklemeyi pahalılaştırır (0 düzenlenen dosya → 2). Kalıp seçimi, hangi eksenin daha sık büyüdüğü ölçüsüne dayanır ve o ölçünün kaynağı değişiklik günlüğüdür.
Bedel ve Ne Zaman Uygulanmayacağı
Bileşiğin üç bedeli vardır. Birincisi peşin satır maliyetidir: 21 satıra karşı 32 satır ve
iki yeni tip. İkincisi, işlem ekseninde ödenen bedeldir — her yeni işlem bütün düğüm
türlerinin gövdesine dokunur. Üçüncüsü arayüzün kapsamıdır. Buradaki ortak arayüz dört
sorgudan oluşuyor; içine ekle gibi bir düğüm işlemi konmadı. Konsaydı yaprak onu ya sessizce
yok sayacak ya hata atacaktı: şeffaflık (yaprakla düğüm birbirinden ayırt edilemez)
ile güvenlik (yaprakta anlamsız yöntem bulunmaz) arasında seçim yapılır ve burada güvenlik
seçildi. Bunun sonucu, ağacı değiştiren kodun düğüm türünü bilmek zorunda kalmasıdır.
Kalıp iki durumda karşılığını vermez. Derinlik sabit ve birse — gönderi listesi hep tek düzeyse — özyineleme yoktur, tür sınaması da yoktur; bileşik yalnız iki sınıf ekler. İşlem sayısı düğüm türü sayısından çok daha hızlı büyüyorsa ifade probleminin pahalı tarafına düşülür; o durumda ağacı tek biçim gezen bir gezinti yordamı ile hesapları ayrı tutmak daha az dosya düzenletir.
Özet
- Bileşiğin çözdüğü problem, ağaç üzerindeki her hesabın “yaprak mı, düğüm mü” sınamasını kendi gövdesinde tekrarlamasıdır; çözüm yaprakla düğümü aynı arayüzün arkasına koymaktır.
- Dört hesap için tür sınaması 4’ten 1’e, ağacın iç şeklini bilen dosya sayısı 4’ten 2’ye indi; bedel 11 satır ve iki yeni tiptir.
- Yeni düğüm türü (palet) sınama düzeninde 4 var olan dosyayı düzenletti ve sınama sayısını 8’e çıkardı; bileşik düzende 1 dosya düzenlendi, 1 yeni dosya eklendi, sınama sayısı 1’de kaldı ve dört hesabın sonucu iki düzende de aynı çıktı.
- Yeni işlem (en ağır yaprak) tersini gösterdi: sınama düzeninde 0, bileşik düzende 2 var olan dosya düzenlendi — ifade probleminin iki yönü.
- Ortak arayüze ağacı değiştiren yöntemler konmadığı için yaprakta anlamsız yöntem yok; şeffaflık yerine güvenlik seçildi. Derinlik sabitse ya da işlem eksenindeki büyüme baskınsa kalıp karşılığını vermez.
Sonraki Adım
Bileşikte düğüm, parçalarına kendisiyle aynı arayüzle sorar; sarılan şey bir kümedir ve sarma işlemi ağacı kurar. Bir sonraki soru aynı sarma tekniğini tek bir nesne üzerinde ve başka bir amaçla kullanır: gönderinin ücretine sırayla yakıt farkı, sigorta, sözleşmeli müşteri indirimi ve vergi eklenmesi gerekiyor. Bu dört ek her gönderide istenmez, birbirinden bağımsız açılıp kapanır ve sırası sonucu değiştirir. Seçeneklerin her bileşimi bir alt sınıf olursa sınıf sayısı seçenek sayısının ikinin kuvveti kadar büyür. Sonraki ders bu sayıyı dört seçenek için ölçer, ücreti aynı arayüzle sarıp yeni davranışı katmanlayan dekoratör kalıbını yazar ve bedelini izlenmesi gereken çağrı sayısıyla sıra duyarlılığıyla birlikte sayar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.