İçeriğe geç
academia.sh

Ders 08 / 30

Köprü

Soyutlama ile gerçekleştirimin ayrı evrimi: rapor türü ve çıktı biçimi eksenlerinin kalıtımla çarpıldığı düzen ile iki hiyerarşiye bölündüğü düzenin tip, satır ve çıktı gövdesi sayısıyla karşılaştırılması, üçüncü biçim eklendiğinde eklenen tip ve satırın ölçülmesi, çarpımın toplama inmesinin bedeli.

İçindekiler

Bağdaştırıcı var olan iki arayüzün arasına sonradan girdi: sağlayıcılar da kitaplığın sözleşmesi de yazılmıştı, kalıp ikisini uzlaştırdı. Bu dersin problemi tasarım aşamasında, henüz hiçbir şey yazılmamışken belirir: bir yetenek iki bağımsız eksende birden değişiyor.

Kitaplık ücret raporu üretecek. Raporun türü bir eksendir — özet, ağırlık kademesi dökümü, bölge dökümü — ve iş kuralı değiştikçe yeni tür istenir. Raporun çıktı biçimi ikinci eksendir: düz metin, ayrılmış değer, hizalanmış tablo. İki eksen birbirinden habersiz büyür; hiçbir biçim belirli bir türe bağlı değildir. Köprü (bridge) kalıbı bu iki ekseni ayrı hiyerarşilere böler ve aralarına bir sözleşme koyar. Kazanç tip sayısında ölçülür: kalıtımla birleştirmede tür sayısı ile biçim sayısının çarpımı, köprüde toplamı.

Problem: Aynı Veri, Ayrı Biçim

Raporlanan gönderi kümesi ve gruplama yardımcısı iki düzende de ortaktır; karşılaştırılan eksenler bunlar değildir.

// veri.mjs — iki duzenin de raporladigi gonderi kumesi; tutar kurustur
const KADEME = [[1, 4990], [5, 8490], [15, 14990], [30, 24990]];
const KATSAYI = { 34: 100, "06": 115, 35: 120, 65: 145 };
export const BOLGE = { 34: "yakin", "06": "orta", 35: "orta", 65: "uzak" };

const tutar = (agirlik, postaKodu) => {
  const kademe = KADEME.find(([ust]) => agirlik <= ust) ?? [0, 24990];
  return Math.round((kademe[1] * (KATSAYI[postaKodu.slice(0, 2)] ?? 165)) / 100);
};

export const GONDERILER = [
  ["G-1", 0.8, "34100"], ["G-2", 12, "06500"], ["G-3", 26, "65100"],
  ["G-4", 3, "35400"], ["G-5", 4.5, "34710"], ["G-6", 18, "06800"],
].map(([kod, agirlik, postaKodu]) => ({ kod, agirlik, postaKodu, tutar: tutar(agirlik, postaKodu) }));

export const KADEME_SIRA = ["0-1 kg", "1-5 kg", "5-15 kg", "15+ kg"];

export const kademeAdi = (agirlik) =>
  agirlik <= 1 ? "0-1 kg" : agirlik <= 5 ? "1-5 kg" : agirlik <= 15 ? "5-15 kg" : "15+ kg";

export const grupla = (gonderiler, anahtar, sira = null) => {
  const kume = new Map();
  for (const g of gonderiler) {
    const a = anahtar(g), v = kume.get(a) ?? [0, 0];
    kume.set(a, [v[0] + 1, v[1] + g.tutar]);
  }
  const satirlar = [...kume].map(([a, v]) => [a, String(v[0]), String(v[1])]);
  return sira === null ? satirlar.sort() : satirlar.sort((x, y) => sira.indexOf(x[0]) - sira.indexOf(y[0]));
};

Çarpımın Sınıflara Açıldığı Düzen

İlk düzende her tür–biçim çifti bir yaprak sınıftır. Veri çıkarımı ara sınıflarda toplanır, çıktıyı üreten yaz yöntemi yapraklara düşer.

