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.