Ders 10 / 30
Dekoratör
Davranışın katmanlanarak eklenmesi: birbirinden bağımsız açılıp kapanan dört ücret ekinin her bileşimi için bir alt sınıf üreten düzen ile aynı arayüzü koruyarak saran dekoratör zincirinin tip ve satır sayısıyla karşılaştırılması, beşinci ek geldiğinde büyümenin ölçülmesi, zincir derinliği ve sıra duyarlılığı cinsinden bedel.
İçindekiler
Bileşikte sarılan şey bir kümeydi: düğüm, parçalarına kendisiyle aynı arayüzle soruyordu ve sarma işlemi bir ağaç kuruyordu. Aynı teknik tek bir nesne üzerinde, başka bir amaçla da kullanılır.
Gönderinin ücretine sırayla dört ek uygulanması gerekiyor: yakıt farkı, sigorta, sözleşmeli müşteri indirimi ve vergi. Dördü birbirinden bağımsız açılıp kapanır — bir gönderide yalnız vergi, başkasında yakıt farkı ile sigorta istenir. Her bileşim bir alt sınıf olursa sınıf sayısı seçenek sayısının ikinin kuvveti kadar büyür. Dekoratör (decorator) bu bileşimi sınıf yerine nesne düzeyinde kurar: her ek, sardığı nesnenin arayüzünü aynen döndüren ve sonucun üstüne kendi katkısını koyan bir sarmalayıcıdır. Ölçüler tip sayısı, satır sayısı, zincir derinliği ve sıra duyarlılığıdır.
Problem: Bağımsız Seçeneklerin Çarpımı
Taban ücret ve örnek gönderiler iki düzende de ortaktır.
// tarife.mjs — iki duzenin de kullandigi taban ucret ve ornek gonderiler; tutar kurustur const KADEME = [[1, 4990], [5, 8490], [15, 14990], [30, 24990]]; const KATSAYI = { 34: 100, "06": 115, 35: 120, 65: 145 }; 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 GONDERILER = [ { kod: "G-1", agirlik: 0.8, postaKodu: "34100", deger: 120000 }, { kod: "G-2", agirlik: 12, postaKodu: "06500", deger: 40000 }, { kod: "G-3", agirlik: 26, postaKodu: "65100", deger: 900000 }, ];
Alt sınıf düzeninde her seçenek alt kümesi için bir sınıf gerekir: yalnız yakıt farkını uygulayan, yakıt farkı ile vergiyi uygulayan, üçünü uygulayan… On altı sınıfın on altısını elle yazmak bu dersin metnine sığmaz; onları bir üretici yazıyor. Üreticinin varlığı zaten ölçünün kendisidir — elle yazılacak gövde sayısı budur.
// uret.mjs — alt sinif patlamasini uretir: dort secenegin butun alt kumeleri import { mkdirSync, writeFileSync } from "node:fs"; import { dirname } from "node:path"; export const ADIM = [ ["Y", "t = Math.round(t * 1.08);"], ["S", "t = t + Math.max(500, Math.round(g.deger * 0.005));"], ["I", "t = Math.round(t * 0.88);"], ["V", "t = Math.round(t * 1.2);"], ]; export function uretVeYaz(yol, adimlar) { const parca = ['import { kademeUcret } from "../tarife.mjs";', ""]; const adlar = []; for (let maske = 0; maske < 2 ** adimlar.length; maske += 1) { const secili = adimlar.filter((_, i) => (maske >> i) & 1); const ad = `Ucret${secili.map(([h]) => h).join("") || "0"}`; adlar.push(ad); parca.push(`export class ${ad} {`, " hesapla(g) {", " let t = kademeUcret(g.agirlik, g.postaKodu);", ...secili.map(([, kod]) => ` ${kod}`), " return t;", " }", "}"); } parca.push(`export const SINIF = { ${adlar.join(", ")} };`); mkdirSync(dirname(yol), { recursive: true }); writeFileSync(yol, `${parca.join("\n")}\n`); return adlar.length; } console.log(`uretilen sinif = ${uretVeYaz("altsinif/hesaplar.mjs", ADIM)}`);
uretilen sinif = 16
Üretilen dosyanın başı, gövdelerin nasıl tekrarlandığını gösteriyor.
head -16 altsinif/hesaplar.mjs
import { kademeUcret } from "../tarife.mjs";
export class Ucret0 {
hesapla(g) {
let t = kademeUcret(g.agirlik, g.postaKodu);
return t;
}
}
export class UcretY {
hesapla(g) {
let t = kademeUcret(g.agirlik, g.postaKodu);
t = Math.round(t * 1.08);
return t;
}
}
export class UcretS {
Yakıt farkı satırı, onu içeren sekiz sınıfın sekizinde ayrı ayrı yazılıdır. Oran her seçenek için aynıdır: bir seçeneğin kodu, alt kümelerin yarısında tekrarlanır.
Çözüm: Aynı Arayüzü Döndüren Sarmalayıcı
Dekoratörün çözümü, eki bir sınıf değil bir nesne yapmaktır. Sarmalayıcı içine aldığı nesneyle aynı arayüzü sunar, bu yüzden kendisi de sarılabilir.
// dekorator/temel.mjs — sarilacak cekirdek: yalniz taban ucreti bilir import { kademeUcret } from "../tarife.mjs"; export const temelUcret = { hesapla: (g) => kademeUcret(g.agirlik, g.postaKodu) };
// dekorator/sarmalar.mjs — dort dekorator: her biri ayni arayuzu doner, icine sardigini bilmez export const yakitFarki = (ic) => ({ ic, hesapla: (g) => Math.round(ic.hesapla(g) * 1.08) }); export const sigorta = (ic) => ({ ic, hesapla: (g) => ic.hesapla(g) + Math.max(500, Math.round(g.deger * 0.005)), }); export const sozlesmeIndirimi = (ic) => ({ ic, hesapla: (g) => Math.round(ic.hesapla(g) * 0.88) }); export const vergi = (ic) => ({ ic, hesapla: (g) => Math.round(ic.hesapla(g) * 1.2) }); export const zincir = (temel, sarmalar) => sarmalar.reduce((n, f) => f(n), temel); export const derinlik = (n) => (n.ic === undefined ? 1 : 1 + derinlik(n.ic));
Her sarmalayıcı sardığı nesneye ic adıyla bir başvuru tutar; derinlik bu başvuruyu izleyerek
zincirin kaç gövdeden oluştuğunu söyler. Aşağıdaki koşum on altı alt kümenin her birini iki
düzende hesaplar ve karşılaştırır.
// kosum.mjs — butun secenek alt kumelerinde iki duzen ayni tutari veriyor mu import { GONDERILER } from "./tarife.mjs"; import { SINIF } from "./altsinif/hesaplar.mjs"; import { temelUcret } from "./dekorator/temel.mjs"; import { yakitFarki, sigorta, sozlesmeIndirimi, vergi, zincir, derinlik } from "./dekorator/sarmalar.mjs"; const SARMA = [["Y", yakitFarki], ["S", sigorta], ["I", sozlesmeIndirimi], ["V", vergi]]; const N = 2 ** SARMA.length; let ayrilan = 0; for (let maske = 0; maske < N; maske += 1) { const secili = SARMA.filter((_, i) => (maske >> i) & 1); const ad = `Ucret${secili.map(([h]) => h).join("") || "0"}`; const alt = new SINIF[ad](); const sarilmis = zincir(temelUcret, secili.map(([, f]) => f)); for (const g of GONDERILER) if (alt.hesapla(g) !== sarilmis.hesapla(g)) ayrilan += 1; if ([0, 1, 5, N - 1].includes(maske)) { console.log(`${ad.padEnd(10)} G-1=${alt.hesapla(GONDERILER[0])} G-3=${alt.hesapla(GONDERILER[2])} ` + `zincir derinligi=${derinlik(sarilmis)}`); } } console.log(`secenek=${SARMA.length} alt kume=${N} ayrilan tutar = ${ayrilan} / ${N * GONDERILER.length}`);
Ucret0 G-1=4990 G-3=36236 zincir derinligi=1 UcretY G-1=5389 G-3=39135 zincir derinligi=2 UcretYI G-1=4742 G-3=34439 zincir derinligi=3 UcretYSIV G-1=6324 G-3=46079 zincir derinligi=5 secenek=4 alt kume=16 ayrilan tutar = 0 / 48
Kırk sekiz hesabın kırk sekizi aynı — on altı alt küme, üç gönderi. Fark tutarlarda değil, kaç tipin taşındığında.
// olc.mjs — iki duzendeki tip ve satir sayisini sayar import { readdirSync, readFileSync } from "node:fs"; for (const dizin of ["altsinif", "dekorator"]) { let tip = 0, satir = 0; for (const d of readdirSync(dizin).filter((x) => x.endsWith(".mjs")).sort()) { const metin = readFileSync(`${dizin}/${d}`, "utf8"); tip += (metin.match(/^export class \w+/gm) ?? []).length + (metin.match(/^export const \w+ = \(ic\)/gm) ?? []).length + (metin.match(/^export const temelUcret/gm) ?? []).length; satir += metin.split("\n").filter((s) => s.trim() !== "" && !s.trim().startsWith("//")).length; } console.log(`${dizin.padEnd(10)} tip=${String(tip).padStart(2)} satir=${String(satir).padStart(3)}`); }
altsinif tip=16 satir=130 dekorator tip= 5 satir= 10
On altı tipe ve 130 satıra karşı beş tip ve 10 satır. Beş tip, çekirdek ile dört sarmalayıcıdır; seçeneklerin bileşimi hiçbir yerde bir tip olarak yazılmaz, çalışma zamanında kurulur.
Beşinci Ek Geldiğinde
Yeni gereksinim uzak bölge ekidir: sabit 1500 kuruş. Alt sınıf düzeninde üretici tablosuna bir satır, dekoratör düzeninde sarmalayıcı dosyasına bir satır girer.
mkdir -p yeni && cp -r tarife.mjs uret.mjs dekorator kosum.mjs olc.mjs yeni/ cd yeni sed -i.y 's#^\];# ["U", "t = t + 1500;"],\n];#' uret.mjs cat >> dekorator/sarmalar.mjs <<'SARMA' export const uzakBolgeEki = (ic) => ({ ic, hesapla: (g) => ic.hesapla(g) + 1500 }); SARMA sed -i.y -e 's#, vergi, zincir, derinlik }#, vergi, uzakBolgeEki, zincir, derinlik }#' \ -e 's#\["V", vergi\]\];#["V", vergi], ["U", uzakBolgeEki]];#' kosum.mjs rm -f *.y node uret.mjs node kosum.mjs | tail -2 node olc.mjs echo "dekorator: eklenen satir=$(diff -rN ../dekorator dekorator | grep '^>' | grep -cvE '^> *(//|$)') duzenlenen dosya=$(diff -rq ../dekorator dekorator | grep -c '^Files ')"
uretilen sinif = 32 UcretYSIVU G-1=7824 G-3=47579 zincir derinligi=6 secenek=5 alt kume=32 ayrilan tutar = 0 / 96 altsinif tip=32 satir=274 dekorator tip= 6 satir= 11 dekorator: eklenen satir=1 duzenlenen dosya=1
İki ölçüm noktası büyüme kuralını veriyor: alt sınıf düzeninde tip sayısı 16’dan 32’ye, satır sayısı 130’dan 274’e çıktı; dekoratör düzeninde 5’ten 6’ya ve 10’dan 11’e. Alt sınıf sayısı seçenek başına ikiye katlanır, dekoratör sayısı seçenek başına bir artar. Doksan altı hesabın doksan altısı iki düzende de aynı kaldı.
Bedel: Zincir Derinliği ve Sıra
Dekoratörün ilk bedeli koşum çıktısında zaten görünüyor: zincir derinliği 1’den 5’e, beşinci
ekle 6’ya çıktı. Alt sınıf düzeninde bir tutar tek gövdede hesaplanır ve hata ayıklarken tek
gövde okunur; dekoratör düzeninde aynı tutar için altı gövde sırayla izlenir. Çekirdeğe
ulaşmak isteyen bir kod da ic başvurusunu izlemek zorundadır; bu, Tasarım İlkeleri kursundaki
Demeter Yasası dersinin ölçtüğü erişim zincirini uzatır.
İkinci bedel daha sinsidir: sonuç sarma sırasına duyarlıdır.
// sira.mjs — ayni iki dekorator iki sirada: sigorta once mi, indirim once mi import { GONDERILER } from "./tarife.mjs"; import { temelUcret } from "./dekorator/temel.mjs"; import { sigorta, sozlesmeIndirimi, zincir } from "./dekorator/sarmalar.mjs"; const g = GONDERILER[0]; const a = zincir(temelUcret, [sigorta, sozlesmeIndirimi]).hesapla(g); const b = zincir(temelUcret, [sozlesmeIndirimi, sigorta]).hesapla(g); console.log(`sigorta -> indirim = ${a}`); console.log(`indirim -> sigorta = ${b}`); console.log(`fark = ${b - a} kurus`);
sigorta -> indirim = 4919 indirim -> sigorta = 4991 fark = 72 kurus
Yetmiş iki kuruş, indirimin sigorta bedeline uygulanıp uygulanmamasından geliyor: çarpan ek ile toplanan ek yer değiştirdiğinde sonuç değişir. Alt sınıf düzeninde sıra sınıfın gövdesinde sabittir ve okunabilir; dekoratör düzeninde sıra zincirin kurulduğu yerdedir ve orada yazılmamış bir kural olarak kalır. Kalıbın kullanıldığı her yerde sıranın nerede tanımlandığı açıkça gösterilmelidir.
Üçüncü bedel geçersiz bileşimlerin engellenememesidir. Zincir kurma işlemi her alt kümeyi kabul eder; “sözleşmeli müşteri indirimi ile hacim indirimi aynı gönderide olmaz” gibi bir kural hiçbir sarmalayıcının göremediği bir kuraldır, çünkü her sarmalayıcı yalnız içine aldığını bilir.
Buradan iki uygulanmama koşulu çıkar. Seçenek sayısı bir ya da ikiyse ikinin kuvveti küçüktür (iki ya da dört) ve dekoratörün dolaylılığı karşılığını vermez. Ekler birbirine bağımlıysa — biri diğerinin varlığını gerektiriyor ya da dışlıyorsa — bağımsız katmanlama varsayımı düşer; o durumda kuralı tek gövdede toplayan bir hesap daha az yanlış bileşim üretir.
Kalıbın bağdaştırıcı ve köprüden ayrımı da buradadır: bağdaştırıcı arayüzü değiştirir, köprü iki ekseni ayırır, dekoratör arayüzü korur ve davranışı üstüne ekler. Sarmalayıcının aynı arayüzü döndürmesi, zincirin sınırsız uzayabilmesinin tek nedenidir.
Özet
- Dekoratörün çözdüğü problem, birbirinden bağımsız açılıp kapanan eklerin her bileşimi için bir tip gerekmesidir; çözüm eki nesne yapmak ve sarılanın arayüzünü aynen döndürmektir.
- Dört seçenek için alt sınıf düzeni 16 tip ve 130 satır, dekoratör düzeni 5 tip ve 10 satır taşıdı; on altı alt kümenin kırk sekiz hesabı iki düzende de aynı çıktı.
- Beşinci ek alt sınıf düzenini 32 tipe ve 274 satıra çıkardı, dekoratör düzenini 6 tipe ve 11 satıra: alt sınıf sayısı seçenek başına ikiye katlanır, dekoratör sayısı bir artar.
- Bedel ölçülür: zincir derinliği 1’den 6’ya çıktı, yani bir tutar için izlenecek gövde sayısı altıya çıktı; sıra değişimi aynı iki ekte 72 kuruş fark üretti.
- Seçenek sayısı bir ya da ikiyse ya da ekler birbirine bağımlıysa kalıp karşılığını vermez; zincir kurma işlemi geçersiz bileşimleri engelleyemez.
Sonraki Adım
Dekoratör zincirini kuran kod, hangi eklerin hangi sırayla uygulanacağını bilmek zorundaydı. Bu bilgi tek bir çağrı için katlanılır, ama kitaplığın tamamı böyle çalışırsa istemci sıralı adımların hepsini öğrenmek zorunda kalır: adresi normalleştir, bölgeyi çöz, tarifeyi seç, ekleri uygula, taşıyıcıyı seç, teslim tarihini hesapla. Altı modülün doğru sırayla çağrılması bilgisi her istemcide tekrar eder; bir adım atlanırsa hata çalışma zamanında çıkar. Sonraki ders istemcinin doğrudan bağ sayısını ve sıralı adım bilgisinin kaç yerde tekrarlandığını sayar, alt sistemin önüne tek bir giriş noktası koyan cephe kalıbını yazar ve cephenin ithal kapanışını küçültüp küçültmediğini ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.