// kalitim/raporlar.mjs — her tur-bicim cifti icin bir yaprak sinif: 3 x 2 = 6
import { BOLGE, KADEME_SIRA, grupla, kademeAdi } from "../veri.mjs";

class Rapor {
  constructor(gonderiler) { this.gonderiler = gonderiler; }
}

export class OzetRapor extends Rapor {
  baslik() { return "ozet"; }
  sutunlar() { return ["olcut", "deger"]; }
  satirlar() {
    const t = this.gonderiler.reduce((s, g) => s + g.tutar, 0);
    return [["gonderi", String(this.gonderiler.length)], ["toplam", String(t)],
      ["ortalama", String(Math.round(t / this.gonderiler.length))]];
  }
}
export class OzetMetin extends OzetRapor {
  yaz() {
    return [this.baslik(), this.sutunlar().join("  "),
      ...this.satirlar().map((s) => s.join("  "))].join("\n");
  }
}
export class OzetAyrilmis extends OzetRapor {
  yaz() {
    return [this.sutunlar().join(";"), ...this.satirlar().map((s) => s.join(";"))].join("\n");
  }
}

export class KademeRapor extends Rapor {
  baslik() { return "kademe dokumu"; }
  sutunlar() { return ["kademe", "adet", "tutar"]; }
  satirlar() { return grupla(this.gonderiler, (g) => kademeAdi(g.agirlik), KADEME_SIRA); }
}
export class KademeMetin extends KademeRapor {
  yaz() {
    return [this.baslik(), this.sutunlar().join("  "),
      ...this.satirlar().map((s) => s.join("  "))].join("\n");
  }
}
export class KademeAyrilmis extends KademeRapor {
  yaz() {
    return [this.sutunlar().join(";"), ...this.satirlar().map((s) => s.join(";"))].join("\n");
  }
}

export class BolgeRapor extends Rapor {
  baslik() { return "bolge dokumu"; }
  sutunlar() { return ["bolge", "adet", "tutar"]; }
  satirlar() { return grupla(this.gonderiler, (g) => BOLGE[g.postaKodu.slice(0, 2)] ?? "bilinmeyen"); }
}
export class BolgeMetin extends BolgeRapor {
  yaz() {
    return [this.baslik(), this.sutunlar().join("  "),
      ...this.satirlar().map((s) => s.join("  "))].join("\n");
  }
}
export class BolgeAyrilmis extends BolgeRapor {
  yaz() {
    return [this.sutunlar().join(";"), ...this.satirlar().map((s) => s.join(";"))].join("\n");
  }
}

Altı yaprağın gövdeleri ikişer ikişer birebir aynıdır. Bu, Temiz Kod kursundaki bilgi yinelemesinin tam örneğidir: ayrılmış değer biçiminin nasıl üretildiği bilgisi üç ayrı sınıfta duruyor. Çağıran taraf çifti bir ada çevirir.

// kalitim/kur.mjs — bilesim koku: tur-bicim cifti dogrudan bir yaprak sinifa esler
import { OzetMetin, OzetAyrilmis, KademeMetin, KademeAyrilmis, BolgeMetin, BolgeAyrilmis } from "./raporlar.mjs";

const YAPRAK = {
  "ozet/metin": OzetMetin, "ozet/ayrilmis": OzetAyrilmis,
  "kademe/metin": KademeMetin, "kademe/ayrilmis": KademeAyrilmis,
  "bolge/metin": BolgeMetin, "bolge/ayrilmis": BolgeAyrilmis,
};

export const rapor = (tur, bicim, gonderiler) => new YAPRAK[`${tur}/${bicim}`](gonderiler);

Çözüm: İki Hiyerarşi, Aralarında Bir Sözleşme

Köprünün çözümü, ikinci ekseni kalıtım ağacından çıkarıp kendi ağacına koymak ve soyutlamanın gerçekleştirime bir başvuru tutmasıdır. Sözleşme üç veriden oluşur: başlık, sütun adları, satırlar.

