İçeriğe geç
academia.sh

Ders 03 / 14

Soyutlama

Soyutlamanın ölçütü olarak açılan ad ile bilinen ayrıntının ayrılması: aynı tarifenin temsili açık ve temsili gizli iki sunumunda istemcilerin bildiği ayrıntının sayılması, temsil değiştiğinde kırılan istemci sayısının karşılaştırılması ve gizlemenin bedelinin ölçülmesi.

İçindekiler

Önceki ders sevkiyatın alanlarını dışarıdan yazılamaz hâle getirdi ve bunun değişmezleri nasıl koruduğunu saydı. O ölçüm bir soruyu açıkta bıraktı: bir modülün dışarıya kaç ad açtığı ve bu adların ne kadar iç ayrıntı taşıdığı. Kapsülleme yazmayı engeller; okumayı ve bağımlılığı engellemez.

Soyutlama (abstraction), bir birimin dışarıya yalnız kullanım için gerekli olanı göstermesi, gerisini gizlemesidir. Programlama Temelleri kursunda tanıtılan tanım budur. Bu derste sorulan soru, tanım değil ölçüttür: “gerekli olan” hangi sayıyla belirlenir ve yanlış çizilen bir sınırın bedeli nedir.

Ölçüt: Açılan Ad ile Bilinen Ayrıntı Aynı Şey Değil

Yaygın ölçüt, dışa açılan ad sayısıdır — az ad açan modül daha iyi soyutlanmış sayılır. Bu ölçüt tek başına yanıltıcıdır, çünkü tek bir ad da bir veri yapısının tüm iç biçimini taşıyabilir. Bir tarife tablosunu tek bir nesne olarak dışa açan modül bir ad açar, ama o nesneyi kullanan her istemci kademelerin nasıl saklandığını, sıralı olup olmadığını ve alanların adlarını bilmek zorunda kalır.

İşe yarayan ölçüt ikilidir: dışa açılan ad sayısı ve istemcinin bilmek zorunda olduğu temsil ayrıntısı sayısı. İkisi birlikte, üçüncü bir sayıyı öngörür — bir ayrıntı değiştiğinde dokunulması gereken istemci sayısı. Bu ders üçünü de sayar.

Aynı Tarifenin İki Sunumu

Ücretlendirme kitaplığının tarife tablosu iki biçimde sunulur. İlkinde temsil dışarıdadır.

// acik/tarife.mjs — temsil disariya acik: kademe dizisi, katsayi tablosu ve esikler okunabilir
export const TARIFE = {
  kademeler: [[1000, 4500], [5000, 7000], [10000, 11000]],
  kiloBasiEk: 1800,
  bolgeKatsayisi: { 1: 1, 2: 1.25, 3: 1.6 },
  asgariUcret: 5000,
};

Üç istemci bu tabloyu kullanır. İlki bir siparişin toplamını çıkarır ve ücreti kendisi kurar.

// acik/siparis.mjs — istemci kademe dizisinin bicimini bilerek ucreti kendisi kurar
import { TARIFE } from "./tarife.mjs";

const ucretliAgirlik = (g) => Math.max(g.agirlikGram, g.hacimCm3 / 3);

export function ucret(gonderi, bolge) {
  const a = ucretliAgirlik(gonderi);
  const kademe = TARIFE.kademeler.find(([ust]) => a <= ust);
  const [sonUst, sonUcret] = TARIFE.kademeler[TARIFE.kademeler.length - 1];
  const taban = kademe !== undefined
    ? kademe[1]
    : sonUcret + Math.ceil((a - sonUst) / 1000) * TARIFE.kiloBasiEk;
  return Math.max(Math.round(taban * TARIFE.bolgeKatsayisi[bolge]), TARIFE.asgariUcret);
}

export const toplam = (gonderiler, bolge) =>
  gonderiler.reduce((t, g) => t + ucret(g, bolge), 0);

İkincisi bir gönderinin bölgelere göre en düşük ve en yüksek ücretini verir; bölge listesini katsayı tablosunun anahtarlarından çıkarır.

// acik/teklif.mjs — istemci bolge listesini katsayi tablosunun anahtarlarindan cikarir
import { TARIFE } from "./tarife.mjs";
import { ucret } from "./siparis.mjs";

export function aralik(gonderi) {
  const ucretler = Object.keys(TARIFE.bolgeKatsayisi).map((b) => ucret(gonderi, Number(b)));
  return { enAz: Math.min(...ucretler), enCok: Math.max(...ucretler) };
}

Üçüncüsü en üst kademeyi aşan ağırlığın ek yükünü toplar.

// acik/kapasite.mjs — istemci en ust kademe sinirini ve kilo basi eki dogrudan okur
import { TARIFE } from "./tarife.mjs";

export function asimEki(gonderiler) {
  const [sonUst] = TARIFE.kademeler[TARIFE.kademeler.length - 1];
  return gonderiler.reduce((t, g) => {
    const a = Math.max(g.agirlikGram, g.hacimCm3 / 3);
    return t + (a <= sonUst ? 0 : Math.ceil((a - sonUst) / 1000) * TARIFE.kiloBasiEk);
  }, 0);
}

İkinci sunumda temsil modülün içinde kalır; dışarıya üç ad açılır. Açılan adlar veri değil, istemcilerin gerçekten sorduğu sorulardır.

// kapali/tarife.mjs — temsil ozel; disariya uc ad acilir
const KADEMELER = [[1000, 4500], [5000, 7000], [10000, 11000]];
const KILO_BASI_EK = 1800;
const BOLGE_KATSAYISI = { 1: 1, 2: 1.25, 3: 1.6 };
const ASGARI_UCRET = 5000;

export function asimEki(agirlik) {
  const sonUst = KADEMELER.at(-1)[0];
  return agirlik <= sonUst ? 0 : Math.ceil((agirlik - sonUst) / 1000) * KILO_BASI_EK;
}

export function ucret(agirlik, bolge) {
  const kademe = KADEMELER.find(([ust]) => agirlik <= ust);
  const taban = kademe !== undefined ? kademe[1] : KADEMELER.at(-1)[1] + asimEki(agirlik);
  return Math.max(Math.round(taban * BOLGE_KATSAYISI[bolge]), ASGARI_UCRET);
}

export const bolgeler = () => Object.keys(BOLGE_KATSAYISI).map(Number);

Aynı üç istemci bu sunumda yalnız çağrı yapar.

// kapali/siparis.mjs — istemci yalnizca ucret adini kullanir, temsili bilmez
import { ucret as tarifeUcreti } from "./tarife.mjs";

const ucretliAgirlik = (g) => Math.max(g.agirlikGram, g.hacimCm3 / 3);

export const ucret = (gonderi, bolge) => tarifeUcreti(ucretliAgirlik(gonderi), bolge);

export const toplam = (gonderiler, bolge) =>
  gonderiler.reduce((t, g) => t + ucret(g, bolge), 0);
// kapali/teklif.mjs — bolge listesi tarifeye sorulur
import { bolgeler } from "./tarife.mjs";
import { ucret } from "./siparis.mjs";

export function aralik(gonderi) {
  const ucretler = bolgeler().map((b) => ucret(gonderi, b));
  return { enAz: Math.min(...ucretler), enCok: Math.max(...ucretler) };
}
// kapali/kapasite.mjs — asim eki tarifeye sorulur
import { asimEki as tarifeAsimi } from "./tarife.mjs";

export function asimEki(gonderiler) {
  return gonderiler.reduce(
    (t, g) => t + tarifeAsimi(Math.max(g.agirlikGram, g.hacimCm3 / 3)), 0);
}

İki Sayı: Açılan Ad ve Bilinen Ayrıntı

Ölçüm, tarifeye ait yedi ayrıntıyı önceden adlandırır ve her istemci kaynağında hangisinin göründüğünü arar. Yedi ayrıntı listenin kendisinde yazılıdır; sayılan şey budur.

// olcum.mjs — disa acilan ad sayisini ve istemcilerin bildigi temsil ayrintisini sayar
import { readFileSync } from "node:fs";

const AYRINTI = [
  ["kademe listesi", /\.kademeler\b/],
  ["kademenin cift olusu", /\(\[ust\]\)|\[sonUst(, sonUcret)?\]|kademe\[1\]/],
  ["kademelerin siralanisi", /kademeler\[TARIFE\.kademeler\.length - 1\]/],
  ["kilo basi ek adi", /\.kiloBasiEk\b/],
  ["katsayi tablosu", /\.bolgeKatsayisi\b/],
  ["bolgelerin anahtar olusu", /Object\.keys\(TARIFE\./],
  ["asgari ucret adi", /\.asgariUcret\b/],
];
const ISTEMCILER = ["siparis.mjs", "teklif.mjs", "kapasite.mjs"];

for (const dizin of ["acik", "kapali"]) {
  const tarife = readFileSync(`${dizin}/tarife.mjs`, "utf8");
  console.log(`${dizin.padEnd(6)} disa acilan ad = ${(tarife.match(/^export /gm) ?? []).length}`);
  const tumu = new Set();
  for (const dosya of ISTEMCILER) {
    const kaynak = readFileSync(`${dizin}/${dosya}`, "utf8");
    const bilinen = AYRINTI.filter(([, k]) => k.test(kaynak)).map(([ad]) => ad);
    bilinen.forEach((a) => tumu.add(a));
    console.log(`  ${dosya.padEnd(13)} bildigi ayrinti = ${bilinen.length}  ${bilinen.join(", ")}`);
  }
  console.log(`  ayri ayrinti toplami = ${tumu.size}`);
}
node olcum.mjs
acik   disa acilan ad = 1
  siparis.mjs   bildigi ayrinti = 6  kademe listesi, kademenin cift olusu, kademelerin siralanisi, kilo basi ek adi, katsayi tablosu, asgari ucret adi
  teklif.mjs    bildigi ayrinti = 2  katsayi tablosu, bolgelerin anahtar olusu
  kapasite.mjs  bildigi ayrinti = 4  kademe listesi, kademenin cift olusu, kademelerin siralanisi, kilo basi ek adi
  ayri ayrinti toplami = 7
kapali disa acilan ad = 3
  siparis.mjs   bildigi ayrinti = 0
  teklif.mjs    bildigi ayrinti = 0
  kapasite.mjs  bildigi ayrinti = 0
  ayri ayrinti toplami = 0

İki sayı ters yönde. Açık sunum bir ad açıyor, kapalı sunum üç. Ad sayısına bakılırsa açık sunum daha dar bir yüzeye sahip görünür. Ayrıntı sayısına bakıldığında tablo tersine döner: açık sunumun istemcileri yedi ayrı temsil ayrıntısını biliyor, kapalı sunumunkiler hiçbirini. Dışa açılan ad sayısı bir soyutlama ölçütü değildir; ölçüt, adların arkasında ne kaldığıdır.

Karşılaştırmanın anlamlı olması için iki sunumun aynı sonucu ürettiğinin gösterilmesi gerekir. Aşağıdaki denetleyici üç istemciyi iki dizinde de çalıştırır ve bilinen değerlerle karşılaştırır.

// kontrol.mjs — uc istemciyi iki dizinde de calistirip bilinen degerlerle karsilastirir
const GONDERILER = [
  { agirlikGram: 800, hacimCm3: 900 },
  { agirlikGram: 4000, hacimCm3: 30000 },
  { agirlikGram: 12000, hacimCm3: 900 },
];
const BEKLENEN = { "siparis.mjs": 37625, "teklif.mjs": "5000/7200", "kapasite.mjs": 3600 };

async function dene(dizin, dosya) {
  if (dosya === "siparis.mjs") {
    const { toplam } = await import(`./${dizin}/siparis.mjs`);
    return toplam(GONDERILER, 2);
  }
  if (dosya === "teklif.mjs") {
    const { aralik } = await import(`./${dizin}/teklif.mjs`);
    const a = aralik(GONDERILER[0]);
    return `${a.enAz}/${a.enCok}`;
  }
  const { asimEki } = await import(`./${dizin}/kapasite.mjs`);
  return asimEki(GONDERILER);
}

for (const dizin of ["acik", "kapali"]) {
  const kirik = [];
  for (const dosya of Object.keys(BEKLENEN)) {
    let sonuc;
    try { sonuc = await dene(dizin, dosya); } catch (e) { sonuc = e.constructor.name; }
    if (sonuc !== BEKLENEN[dosya]) kirik.push(`${dosya}->${sonuc}`);
  }
  console.log(`${dizin.padEnd(6)} kirilan istemci = ${kirik.length}  ${kirik.join(" ")}`);
}
node kontrol.mjs
acik   kirilan istemci = 0
kapali kirilan istemci = 0

Aynı sonuçlar üretiliyorsa, gizlemenin kod miktarına bedeli de sorulabilir. Sanılanın aksine bu örnekte bedel negatiftir.

for d in acik kapali; do
  echo "$d  bos olmayan satir = $(cat $d/tarife.mjs $d/siparis.mjs $d/teklif.mjs $d/kapasite.mjs | grep -c .)"
done
acik  bos olmayan satir = 37
kapali  bos olmayan satir = 34

Kapalı sunum üç satır daha kısa. Modüle eklenen üç işlev gövdesi, istemcilerden kalkan tekrar eden temsil bilgisinden daha az yer tuttu. Kod miktarı, ayrıntı sayısı yeterince çok istemciye yayıldığında soyutlama lehine döner.

Ayrıntı Değiştiğinde

Yedi ayrıntının ikisi değişir: kademeler adlandırılmış alanlara geçer ve bölge katsayıları bir listeye dönüşür. Tarifenin anlamı aynı kalır — aynı kademeler, aynı katsayılar.

// acik/tarife-v2.mjs — ayni tarife: kademeler adli alanlarla, bolgeler liste olarak
export const TARIFE = {
  kademeler: [
    { ustGram: 1000, ucret: 4500 },
    { ustGram: 5000, ucret: 7000 },
    { ustGram: 10000, ucret: 11000 },
  ],
  kiloBasiEk: 1800,
  bolgeKatsayisi: [{ bolge: 1, katsayi: 1 }, { bolge: 2, katsayi: 1.25 }, { bolge: 3, katsayi: 1.6 }],
  asgariUcret: 5000,
};
// kapali/tarife-v2.mjs — ayni temsil degisikligi; disa acilan uc ad aynen korunur
const KADEMELER = [
  { ustGram: 1000, ucret: 4500 },
  { ustGram: 5000, ucret: 7000 },
  { ustGram: 10000, ucret: 11000 },
];
const KILO_BASI_EK = 1800;
const BOLGE_KATSAYISI = [{ bolge: 1, katsayi: 1 }, { bolge: 2, katsayi: 1.25 }, { bolge: 3, katsayi: 1.6 }];
const ASGARI_UCRET = 5000;

export function asimEki(agirlik) {
  const sonUst = KADEMELER.at(-1).ustGram;
  return agirlik <= sonUst ? 0 : Math.ceil((agirlik - sonUst) / 1000) * KILO_BASI_EK;
}

export function ucret(agirlik, bolge) {
  const kademe = KADEMELER.find((k) => agirlik <= k.ustGram);
  const taban = kademe !== undefined ? kademe.ucret : KADEMELER.at(-1).ucret + asimEki(agirlik);
  return Math.max(Math.round(taban * BOLGE_KATSAYISI.find((b) => b.bolge === bolge).katsayi), ASGARI_UCRET);
}

export const bolgeler = () => BOLGE_KATSAYISI.map((b) => b.bolge);

Her iki dizinde de değiştirilen dosya sayısı birdir. Aynı denetleyici yeniden çalıştırılır.

cp acik/tarife-v2.mjs acik/tarife.mjs
cp kapali/tarife-v2.mjs kapali/tarife.mjs
node kontrol.mjs
acik   kirilan istemci = 3  siparis.mjs->TypeError teklif.mjs->TypeError kapasite.mjs->TypeError
kapali kirilan istemci = 0

Tek dosyada yapılan bir değişiklik açık sunumda üç istemcinin üçünü de kırdı, kapalı sunumda hiçbirini. Kırılma sayısı, ölçülen yedi ayrıntının doğrudan sonucudur: bir istemci bir ayrıntıyı biliyorsa, o ayrıntı değiştiğinde kırılır.

Buradaki kırılmaların hepsi çalışma zamanı hatası verdi; sessiz kalmadı. Bu, temsil değişikliğinin tipini bozacak kadar büyük olmasındandır. Daha küçük değişiklikler — kademelerin sıralamasının bozulması, birim değişimi — aynı kaynaklarda hata değil yanlış sayı üretir; kırılan istemci sayısı yine üç olur, ama sayaç yerine bir doğrulama gerektirir.

Gizlemenin Bedeli

Soyutlama bedava değildir. Kod miktarı bedel değildi — ölçüm 37’ye karşı 34 satır verdi. Gerçek bedel, doğrudan ölçülen üç sayısıdır: kapalı modül üç soru yanıtlar. Dördüncü bir soru — tarife tablosunun belge olarak yazdırılması, kademe sınırlarının bir arayüzde listelenmesi — açık sunumda modüle hiç dokunmadan yanıtlanır, kapalı sunumda modülün değiştirilmesini gerektirir. Gizleme, yeni ihtiyacın maliyetini istemciden modül sahibine taşır.

Bu bedel, soyutlamanın yanlış çizildiği durumda ödenip karşılığı alınmaz. Modül kademe listesini olduğu gibi döndüren bir dördüncü ad açarsa, ölçülen sıfır yeniden yediye doğru tırmanır: gizlenen temsil, adı değişmiş biçimde yeniden dışarı çıkar. Bu duruma sızdıran soyutlama (leaky abstraction) denir. Dördüncü sorunun doğru yanıtı, iç listeyi vermek değil, sorulan soruyu modülün kendi sözcükleriyle karşılayan bir görünüm döndürmektir — alanları modülün tanımladığı, iç temsille bağı olmayan bir özet.

Özet

  • Dışa açılan ad sayısı tek başına soyutlama ölçütü değildir: açık sunum bir ad açtı, kapalı sunum üç; buna rağmen ayrıntı bakımından ilişki tersineydi.
  • İstemcilerin bildiği temsil ayrıntısı açık sunumda yedi, kapalı sunumda sıfır ölçüldü; bu sayı, değişimde kırılacak istemci sayısını önceden verir.
  • Tek dosyada yapılan bir temsil değişikliği açık sunumda üç istemcinin üçünü de kırdı, kapalı sunumda hiçbirini kırmadı.
  • Soyutlamanın kod bedeli bu örnekte negatif çıktı (37’ye karşı 34 satır); ayrıntı yeterince çok istemciye yayıldığında gizleme kodu kısaltır.
  • Gerçek bedel modülün yanıtladığı soru sayısıyla ölçülür: kapalı modül üç soru yanıtlar, dördüncüsü modülün değiştirilmesini gerektirir.
  • İç temsili adı değişmiş biçimde geri veren bir ad, sızdıran soyutlamadır; ayrıntı sayısını sıfırdan geri tırmandırır.

Sonraki Adım

Kapalı tarife modülü tek bir tarife biçimini gizliyor. Gerçek bir ücretlendirme kitaplığında tarife türü birden fazladır: yurt içi kademe tarifesi, aktarmalı tarife, sözleşmeli müşteri tarifesi. Aynı adları farklı gerçekleştirimlerle karşılamanın yollarından biri, bir türü bir başkasından türetmektir. Sonraki ders bu ilişkiyi ele alır ve türetmenin ne zaman gerçek bir alt tip ürettiğini ölçer: aynı üst tip üzerinden çalışan bir istemci, sözleşmeye uyan ve uymayan iki alt sınıfla koşturulur ve uymayan sürümün kaç yanlış sonuç ürettiği sayılır.

İ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