Ders 02 / 19
Açık–Kapalı İlkesi
Genişlemeye açık, değişikliğe kapalı tasarımın ölçülmesi: aynı tarife türü iki ayrı switch üzerinden seçilen sürüm ile tarifelerin kendini bir kayda yazdığı sürüme yeni bir tarife eklenir, düzenlenen var olan dosya sayısı, eklenen satır sayısı ve hesap modülünün değişip değişmediği karşılaştırılır.
İçindekiler
Tek sorumluluk ilkesi kararları aktörlerine göre ayırdı, ama her aktör yine var olan bir
dosyayı düzenledi. Bölge tablosuna yeni bir bölge eklemek bolge.mjs içindeki nesneyi
düzenlemektir. Bu, tablodaki bir satırı çoğaltmak olduğu sürece ucuzdur. Tarifenin
türüne dokunan bir istek geldiğinde — ağırlık yerine hacme göre ücretlendiren ikinci bir
tarife — maliyet başka türlü çıkar.
Açık–kapalı ilkesi (open–closed principle) bu iki işlemi ayırır: bir modül yeni davranışla genişlemeye açık, var olan metninin değiştirilmesine kapalı olmalıdır. İddia ölçülebilir: aynı gereksinim iki tasarıma uygulanır, düzenlenen var olan dosya sayısı, eklenen satır sayısı ve hesap modülünün değişip değişmediği sayılır.
Türün Switch ile Seçildiği Sürüm
İlk sürümde tarife türü bir alan olarak gönderinin üstünde durur ve iki ayrı yerde switch
ile çözülür: ücretin tabanı ve fatura satırının açıklaması.
// v1/ucret.mjs — tarife turu iki ayri switch ile secilir export const BOLGE = { "34": 100, "06": 115, "65": 140 }; const katsayi = (adres) => BOLGE[adres.slice(0, 2)] ?? 160; export function taban(gonderi) { switch (gonderi.tarife) { case "agirlik": return gonderi.agirlik <= 1 ? 3900 : gonderi.agirlik <= 5 ? 6400 : 11800; case "sabit": return 4500; default: throw new RangeError(`bilinmeyen tarife: ${gonderi.tarife}`); } } export function aciklama(gonderi) { switch (gonderi.tarife) { case "agirlik": return `agirlik kademesi (${gonderi.agirlik} kg)`; case "sabit": return "sabit ucret"; default: throw new RangeError(`bilinmeyen tarife: ${gonderi.tarife}`); } } export const ucret = (g) => Math.round((taban(g) * katsayi(g.adres)) / 100);
// v1/kur.mjs — ornek gonderiler icin ucret satirlari import { ucret, aciklama } from "./ucret.mjs"; export const ORNEK = [ { tarife: "agirlik", agirlik: 0.8, adres: "34100", en: 20, boy: 20, yukseklik: 15 }, { tarife: "sabit", agirlik: 3.0, adres: "06500", en: 30, boy: 40, yukseklik: 50 }, ]; for (const g of ORNEK) console.log(`${ucret(g)} kurus ${aciklama(g)}`);
node v1/kur.mjs
3900 kurus agirlik kademesi (0.8 kg) 5175 kurus sabit ucret
Bu tasarımda tarife türü bir veri, davranış ise türü çözen kod. İki switch aynı
listeyi ayrı ayrı sayıyor; üçüncü bir kullanım yeri çıksa üçüncü bir liste olacak.
Türün Kayda Taşındığı Sürüm
İkinci sürümde tarife bir tür etiketi değil, davranışı taşıyan bir nesnedir. Nesneler ortak bir kayda kendilerini yazar; hesap modülü kayda sorar ve tür adlarını hiç bilmez.
// v2/kayit.mjs — tarife kaydi: yeni tarifeler bu tabloya kendini yazar const TARIFELER = new Map(); export function kaydet(tarife) { TARIFELER.set(tarife.ad, tarife); } export function tarife(ad) { const t = TARIFELER.get(ad); if (t === undefined) throw new RangeError(`bilinmeyen tarife: ${ad}`); return t; }
// v2/tarifeler/agirlik.mjs — agirlik kademesi tarifesi import { kaydet } from "../kayit.mjs"; kaydet({ ad: "agirlik", taban: (g) => (g.agirlik <= 1 ? 3900 : g.agirlik <= 5 ? 6400 : 11800), aciklama: (g) => `agirlik kademesi (${g.agirlik} kg)`, });
// v2/tarifeler/sabit.mjs — sabit ucret tarifesi import { kaydet } from "../kayit.mjs"; kaydet({ ad: "sabit", taban: () => 4500, aciklama: () => "sabit ucret" });
// v2/ucret.mjs — tarife turune bakmayan hesap; secim kayittan gelir import { tarife } from "./kayit.mjs"; export const BOLGE = { "34": 100, "06": 115, "65": 140 }; const katsayi = (adres) => BOLGE[adres.slice(0, 2)] ?? 160; export const ucret = (g) => Math.round((tarife(g.tarife).taban(g) * katsayi(g.adres)) / 100); export const aciklama = (g) => tarife(g.tarife).aciklama(g);
// v2/kur.mjs — bilesim koku: kullanilacak tarifeleri yalniz burasi bilir import "./tarifeler/agirlik.mjs"; import "./tarifeler/sabit.mjs"; import { ucret, aciklama } from "./ucret.mjs"; export const ORNEK = [ { tarife: "agirlik", agirlik: 0.8, adres: "34100", en: 20, boy: 20, yukseklik: 15 }, { tarife: "sabit", agirlik: 3.0, adres: "06500", en: 30, boy: 40, yukseklik: 50 }, ]; for (const g of ORNEK) console.log(`${ucret(g)} kurus ${aciklama(g)}`);
node v2/kur.mjs
3900 kurus agirlik kademesi (0.8 kg) 5175 kurus sabit ucret
Aynı çıktı. Fark davranışta değil, yeni bir türün nereye yazılacağında. Tarife adlarının listesi tek bir yerde durur ve orası bileşim köküdür — Katman Sorumlulukları dersinde tanıtılan, parçaları yalnızca kendisinin birbirine bağladığı modül.
Genişlemenin Ölçülmesi
Yeni gereksinim hacim tarifesidir: desi hesabı en, boy ve yükseklikten türetilir, taban ücret desi başına artar. İki ağaç kopyalanır ve değişiklik ikisine de uygulanır.
cp -r v1 v1-yeni cp -r v2 v2-yeni
// v1-yeni/ucret.mjs — hacim tarifesi eklendi: iki switch de duzenlendi export const BOLGE = { "34": 100, "06": 115, "65": 140 }; const katsayi = (adres) => BOLGE[adres.slice(0, 2)] ?? 160; const desi = (g) => Math.ceil((g.en * g.boy * g.yukseklik) / 5000); export function taban(gonderi) { switch (gonderi.tarife) { case "agirlik": return gonderi.agirlik <= 1 ? 3900 : gonderi.agirlik <= 5 ? 6400 : 11800; case "sabit": return 4500; case "hacim": return 2600 + desi(gonderi) * 700; default: throw new RangeError(`bilinmeyen tarife: ${gonderi.tarife}`); } } export function aciklama(gonderi) { switch (gonderi.tarife) { case "agirlik": return `agirlik kademesi (${gonderi.agirlik} kg)`; case "sabit": return "sabit ucret"; case "hacim": return `hacim tarifesi (${desi(gonderi)} desi)`; default: throw new RangeError(`bilinmeyen tarife: ${gonderi.tarife}`); } } export const ucret = (g) => Math.round((taban(g) * katsayi(g.adres)) / 100);
// v2-yeni/tarifeler/hacim.mjs — hacim tarifesi: hesap modulu duzenlenmedi import { kaydet } from "../kayit.mjs"; const desi = (g) => Math.ceil((g.en * g.boy * g.yukseklik) / 5000); kaydet({ ad: "hacim", taban: (g) => 2600 + desi(g) * 700, aciklama: (g) => `hacim tarifesi (${desi(g)} desi)`, });
Kalan iki düzenleme her iki ağaçta da örnek veriye ve kayıt listesine aittir; aşağıdaki
betik bunları uygular, iki sürümü çalıştırır ve farkı sayar. Yerinde düzenlemede yedek
uzantısı verilmiştir; GNU ve BSD sed bu biçimde aynı davranır.
ORNEK=' { tarife: "hacim", agirlik: 3.0, adres: "06500", en: 30, boy: 40, yukseklik: 50 },' sed -i.y "s#^];#$ORNEK\n];#" v1-yeni/kur.mjs v2-yeni/kur.mjs sed -i.y 's#^import "./tarifeler/sabit.mjs";#&\nimport "./tarifeler/hacim.mjs";#' v2-yeni/kur.mjs rm -f v1-yeni/*.y v2-yeni/*.y node v1-yeni/kur.mjs node v2-yeni/kur.mjs olc() { echo "$1 -> $2" echo " duzenlenen var olan dosya = $(diff -rq $1 $2 | grep -c '^Files ')" echo " yeni dosya = $(diff -rq $1 $2 | grep -c '^Only in ')" echo " eklenen satir = $(diff -rN $1 $2 | grep '^>' | grep -cvE '^> *(//|$)')" echo " hesap modulu degisti mi = $(diff -qI '^//' $1/ucret.mjs $2/ucret.mjs > /dev/null && echo hayir || echo evet)" } olc v1 v1-yeni olc v2 v2-yeni
3900 kurus agirlik kademesi (0.8 kg) 5175 kurus sabit ucret 12650 kurus hacim tarifesi (12 desi) 3900 kurus agirlik kademesi (0.8 kg) 5175 kurus sabit ucret 12650 kurus hacim tarifesi (12 desi) v1 -> v1-yeni duzenlenen var olan dosya = 2 yeni dosya = 0 eklenen satir = 6 hesap modulu degisti mi = evet v2 -> v2-yeni duzenlenen var olan dosya = 1 yeni dosya = 1 eklenen satir = 9 hesap modulu degisti mi = hayir
Sonuçlar aynı; maliyetin dağılımı farklı. İkinci sürüm daha çok satır ekledi (9’a 6), çünkü yeni tarife kendi dosyasında tam bir nesne olarak duruyor. Buna karşılık düzenlenen var olan dosya sayısı ikiden bire indi ve bu tek dosya bileşim köküdür. Son satır belirleyici olan: ücret hesabını yapan modül birinci sürümde değişti, ikincisinde değişmedi.
Karşılaştırma iki tür maliyeti ayırıyor. Eklenen satır ucuzdur: yeni metin var olan bir
davranışı bozamaz. Düzenlenen satır pahalıdır: taban ve aciklama işlevlerinin daha önce
doğru çalışan iki tarifesi, üçüncü tarife eklenirken yeniden gözden geçirilmek zorundadır.
İlkenin ölçüsü “kaç satır yazıldı” değil, “kaç satır yeniden doğrulanmak zorunda kaldı”
sorusudur.
Kapalılığın Sınırı
Kapalılık evrensel olamaz. İkinci sürüm yeni tarife türüne kapalıdır, ama tarife sözleşmesinin kendisine kapalı değildir. Tarife nesnesine üçüncü bir yöntem eklemek — örneğin azami desi sınırı — kayıttaki bütün tarife dosyalarını düzenlemeyi gerektirir; maliyet tarife sayısıyla doğru orantılı büyür.
Seçim bu yüzden hangi eksenin değişeceğinin tahminine dayanır. Tarife sayısı artıyorsa
ikinci sürüm kazanır; tarife sözleşmesi oynuyor ve tür sayısı iki üçte kalıyorsa switch
daha ucuzdur, çünkü kayıt düzeneğinin kendisi de bir maliyettir. İlke, her yerde soyutlama
kurmayı değil, hangi eksende değişiklik geldiğini ölçüp o eksene kapanmayı söyler. Ölçünün
kaynağı da tahmin değil, geçmiş genişleme istekleridir.
Özet
- Açık–kapalı ilkesi genişleme (yeni metin eklemek) ile değişiklik (var olan metni düzenlemek) arasında ayrım yapar ve ikincisini sınırlar.
- Aynı gereksinim iki tasarıma uygulandığında
switchsürümü 2 var olan dosyayı düzenledi ve hesap modülünü değiştirdi; kayıt sürümü 1 dosya düzenledi, 1 yeni dosya ekledi ve hesap modülüne dokunmadı. - Kayıt sürümü daha çok satır ekledi (9’a 6); ilkenin ölçüsü yazılan satır sayısı değil, yeniden doğrulanmak zorunda kalan satır sayısıdır.
- Kapalılık tek bir eksende geçerlidir: kayıt sürümü yeni tarife türüne kapalıdır, tarife sözleşmesinin değişmesine değil.
- Hangi eksene kapanılacağı tahminle değil, geçmişte gelen genişleme istekleriyle belirlenir.
Sonraki Adım
Kayıt sürümünde bütün tarifeler aynı sözleşmeyi karşıladığı için hesap modülü hangi tarifeyi
aldığını sormadan çalıştı. Bu, sözleşmenin gerçekten karşılandığı varsayımına dayanıyor.
Yeni bir tarife taban yöntemini tanımlar ama negatif değer döndürürse, ya da yalnız belli
gönderilerde çalışıp kalanında hata atarsa, hesap modülü değişmeden bozulur. Sonraki ders
alt tip sözleşmesinin ne anlama geldiğini tanımlar, sözleşmeyi bozan bir tarife yazar ve
üst tip üzerinden çalışan bir sözleşme sınamasını iki tarifeyle koşturarak bozan sürümde
kırıldığını gösterir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.