// kopru/bicimler.mjs — gerceklestirim hiyerarsisi: cikti bicimi burada, rapor turu bilinmez
export const metinBicimi = {
  uret(baslik, sutunlar, satirlar) {
    return [baslik, sutunlar.join("  "), ...satirlar.map((s) => s.join("  "))].join("\n");
  },
};

export const ayrilmisBicimi = {
  uret(baslik, sutunlar, satirlar) {
    return [sutunlar.join(";"), ...satirlar.map((s) => s.join(";"))].join("\n");
  },
};
// kopru/raporlar.mjs — soyutlama hiyerarsisi: veri cikarimi burada, cikti bicimi bicim nesnesinde
import { BOLGE, KADEME_SIRA, grupla, kademeAdi } from "../veri.mjs";

class Rapor {
  constructor(gonderiler, bicim) {
    this.gonderiler = gonderiler;
    this.bicim = bicim;
  }
  yaz() { return this.bicim.uret(this.baslik(), this.sutunlar(), this.satirlar()); }
}

export class OzetRapor extends Rapor {
  baslik() { return "ozet"; }
  sutunlar() { return ["olcut", "deger"]; }
  satirlar() {
    const t = this.gonderiler.reduce((s, g) => s + g.tutar, 0);
    return [["gonderi", String(this.gonderiler.length)], ["toplam", String(t)],
      ["ortalama", String(Math.round(t / this.gonderiler.length))]];
  }
}

export class KademeRapor extends Rapor {
  baslik() { return "kademe dokumu"; }
  sutunlar() { return ["kademe", "adet", "tutar"]; }
  satirlar() { return grupla(this.gonderiler, (g) => kademeAdi(g.agirlik), KADEME_SIRA); }
}

export class BolgeRapor extends Rapor {
  baslik() { return "bolge dokumu"; }
  sutunlar() { return ["bolge", "adet", "tutar"]; }
  satirlar() { return grupla(this.gonderiler, (g) => BOLGE[g.postaKodu.slice(0, 2)] ?? "bilinmeyen"); }
}

Yaprak sınıf kalmadı: çift artık bir sınıf adı değil, iki nesnenin bileşimidir ve bileşim kökünde kurulur.

// kopru/kur.mjs — bilesim koku: iki eksen burada birlestirilir
import { OzetRapor, KademeRapor, BolgeRapor } from "./raporlar.mjs";
import { metinBicimi, ayrilmisBicimi } from "./bicimler.mjs";

const RAPOR = { ozet: OzetRapor, kademe: KademeRapor, bolge: BolgeRapor };
const BICIM = { metin: metinBicimi, ayrilmis: ayrilmisBicimi };

export const rapor = (tur, bicim, gonderiler) => new RAPOR[tur](gonderiler, BICIM[bicim]);
// kosum.mjs — iki duzen alti tur-bicim ciftinde ayni metni uretiyor mu
import { GONDERILER } from "./veri.mjs";
import { rapor as kalitimRapor } from "./kalitim/kur.mjs";
import { rapor as kopruRapor } from "./kopru/kur.mjs";

let ayrilan = 0;
for (const tur of ["ozet", "kademe", "bolge"]) {
  for (const bicim of ["metin", "ayrilmis"]) {
    if (kalitimRapor(tur, bicim, GONDERILER).yaz() !== kopruRapor(tur, bicim, GONDERILER).yaz()) ayrilan += 1;
  }
}
console.log(kalitimRapor("bolge", "metin", GONDERILER).yaz());
console.log("--");
console.log(kopruRapor("bolge", "ayrilmis", GONDERILER).yaz());
console.log(`ayrilan cift = ${ayrilan} / 6`);
bolge dokumu
bolge  adet  tutar
orta  3  56166
uzak  1  36236
yakin  2  13480
--
bolge;adet;tutar
orta;3;56166
uzak;1;36236
yakin;2;13480
ayrilan cift = 0 / 6

Altı çiftin altısında aynı metin. Fark çıktıda değil, çıktının kaç tipe bölündüğünde.

Tip ve Gövde Sayısının Ölçülmesi

// tip-say.mjs — iki duzendeki tip sayisini, satir sayisini ve cikti govdesi sayisini sayar
import { readdirSync, readFileSync } from "node:fs";

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

for (const dizin of ["kalitim", "kopru"]) {
  let tip = 0, govde = 0, satir = 0;
  for (const d of readdirSync(dizin).sort()) {
    const metin = readFileSync(`${dizin}/${d}`, "utf8");
    tip += say(metin, /^(?:export )?class \w+/gm) + say(metin, /^export const \w+Bicimi/gm);
    govde += say(metin, /^  yaz\(\)/gm) + say(metin, /^  uret\(/gm);
    satir += metin.split("\n").filter((s) => s.trim() !== "" && !s.trim().startsWith("//")).length;
  }
  console.log(`${dizin.padEnd(8)} tip=${String(tip).padStart(2)}  satir=${String(satir).padStart(3)}  cikti govdesi=${govde}`);
}
kalitim  tip=10  satir= 63  cikti govdesi=6
kopru    tip= 6  satir= 42  cikti govdesi=3

Üç tür ve iki biçim için kalıtım düzeni 10 tip taşıyor: bir taban, üç ara sınıf, altı yaprak. Köprü düzeni 6 tip taşıyor: bir taban, üç rapor, iki biçim nesnesi. Çıktı gövdesi sayısı 6’ya 3 — köprüdeki üçün biri devir gövdesi (yaz), ikisi biçimin kendisi. Aynı biçim gövdesi kalıtım düzeninde tür sayısı kadar, yani 3 kez yazılmıştır.

Üçüncü Biçim Eklenince

Yeni gereksinim hizalanmış tablodur: sütunlar en geniş değere göre doldurulur. Bu, ikinci eksende bir eklemedir; hiçbir rapor türünün veri çıkarımını ilgilendirmez.

mkdir -p yeni && cp -r veri.mjs kalitim kopru kosum.mjs tip-say.mjs yeni/
ls yeni
kalitim
kopru
kosum.mjs
tip-say.mjs
veri.mjs

Kalıtım düzeninde biçim gövdesinin üç kez yazılmasını önlemenin yolu onu bir yardımcıya çıkarmaktır — ve o yardımcıya bir ad ile bir sözleşme verildiği anda köprünün gerçekleştirim tarafı yazılmış olur. Fark, yaprak sınıfların yine gerekmesidir.

// yeni/kalitim/hizali.mjs — ucuncu bicim: govde bir kez yazildi ama uc yaprak sinif gerekti
import { OzetRapor, KademeRapor, BolgeRapor } from "./raporlar.mjs";

const hizala = (baslik, sutunlar, satirlar) => {
  const en = sutunlar.map((s, i) => Math.max(s.length, ...satirlar.map((r) => r[i].length)));
  const satir = (r) => r.map((h, i) => h.padEnd(en[i])).join(" | ").trimEnd();
  return [baslik, satir(sutunlar), ...satirlar.map(satir)].join("\n");
};

export class OzetHizali extends OzetRapor {
  yaz() { return hizala(this.baslik(), this.sutunlar(), this.satirlar()); }
}
export class KademeHizali extends KademeRapor {
  yaz() { return hizala(this.baslik(), this.sutunlar(), this.satirlar()); }
}
export class BolgeHizali extends BolgeRapor {
  yaz() { return hizala(this.baslik(), this.sutunlar(), this.satirlar()); }
}

Köprü düzeninde eklenen şey gerçekleştirim hiyerarşisine bir nesnedir.

cat >> yeni/kopru/bicimler.mjs <<'BICIM'

export const hizaliBicimi = {
  uret(baslik, sutunlar, satirlar) {
    const en = sutunlar.map((s, i) => Math.max(s.length, ...satirlar.map((r) => r[i].length)));
    const satir = (r) => r.map((h, i) => h.padEnd(en[i])).join(" | ").trimEnd();
    return [baslik, satir(sutunlar), ...satirlar.map(satir)].join("\n");
  },
};
BICIM
tail -3 yeni/kopru/bicimler.mjs
    return [baslik, satir(sutunlar), ...satirlar.map(satir)].join("\n");
  },
};

