Ders 02 / 14
Kapsülleme
Değişmezin sahibini belirleyen araç olarak kapsülleme: alanları açık ve kapalı iki sevkiyat sürümüne aynı işlem dizisinin uygulanması, bozuk duruma düşen nesne sayısının sayılması ve dönüş değeri kapsanmadığında korumanın nerede kırıldığı.
İçindekiler
Önceki ders bölünmüş ücretlendirme akışının bir boşluk bıraktığını gösterdi: “ücret
hesaplanan her gönderi doğrulanmıştır” kuralı hiçbir yerde korunmuyor, yalnız
ucretHesapla yordamının içindeki çağrı sırasında yaşıyordu. Yordamlar veriyi argüman
olarak alır ve verinin geçmişi hakkında hiçbir güvence taşımaz.
Kapsülleme (encapsulation), bu boşluğu kapatan araçtır: veriyi ve onu değiştiren işlemleri tek bir birimde toplar, veriye erişimi bu işlemlerle sınırlar. Programlama Temelleri kursunda tanıtılan tanım budur. Bu derste tanım tekrarlanmaz; sorulan soru farklıdır — kapsülleme ne zaman gerçekten çalışır, nerede kırılır ve bedeli nedir.
Kapsülleme Neyi Kapatır
Yaygın bir yanlış okuma, kapsüllemenin “tüm alanları gizlemek” olduğudur. Ölçüt gizlilik değil, değişmez (invariant). Bir alan, bir değişmezin denklemine giriyorsa dışarıdan yazılamamalıdır; girmiyorsa gizlenmesi bir şey kazandırmaz.
Bu kursun kitaplığında bir sevkiyat, bir araca yüklenen gönderiler kümesidir ve üç
değişmez taşır:
- Kayıtlı toplam ağırlık, yüklü gönderilerin ağırlık toplamına eşittir.
- Toplam ağırlık, aracın kapasitesini aşmaz.
- Mühürlenmiş bir sevkiyata gönderi eklenmemiştir.
kapasiteGram ikinci değişmezin bir tarafıdır; dışarıdan yazılabilirse değişmez tek
atamayla çiğnenir. aracId ise hiçbir denklemde geçmez; onu gizlemek yalnız erişimci
yazdırır. Kapsülleme kararı alan alan verilir, sınıf toptan kapatılarak değil.
Değişmezlerin denetimi tek bir yerde yazılır ki iki sürüme de aynı ölçüt uygulanabilsin. Aynı dosyada, koşumların yinelenebilir olması için sabit tohumlu bir sayı üreteci de bulunuyor.
// denetci.mjs — uc degismezi tek yerde tanimlar; iki surume de ayni denetim uygulanir export function ihlaller(d) { const bozuk = []; const toplam = d.gonderiler.reduce((t, g) => t + g.agirlikGram, 0); if (toplam !== d.toplamAgirlik) bozuk.push("toplam-tutarsiz"); if (d.toplamAgirlik > d.kapasiteGram) bozuk.push("kapasite-asildi"); if (d.muhurlu && d.gonderiler.length > d.muhurdekiSayi) bozuk.push("muhur-sonrasi-ekleme"); return bozuk; } export function uretec(tohum) { let x = tohum; return () => (x = (x * 1103515245 + 12345) % 2147483648) / 2147483648; }
Alanları Açık Sürüm
İlk sürüm sevkiyatı düz bir nesne olarak tutar ve kuralları bir ekle yordamına yazar.
Kurallar burada vardır; eksik olan, onlardan geçmenin zorunlu olmasıdır.
// sevkiyat-acik.mjs — alanlari disariya acik sevkiyat; kurallar ekle() icinde ama zorunlu degil export function sevkiyatOlustur(aracId, kapasiteGram) { return { aracId, kapasiteGram, gonderiler: [], toplamAgirlik: 0, muhurlu: false, muhurdekiSayi: 0 }; } export function ekle(sevkiyat, gonderi) { if (sevkiyat.muhurlu) return false; if (sevkiyat.toplamAgirlik + gonderi.agirlikGram > sevkiyat.kapasiteGram) return false; sevkiyat.gonderiler.push(gonderi); sevkiyat.toplamAgirlik += gonderi.agirlikGram; return true; } export function muhurle(sevkiyat) { sevkiyat.muhurlu = true; sevkiyat.muhurdekiSayi = sevkiyat.gonderiler.length; }
Bu tasarımda sevkiyat.gonderiler.push(g) yazmak, ekle(sevkiyat, g) yazmaktan kısadır
ve aynı görünen sonucu üretir. Büyüyen bir kod tabanında bu kısa yol er geç kullanılır:
bir aktarma adımında, bir içe aktarma betiğinde, bir hata düzeltmesinde. Kısa yolun
sonucu, atlanan iki denetimdir.
Alanları Kapalı Sürüm
İkinci sürümde değişmeze giren dört alan özeldir. Dışarıdan görünen tek yol iki yöntemdir.
// sevkiyat-kapali.mjs — degismeze giren alanlar ozel; her degisiklik iki yontemden gecer export class Sevkiyat { #kapasiteGram; #gonderiler = []; #toplamAgirlik = 0; #muhurlu = false; #muhurdekiSayi = 0; constructor(aracId, kapasiteGram) { if (kapasiteGram <= 0) throw new RangeError("kapasite pozitif olmali"); this.aracId = aracId; this.#kapasiteGram = kapasiteGram; } ekle(gonderi) { if (this.#muhurlu) return false; if (this.#toplamAgirlik + gonderi.agirlikGram > this.#kapasiteGram) return false; this.#gonderiler.push({ ...gonderi }); this.#toplamAgirlik += gonderi.agirlikGram; return true; } muhurle() { this.#muhurlu = true; this.#muhurdekiSayi = this.#gonderiler.length; } durum() { return { kapasiteGram: this.#kapasiteGram, gonderiler: this.#gonderiler.map((g) => ({ ...g })), toplamAgirlik: this.#toplamAgirlik, muhurlu: this.#muhurlu, muhurdekiSayi: this.#muhurdekiSayi, }; } }
aracId açık kaldı: hiçbir değişmezin parçası değil. ekle yönteminin içindeki
{ ...gonderi } bir savunma kopyasıdır; çağıranın elinde kalan nesne sonradan
değiştirilse bile sevkiyatın toplamı bozulmaz. durum() de aynı gerekçeyle kopya
döndürür — bu kararın bedeli dersin ilerleyen bölümünde ölçülecek.
Aynı İşlem Dizisi, İki Sonuç
Ölçüm şu biçimde kurulur: sabit tohumlu bir üreteçle 200 sevkiyat için 20’şer işlemlik bir
dizi üretilir; aynı dizi iki sürüme de uygulanır. Dizideki işlemlerin bir bölümü kısa
yoldur — açık sürümde doğrudan alan yazımına, kapalı sürümde ise ekle çağrısına karşılık
gelir, çünkü orada başka yol yoktur.
// kosum.mjs — ayni islem dizisi iki surume uygulanir, bozuk duruma dusen nesne sayilir import { sevkiyatOlustur, ekle, muhurle } from "./sevkiyat-acik.mjs"; import { Sevkiyat } from "./sevkiyat-kapali.mjs"; import { ihlaller, uretec } from "./denetci.mjs"; const NESNE = 200; const ISLEM = 20; function islemler() { const rast = uretec(20250729); const dizi = []; for (let n = 0; n < NESNE; n += 1) { const tek = []; for (let i = 0; i < ISLEM; i += 1) { const g = { id: `${n}-${i}`, agirlikGram: 100 + Math.floor(rast() * 3900) }; const r = rast(); if (r < 0.10) tek.push({ tur: "muhurle" }); else if (r < 0.45) tek.push({ tur: rast() < 0.5 ? "kisayol-tam" : "kisayol-yarim", g }); else tek.push({ tur: "ekle", g }); } dizi.push(tek); } return dizi; } const dizi = islemler(); let acikBozuk = 0; const acikIhlal = new Map(); for (const tek of dizi) { const s = sevkiyatOlustur("34ABC01", 20000); for (const op of tek) { if (op.tur === "muhurle") muhurle(s); else if (op.tur === "ekle") ekle(s, op.g); else if (op.tur === "kisayol-tam") { s.gonderiler.push(op.g); s.toplamAgirlik += op.g.agirlikGram; } else s.gonderiler.push(op.g); } const bozuk = ihlaller(s); if (bozuk.length > 0) acikBozuk += 1; for (const ad of bozuk) acikIhlal.set(ad, (acikIhlal.get(ad) ?? 0) + 1); } let kapaliBozuk = 0; let reddedilen = 0; for (const tek of dizi) { const s = new Sevkiyat("34ABC01", 20000); for (const op of tek) { if (op.tur === "muhurle") s.muhurle(); else if (s.ekle(op.g) === false) reddedilen += 1; } if (ihlaller(s.durum()).length > 0) kapaliBozuk += 1; } console.log(`nesne sayisi = ${NESNE}`); console.log(`acik surum bozuk = ${acikBozuk}`); console.log(`kapali surum bozuk = ${kapaliBozuk}`); console.log(`kapali surum reddetti = ${reddedilen}`); for (const [ad, sayi] of [...acikIhlal].sort()) console.log(` ihlal ${ad.padEnd(21)} = ${sayi}`);
node kosum.mjs
nesne sayisi = 200 acik surum bozuk = 198 kapali surum bozuk = 0 kapali surum reddetti = 2428 ihlal kapasite-asildi = 43 ihlal muhur-sonrasi-ekleme = 137 ihlal toplam-tutarsiz = 195
İki yüz sevkiyatın 198’i açık sürümde en az bir değişmezi çiğnedi. Kapalı sürümde bozuk nesne yok — ama asıl bilgi son satırda: kapalı sürüm 2428 işlemi reddetti. Aynı 2428 işlem açık sürümde sessizce kabul edilmişti.
Bu, kapsüllemenin ne yapıp ne yapmadığını tam olarak söyler. Kapsülleme talebi ortadan
kaldırmaz; sessiz bozulmayı görünür bir redde çevirir. Reddin çağırana nasıl
bildirileceği ayrı bir tasarım kararıdır: burada false dönüldü, sebebi taşıyan bir sonuç
nesnesi de dönülebilirdi. Ölçülebilir olan, bozulmanın 198’den 0’a inmesidir.
Koruma Nerede Kırılır
Özel alan tek başına yetmez. Bir yöntem iç veriyi olduğu gibi döndürüyorsa, çağıran o
veriyi değiştirerek değişmezi dışarıdan bozabilir. Aşağıdaki sınıf alanları özel tutar ama
manifesto() iç diziyi doğrudan verir.
// sizdiran-sevkiyat.mjs — alanlar ozel ama manifesto() ic diziyi oldugu gibi dondurur export class SizdiranSevkiyat { #kapasiteGram; #gonderiler = []; #toplamAgirlik = 0; constructor(kapasiteGram) { this.#kapasiteGram = kapasiteGram; } ekle(gonderi) { if (this.#toplamAgirlik + gonderi.agirlikGram > this.#kapasiteGram) return false; this.#gonderiler.push({ ...gonderi }); this.#toplamAgirlik += gonderi.agirlikGram; return true; } manifesto() { return this.#gonderiler; } kopyaManifesto() { return this.#gonderiler.map((g) => ({ ...g })); } durum() { return { kapasiteGram: this.#kapasiteGram, gonderiler: this.kopyaManifesto(), toplamAgirlik: this.#toplamAgirlik, muhurlu: false, muhurdekiSayi: 0 }; } }
Çağıran taraf, manifestoyu alıp aktarma parçasını listeye ekleyen sıradan bir adımdır.
// sizinti.mjs — donus degeri kapsanmadiginda ozel alanin korumasi ne kadar dayanir import { SizdiranSevkiyat } from "./sizdiran-sevkiyat.mjs"; import { ihlaller, uretec } from "./denetci.mjs"; function kosu(manifestoAl) { const rast = uretec(20250729); let bozuk = 0; for (let n = 0; n < 200; n += 1) { const s = new SizdiranSevkiyat(20000); for (let i = 0; i < 8; i += 1) { s.ekle({ id: `${n}-${i}`, agirlikGram: 100 + Math.floor(rast() * 3900) }); } // aktarma adimi: manifestoyu alip aktarma parcasini listeye ekler manifestoAl(s).push({ id: `${n}-aktarma`, agirlikGram: 2500 }); if (ihlaller(s.durum()).length > 0) bozuk += 1; } return bozuk; } console.log(`ic diziyi donduren manifesto -> bozuk nesne = ${kosu((s) => s.manifesto())}`); console.log(`kopya donduren manifesto -> bozuk nesne = ${kosu((s) => s.kopyaManifesto())}`);
node sizinti.mjs
ic diziyi donduren manifesto -> bozuk nesne = 200 kopya donduren manifesto -> bozuk nesne = 0
İki yüz nesnenin tamamı bozuldu; tek fark, döndürülen değerin kopya olup olmamasıydı.
Kapsüllemenin sınırı erişim belirtecinde değil, nesnenin dışına çıkan her referansta
çizilir. Aynı gözlem yapıcıya giren referanslar için de geçerlidir; kapalı sürümün ekle
yönteminde kopya alınmasının nedeni buydu.
Bedel
Kopya döndürmek bedava değildir. durum() çağrısı, gönderi sayısıyla orantılı iş yapar;
sıcak bir döngüde çağrılırsa maliyet ölçülebilir hâle gelir. Üç yaygın çıkış yolu vardır:
değiştirilemez öge kullanmak (kopya gereksizleşir), salt okunur bir görünüm döndürmek, ya
da listeyi hiç döndürmeyip sorulan soruyu nesneye sormak — “kaç gönderi var”, “toplam
ağırlık nedir”.
İkinci bedel, kapsüllemenin yanlış yere uygulanmasıdır. Hiçbir değişmez taşımayan bir veri taşıyıcısında — bir adres kaydı, bir tarife satırı — her alan için erişimci yazmak, açık alanla aynı korumayı sıfır kazançla pahalıya alır. Ölçüt değişmedi: kapatılan şey bir kuralı koruyorsa kapsülleme, korumuyorsa tören.
Özet
- Kapsüllemenin ölçütü gizlilik değil değişmezdir; bir alan bir değişmezin denklemine girmiyorsa gizlenmesi kazanç sağlamaz.
- Aynı 4000 işlemlik dizi altında alanları açık sürümde 200 sevkiyatın 198’i bozuk duruma düştü, kapalı sürümde hiçbiri düşmedi.
- Kapsülleme talebi yok etmez, sessiz bozulmayı görünür redde çevirir: kapalı sürüm 2428 işlemi reddetti, açık sürüm aynı işlemleri sessizce kabul etmişti.
- İç veriyi olduğu gibi döndüren bir yöntem korumayı tümüyle kaldırır; iç diziyi döndüren sürümde 200 nesnenin 200’ü bozuldu, kopya döndürende hiçbiri bozulmadı.
- Kopyanın bedeli gönderi sayısıyla orantılıdır; alternatifleri değiştirilemez öge, salt okunur görünüm ve listeyi döndürmek yerine soruyu nesneye sormaktır.
Sonraki Adım
Kapsülleme, verinin dışarıdan yazılmasını engelledi. Ama sevkiyatın dışarıya kaç ad açtığı, bu adların ne kadar iç ayrıntı sızdırdığı ve bir ayrıntı değiştiğinde kaç çağrı noktasının etkilendiği henüz sorulmadı. Sonraki ders bu soruyu ele alır: bir modülün dışa açtığı ad sayısı ile istemcinin bilmek zorunda olduğu ayrıntı sayısı ölçülür, temsil değiştirildiğinde iki tasarımda kaç dosyanın kırıldığı sayılır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.