İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat