İçeriğe geç
academia.sh

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 switch sü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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat