İçeriğe geç
academia.sh

Ders 09 / 16

Kapsam ve Görünürlük

En dar görünürlük ilkesinin ithal grafiğiyle ölçülmesi: aynı ücret paketinin her adı dışa açan ve yalnız iki ad açan iki sürümünde dışa açılan yüzeyin, bağımlı dosya sayısının, ölü yüzeyin ve serbestçe değiştirilebilir ad sayısının karşılaştırılması.

İçindekiler

İmza kararları bir fonksiyonun dışarıya ne söylediğini belirliyor. Aynı soru modül düzeyinde de sorulur: bir modül dışarıya kaç ad açıyor? Önceki dersteki bolunmus/ucret.mjs dosyasında temelUcret dışa aktarılmamış, iki ücret fonksiyonu aktarılmıştı. Bu karar keyfî değil.

Kapsam (scope), bir adın görülebildiği kod bölgesidir. Bir modül dosyasında tanımlanan her ad, varsayılan olarak yalnız o dosyada görünür; export sözcüğü o adı dosyanın dışına taşır. Dışa açılan her ad, modülün geri alamayacağı bir sözdür: başka bir dosya o adı kullandığı anda ad artık modülün kendi malı değildir. Bu ders o sözün bedelini sayıyor.

Her Adı Açan Paket

Ücret paketi dört dosyadan oluşuyor: hesabın kendisi ve onu kullanan üç modül — rapor, etiket, fatura. Birinci sürümde hesap modülü her adını dışa açıyor.

mkdir -p genis dar
// genis/ucret.mjs — modulun her adi disa acilmis
export const HACIMSEL_BOLEN = 3000;
export const ASGARI_UCRET = 52;
export const BOLGE_KATSAYILARI = { 1: 1, 2: 1.35, 3: 1.8 };
export const AGIRLIK_KADEMELERI = [{ ustSinirKg: 1, kiloBasiUcret: 38 },
  { ustSinirKg: 5, kiloBasiUcret: 26 }, { ustSinirKg: 10, kiloBasiUcret: 21 },
  { ustSinirKg: 30, kiloBasiUcret: 17 }];

export function hacimselAgirlikKg(gonderi) {
  return (gonderi.enCm * gonderi.boyCm * gonderi.yukseklikCm) / HACIMSEL_BOLEN;
}

export function ucretlendirilenAgirlikKg(gonderi) {
  return Math.max(gonderi.agirlikKg, hacimselAgirlikKg(gonderi));
}

export function kademeSec(agirlikKg) {
  return AGIRLIK_KADEMELERI.find((k) => agirlikKg <= k.ustSinirKg);
}

export function kademeUcreti(agirlikKg) {
  return kademeSec(agirlikKg).kiloBasiUcret * agirlikKg;
}

export function ucretHesapla(gonderi) {
  const agirlikKg = ucretlendirilenAgirlikKg(gonderi);
  return Math.max(kademeUcreti(agirlikKg) * BOLGE_KATSAYILARI[gonderi.bolgeKodu], ASGARI_UCRET);
}

Açık olan her ad, kullanılabilir olduğu için kullanılır.

// genis/rapor.mjs — iki ic ada ve bir sabite dogrudan basvuruyor
import { ucretHesapla, kademeSec, AGIRLIK_KADEMELERI } from "./ucret.mjs";

export function rapor(gonderiler) {
  const satirlar = gonderiler.map((g) =>
    `${g.kod} kademe<=${kademeSec(g.agirlikKg).ustSinirKg}kg ucret=${ucretHesapla(g).toFixed(2)}`);
  return satirlar.concat(`kademe sayisi=${AGIRLIK_KADEMELERI.length}`);
}
// genis/etiket.mjs — hacimsel bolene dogrudan basvuruyor
import { ucretlendirilenAgirlikKg, HACIMSEL_BOLEN } from "./ucret.mjs";

export function etiket(gonderi) {
  return `${gonderi.kod} ${ucretlendirilenAgirlikKg(gonderi).toFixed(2)}kg (bolen ${HACIMSEL_BOLEN})`;
}
// genis/fatura.mjs — kademe ucretini ayrica hesaplatiyor
import { ucretHesapla, kademeUcreti } from "./ucret.mjs";

export function fatura(gonderi) {
  const agirlikKg = Math.max(gonderi.agirlikKg,
    (gonderi.enCm * gonderi.boyCm * gonderi.yukseklikCm) / 3000);
  return `${gonderi.kod} kademe=${kademeUcreti(agirlikKg).toFixed(2)} toplam=${ucretHesapla(gonderi).toFixed(2)}`;
}

fatura.mjs dikkat çekici: hacimsel bölen dışa açık olmasına rağmen o dosyada 3000 sayısı elle yazılmış. Yüzeyin geniş olması tutarlılığı garanti etmiyor, yalnız seçenek sayısını arttırıyor.

İki Adı Açan Paket

İkinci sürümde hesap modülü iki ad açıyor: ücretin kendisi ve hesabın dökümü. Kullanan modüllerin ihtiyacı olan ara değerler dökümde adlarıyla veriliyor; modülün sabitleri ve ara fonksiyonları dışarı çıkmıyor.

// dar/ucret.mjs — iki ad disa acik, gerisi modulun ici
const HACIMSEL_BOLEN = 3000;
const ASGARI_UCRET = 52;
const BOLGE_KATSAYILARI = { 1: 1, 2: 1.35, 3: 1.8 };
const AGIRLIK_KADEMELERI = [{ ustSinirKg: 1, kiloBasiUcret: 38 },
  { ustSinirKg: 5, kiloBasiUcret: 26 }, { ustSinirKg: 10, kiloBasiUcret: 21 },
  { ustSinirKg: 30, kiloBasiUcret: 17 }];

function hacimselAgirlikKg(gonderi) {
  return (gonderi.enCm * gonderi.boyCm * gonderi.yukseklikCm) / HACIMSEL_BOLEN;
}

function agirlikKg(gonderi) {
  return Math.max(gonderi.agirlikKg, hacimselAgirlikKg(gonderi));
}

function kademeSec(kg) {
  return AGIRLIK_KADEMELERI.find((k) => kg <= k.ustSinirKg);
}

function kademeUcreti(kg) {
  return kademeSec(kg).kiloBasiUcret * kg;
}

export function ucretHesapla(gonderi) {
  const kg = agirlikKg(gonderi);
  return Math.max(kademeUcreti(kg) * BOLGE_KATSAYILARI[gonderi.bolgeKodu], ASGARI_UCRET);
}

export function ucretDokumu(gonderi) {
  const kg = agirlikKg(gonderi);
  return { agirlikKg: kg, hacimselBolen: HACIMSEL_BOLEN, kademeSayisi: AGIRLIK_KADEMELERI.length,
    kademeUstSinirKg: kademeSec(kg).ustSinirKg, kademeUcreti: kademeUcreti(kg) };
}
// dar/rapor.mjs — yalnizca dokum ve ucret uzerinden
import { ucretHesapla, ucretDokumu } from "./ucret.mjs";

export function rapor(gonderiler) {
  const satirlar = gonderiler.map((g) =>
    `${g.kod} kademe<=${ucretDokumu(g).kademeUstSinirKg}kg ucret=${ucretHesapla(g).toFixed(2)}`);
  return satirlar.concat(`kademe sayisi=${ucretDokumu(gonderiler[0]).kademeSayisi}`);
}
// dar/etiket.mjs — bolen degeri dokumden geliyor
import { ucretDokumu } from "./ucret.mjs";

export function etiket(gonderi) {
  const dokum = ucretDokumu(gonderi);
  return `${gonderi.kod} ${dokum.agirlikKg.toFixed(2)}kg (bolen ${dokum.hacimselBolen})`;
}
// dar/fatura.mjs — kademe ucreti dokumden geliyor
import { ucretHesapla, ucretDokumu } from "./ucret.mjs";

export function fatura(gonderi) {
  return `${gonderi.kod} kademe=${ucretDokumu(gonderi).kademeUcreti.toFixed(2)}` +
    ` toplam=${ucretHesapla(gonderi).toFixed(2)}`;
}

İki paketin aynı çıktıyı ürettiği doğrulanır.

// karsilastir.mjs — iki paketin ayni ciktiyi urettigi dogrulanir
import { rapor as genisRapor } from "./genis/rapor.mjs";
import { etiket as genisEtiket } from "./genis/etiket.mjs";
import { fatura as genisFatura } from "./genis/fatura.mjs";
import { rapor as darRapor } from "./dar/rapor.mjs";
import { etiket as darEtiket } from "./dar/etiket.mjs";
import { fatura as darFatura } from "./dar/fatura.mjs";

const GONDERILER = [
  { kod: "GN-4172", agirlikKg: 2.4, enCm: 30, boyCm: 24, yukseklikCm: 18, bolgeKodu: 2 },
  { kod: "GN-4173", agirlikKg: 0.4, enCm: 10, boyCm: 10, yukseklikCm: 10, bolgeKodu: 1 },
];

const genis = [...genisRapor(GONDERILER), ...GONDERILER.map(genisEtiket), ...GONDERILER.map(genisFatura)];
const dar = [...darRapor(GONDERILER), ...GONDERILER.map(darEtiket), ...GONDERILER.map(darFatura)];

for (const [i, satir] of genis.entries()) {
  console.log(`${satir === dar[i] ? "ayni  " : "FARKLI"} ${satir}`);
}
ayni   GN-4172 kademe<=5kg ucret=151.63
ayni   GN-4173 kademe<=1kg ucret=52.00
ayni   kademe sayisi=4
ayni   GN-4172 4.32kg (bolen 3000)
ayni   GN-4173 0.40kg (bolen 3000)
ayni   GN-4172 kademe=112.32 toplam=151.63
ayni   GN-4173 kademe=15.20 toplam=52.00

Yüzey Ölçer

Ölçer bir dizindeki modülleri okur, her modülün dışa açtığı ve içeride tuttuğu adları çıkarır, sonra ithal satırlarından hangi adın hangi dosyalarca kullanıldığını sayar. Son sütun, modülün kendi başına değiştirebileceği ad sayısıdır: dışarıdan bağımlısı olmayan her ad bu sayıya girer.

// yuzey-olc.mjs — modullerin disa actigi ad sayisi, bu adlarin bagimlilari ve degisim ozgurlugu
import { readFileSync, readdirSync } from "node:fs";

const acikAdlar = (m) => [...m.matchAll(/^export\s+(?:const|function|class)\s+(\w+)/gm)].map((e) => e[1]);
const icAdlar = (m) => [...m.matchAll(/^(?:const|function|class)\s+(\w+)/gm)].map((e) => e[1]);
const ithaller = (m) => [...m.matchAll(/import\s*{([^}]*)}\s*from\s*"\.\/([\w.-]+)"/g)]
  .map((e) => [e[2], e[1].split(",").map((s) => s.trim()).filter(Boolean)]);

for (const dizin of process.argv.slice(2)) {
  const dosyalar = readdirSync(dizin).filter((a) => a.endsWith(".mjs")).sort();
  const metin = Object.fromEntries(dosyalar.map((a) => [a, readFileSync(`${dizin}/${a}`, "utf8")]));
  const kullanim = new Map();
  for (const m of Object.values(metin)) {
    for (const [hedef, adlar] of ithaller(m)) {
      for (const ad of adlar) kullanim.set(`${hedef}:${ad}`, (kullanim.get(`${hedef}:${ad}`) ?? 0) + 1);
    }
  }
  console.log(dizin);
  for (const dosya of dosyalar) {
    const acik = acikAdlar(metin[dosya]);
    const ic = icAdlar(metin[dosya]);
    const bagimli = acik.map((ad) => [ad, kullanim.get(`${dosya}:${ad}`) ?? 0]);
    const bagli = bagimli.filter(([, n]) => n > 0);
    console.log(`  ${dosya.padEnd(11)} disa acik=${acik.length}  ic ad=${ic.length}` +
      `  bagimlisi olan=${bagli.length}  olu yuzey=${acik.length - bagli.length}` +
      `  serbestce degistirilebilir=${acik.length + ic.length - bagli.length}`);
    for (const [ad, n] of bagli) console.log(`    ${ad.padEnd(24)} bagimli dosya=${n}`);
  }
}
node yuzey-olc.mjs genis dar
genis
  etiket.mjs  disa acik=1  ic ad=0  bagimlisi olan=0  olu yuzey=1  serbestce degistirilebilir=1
  fatura.mjs  disa acik=1  ic ad=0  bagimlisi olan=0  olu yuzey=1  serbestce degistirilebilir=1
  rapor.mjs   disa acik=1  ic ad=0  bagimlisi olan=0  olu yuzey=1  serbestce degistirilebilir=1
  ucret.mjs   disa acik=9  ic ad=0  bagimlisi olan=6  olu yuzey=3  serbestce degistirilebilir=3
    HACIMSEL_BOLEN           bagimli dosya=1
    AGIRLIK_KADEMELERI       bagimli dosya=1
    ucretlendirilenAgirlikKg bagimli dosya=1
    kademeSec                bagimli dosya=1
    kademeUcreti             bagimli dosya=1
    ucretHesapla             bagimli dosya=2
dar
  etiket.mjs  disa acik=1  ic ad=0  bagimlisi olan=0  olu yuzey=1  serbestce degistirilebilir=1
  fatura.mjs  disa acik=1  ic ad=0  bagimlisi olan=0  olu yuzey=1  serbestce degistirilebilir=1
  rapor.mjs   disa acik=1  ic ad=0  bagimlisi olan=0  olu yuzey=1  serbestce degistirilebilir=1
  ucret.mjs   disa acik=2  ic ad=8  bagimlisi olan=2  olu yuzey=0  serbestce degistirilebilir=8
    ucretHesapla             bagimli dosya=2
    ucretDokumu              bagimli dosya=3

Hesap modülünün yüzeyi dokuzdan ikiye indi. Bundan daha önemlisi son sütun: geniş sürümde modül kendi adlarının üçünü, dar sürümde sekizini kimseye sormadan değiştirebilir. Aradaki fark, altı adın dışarıdan bağlanmış olmasıdır.

Üç kullanıcı modülün olu yuzey=1 görünmesi ölçümün sınırıdır: bunlar paketin dış yüzüdür, kendi içinde kimse onları kullanmaz. Ölçü yalnız verilen dizindeki bağları görür.

Yeniden Adlandırma Maliyeti

Değişim özgürlüğü soyut bir kavram değil. Bir adı yeniden adlandırmanın maliyeti, o adın geçtiği dosya sayısıdır.

for d in genis dar; do
  echo "$d: kademeSec $(grep -rl kademeSec $d | wc -l | tr -d ' ') dosyada," \
    "HACIMSEL_BOLEN $(grep -rl HACIMSEL_BOLEN $d | wc -l | tr -d ' ') dosyada"
done
genis: kademeSec 2 dosyada, HACIMSEL_BOLEN 2 dosyada
dar: kademeSec 1 dosyada, HACIMSEL_BOLEN 1 dosyada

Fark iki dosyaya karşı bir dosya gibi görünüyor, çünkü paket küçük. Ölçünün asıl anlamı oranda: dar sürümde bu iki ad modülün içindedir ve dışarıdan kaç dosya olursa olsun sayı bir kalır. Geniş sürümde sayı, o adı ithal eden dosya sayısıyla birlikte büyür.

Kademe seçiminin Map tabanlı bir yapıya dönüştürülmesi, hacimsel bölenin taşıyıcıya göre değişen bir alana taşınması, asgari ücretin tarifeden okunması — bunların hepsi dar sürümde modülün iç işidir. Geniş sürümde her biri dışarıdaki dosyaları da değiştirmeyi gerektirir.

En Dar Görünürlük İlkesi

İlke tek cümledir: bir ad, işini görebilecek en dar kapsamda tanımlanır. Uygulaması üç adımdır.

Varsayılan kapalıdır. Yeni bir ad önce dosyanın içinde kalır. Dışa açma, bir kullanıcı gerçekten ortaya çıktığında ve o kullanıcının başka yolu olmadığında yapılır.

Genişletmek kolay, daraltmak zordur. Kapalı bir adı açmak tek satırlık bir değişikliktir ve hiçbir dosyayı bozmaz. Açık bir adı kapatmak, onu kullanan her dosyayı değiştirmeyi gerektirir; ölçümdeki bagimli dosya sütunu tam olarak bu maliyettir.

Yüzey, ara değerleri değil kararları taşır. ucretDokumu bunun örneğiydi: kullanan modüllerin ihtiyacı ara fonksiyonlar değil, hesabın sonuçlarıydı. Dışarıya bir sabit vermek yerine o sabitin belirlediği sonucu vermek, sabitin değişme özgürlüğünü korur.

Aynı ilke fonksiyon içinde de işler. Bir değişken kullanıldığı bloğun dışında tanımlandığında, o bloğun dışındaki her satır onu değiştirebilir hâle gelir; okuyan kişi de değerin ne olduğunu anlamak için aradaki satırların hepsini izlemek zorunda kalır.

Özet

  • Dışa açılan her ad geri alınması pahalı bir sözdür; ölçüsü, o adı ithal eden dosya sayısıdır.
  • Aynı davranışı üreten iki pakette hesap modülünün yüzeyi dokuz ve iki addı; ölü yüzey üçten sıfıra indi.
  • Modülün kimseye sormadan değiştirebileceği ad sayısı üçten sekize çıktı; aradaki fark, dışarıdan bağlanmış altı addır.
  • Ara değerleri değil kararları dışa açmak sabitlerin değişme özgürlüğünü korur; ucretDokumu sabitleri değil sonuçları veriyor.
  • Genişletmek tek satırlık bir değişikliktir, daraltmak her bağımlı dosyayı değiştirmeyi gerektirir; bu yüzden varsayılan kapalıdır.

Sonraki Adım

Görünürlük kararı bir modülün dışarıya ne verdiğini belirliyor. Bir soru daha kalıyor: bu modüller hangi ölçüte göre ayrıldı? Ücret hesabı bir dosyada, rapor başka bir dosyada duruyor; ayrım işlevsel görünüyor. Ancak bir kod tabanını asıl zorlayan şey, farklı kişilerin farklı nedenlerle aynı dosyaya dokunmasıdır. Muhasebenin istediği bir yuvarlama değişikliği ile operasyonun istediği bir kademe değişikliği aynı dosyada buluşuyorsa, iki karar birbirini bekler. Sonraki ders bu buluşmayı ölçer: iki farklı değişim nedeni aynı dosyaya kaç kez dokunuyor ve kod aktörlere göre düzenlendiğinde bu sayı ne oluyor.

İ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