Geriye iki bileşim kökünün ve koşum betiğinin güncellenmesi kalıyor; aşağıdaki betik bunları uygular, dokuz çifti koşturur ve iki düzeni sayar.

cd yeni
sed -i.y -e 's#^import { OzetMetin.*$#&\nimport { OzetHizali, KademeHizali, BolgeHizali } from "./hizali.mjs";#' \
         -e 's#^  "bolge/metin": BolgeMetin, "bolge/ayrilmis": BolgeAyrilmis,#&\n  "ozet/hizali": OzetHizali, "kademe/hizali": KademeHizali, "bolge/hizali": BolgeHizali,#' kalitim/kur.mjs
sed -i.y -e 's#, ayrilmisBicimi } from "./bicimler.mjs";#, ayrilmisBicimi, hizaliBicimi } from "./bicimler.mjs";#' \
         -e 's#ayrilmis: ayrilmisBicimi };#ayrilmis: ayrilmisBicimi, hizali: hizaliBicimi };#' kopru/kur.mjs
sed -i.y -e 's#\["metin", "ayrilmis"\]#["metin", "ayrilmis", "hizali"]#' -e 's#/ 6`#/ 9`#' \
         -e 's#kopruRapor("bolge", "ayrilmis"#kopruRapor("kademe", "hizali"#' kosum.mjs
rm -f kalitim/*.y kopru/*.y *.y
node kosum.mjs | tail -7
node tip-say.mjs
for d in kalitim kopru; do
  echo "$d: eklenen satir=$(diff -rN "../$d" "$d" | grep '^>' | grep -cvE '^> *(//|$)')  duzenlenen dosya=$(diff -rq "../$d" "$d" | grep -c '^Files ')"
done
kademe dokumu
kademe  | adet | tutar
0-1 kg  | 1    | 4990
1-5 kg  | 2    | 18678
5-15 kg | 1    | 17239
15+ kg  | 2    | 64975
ayrilan cift = 0 / 9
kalitim  tip=13  satir= 80  cikti govdesi=9
kopru    tip= 7  satir= 49  cikti govdesi=4
kalitim: eklenen satir=17  duzenlenen dosya=1
kopru: eklenen satir=9  duzenlenen dosya=2

Dokuz çift de aynı metni üretti. Kalıtım düzenine 3 tip ve 17 satır girdi, köprü düzenine 1 tip ve 9 satır. İki ölçüm noktası çarpımı gösteriyor: kalıtım 10 → 13 (üç tür × üç biçim artı taban ve ara sınıflar), köprü 6 → 7. Dördüncü biçim kalıtımda 3 tip daha, köprüde 1 tip daha getirir; fark biçim sayısıyla değil, tür sayısı kadar büyür.

Düzenlenen dosya sayısında köprü geride: 2’ye 1. Sebep, köprünün yeni biçimi var olan gerçekleştirim dosyasına eklemesidir; kalıtım ise yeni dosya açtı. Bu iki sayı birlikte okunur — kalıtımda düzenlenen tek dosya bileşim köküne üç satır eklerken, köprüde düzenlenen iki dosyanın ikisi de tek satırlık listelerdir.

Bedel ve Ne Zaman Uygulanmayacağı

Köprünün ilk bedeli sözleşmenin kendisidir. Soyutlama ile gerçekleştirim arasındaki üç veri — başlık, sütunlar, satırlar — bütün biçimlerin yetinmek zorunda olduğu dildir. Sütun genişliğine göre satır kırmak, alt toplam eklemek ya da hücreye para birimi yazmak isteyen bir biçim bu sözleşmeye sığmaz; sözleşmenin büyütülmesi gerekir ve büyüyen sözleşme, bütün biçim nesnelerinin o alana bir yanıt vermesini zorunlu kılar. Maliyet gerçekleştirim sayısıyla doğru orantılı büyür.

İkinci bedel dolaylılıktır. Kalıtım düzeninde bir çıktı tek gövdede üretilir; köprüde çağrı yaz yönteminden uret yöntemine geçer, izlenecek gövde sayısı 1’den 2’ye çıkar. Üçüncüsü hücreye özel davranışın kaybıdır: kalıtım düzeninde altı yaprağın herhangi biri tek başına özelleştirilebilir — “bölge raporunun ayrılmış değer çıktısında başlık satırı olmasın” isteği tek bir yaprağın gövdesini değiştirir. Köprüde aynı istek ya biçim nesnesine rapor türüne bakan bir koşul koyar — ki bu gerçekleştirimi soyutlamaya bağlar ve iki eksenin bağımsızlığını bozar — ya da sözleşmeye bir bayrak ekler.

Buradan iki uygulanmama koşulu çıkar. Eksenlerden biri tek üyeli kalacaksa çarpım zaten toplama eşittir ve köprü yalnız bir dolaylılık katmanıdır. Hücrelerin çoğu özel davranış istiyorsa — yani biçim gerçekten türe bağlıysa — eksenler bağımsız değildir; o durumda köprü her hücrede koşul biriktirir ve kalıtım düzeni daha az koşulla çalışır.

Özet

  • Köprünün çözdüğü problem, bir yeteneğin iki bağımsız eksende birden değişmesidir; çözüm, ikinci ekseni ayrı bir hiyerarşiye alıp soyutlamanın gerçekleştirime başvuru tutmasıdır.
  • Üç tür ve iki biçim için kalıtım düzeni 10 tip ve 63 satır, köprü düzeni 6 tip ve 42 satır taşır; çıktı gövdesi 6’ya 3’tür ve aynı biçim gövdesi kalıtımda tür sayısı kadar yazılır.
  • Üçüncü biçim kalıtım düzenine 3 tip ile 17 satır, köprü düzenine 1 tip ile 9 satır ekledi; dokuz tür–biçim çiftinin çıktısı iki düzende de aynı kaldı.
  • İki ölçüm noktası büyüme kuralını doğruluyor: kalıtımda tip sayısı tür ile biçimin çarpımı, köprüde toplamı kadar artar.
  • Bedel, bütün gerçekleştirimlerin yetinmek zorunda olduğu sabit bir sözleşme, bir dolaylılık basamağı ve hücreye özel davranışın kaybıdır; eksenlerden biri tek üyeliyse ya da biçim gerçekten türe bağlıysa kalıp karşılığını vermez.

Sonraki Adım

Bağdaştırıcı ile köprü, tek bir nesnenin arkasına başka bir nesne koydu; ikisinde de sarılan şey birdi. Raporlanan gönderi listesi de düzdü: altı gönderi, hepsi aynı düzeyde. Oysa alan bunu bozar — bir gönderi, içinde başka gönderiler taşıyan konsolide bir gönderi olabilir ve o gönderilerden biri yine konsolide olabilir. Ağırlık toplamı, ücret toplamı ve teslim süresi artık bir ağaç üzerinde hesaplanır. Her işlem “bu tek parça mı, konsolide mi” sorusunu kendi gövdesinde sorarsa, tür sınaması işlem sayısı kadar tekrarlanır. Sonraki ders bu sınamaları sayar, yaprakla düğümü aynı arayüzün arkasına koyan bileşik kalıbını yazar ve iki düzeni iki ayrı genişleme yönünde ölçer: yeni bir düğüm türü ve yeni bir işlem eklendiğinde dokunulan dosya, eklenen satır ve tür sınaması sayısı.

İ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