Ders 13 / 14
Yan Etkilerin Sınıra İtilmesi
Saf çekirdek, kirli kabuk düzeni: dosya okuma, saat ve kayıt yazmanın hesabın içinden dış katmana taşınması, iki düzende sahte bağımlılık gerektiren deneme sayısının ve dış dünyaya dokunan satır sayısının karşılaştırılması.
İçindekiler
Önceki dersin dört adımı da saftı: hiçbiri okumadığı bir şeye bakmadı, hiçbiri dışarı yazmadı. Gerçek bir ücretlendirme koşumu ise saf değildir. Gönderiler bir dosyadan okunur, gece tarifesinin geçerli olup olmadığı saate bakılarak belirlenir, sonuçlar bir dosyaya ve bir kayıt akışına yazılır.
Bu üç bağımlılık ortadan kaldırılamaz; bir programın işe yaraması için dış dünyayla konuşması gerekir. Kaldırılabilecek olan, bunların ücret kurallarıyla aynı yerde durmasıdır. Bu ders o ayrımı kurar ve iki düzeni ölçer: çekirdeğin sınanması için kaç sahte bağımlılık gerekiyor, dış dünyaya dokunan satırlar hangi dosyalarda toplanıyor.
Serpiştirilmiş Düzen
İlk düzen doğal olanıdır: koşum baştan sona yazılır, her şey geldiği yerde yapılır. Tarife verisi iki tasarımın da ortak girdisidir ve ayrı bir dosyada durur.
// tarife.mjs — iki tasarimin da kullandigi tarife verisi export const TARIFE = { asgariUcret: 4990, geceEki: 1500, kademeler: [ { ustAgirlikGram: 1000, ucret: 4990 }, { ustAgirlikGram: 5000, ucret: 6490 }, { ustAgirlikGram: 15000, ucret: 10900 }, ], bolgeKatsayisi: { 1: 1.0, 2: 1.15, 3: 1.35 }, };
// icice.mjs — dosya okuma, saat ve kayit yazma hesabin icine serpistirilmis import fs from "node:fs"; import { TARIFE } from "./tarife.mjs"; export function gunlukKosum(girdiYolu, ciktiYolu, kayitYolu) { const gonderiler = JSON.parse(fs.readFileSync(girdiYolu, "utf8")); const saat = new Date(Date.now()).getUTCHours(); const satirlar = []; let toplam = 0; for (const g of gonderiler) { const kademe = TARIFE.kademeler.find((k) => g.agirlikGram <= k.ustAgirlikGram) ?? TARIFE.kademeler.at(-1); let ucret = Math.round(kademe.ucret * TARIFE.bolgeKatsayisi[g.bolge]); if (g.sozlesmeli) ucret = Math.round(ucret * 0.88); if (saat >= 20 || saat < 6) ucret += TARIFE.geceEki; ucret = Math.max(TARIFE.asgariUcret, ucret); satirlar.push(`${g.kod},${ucret}`); toplam += ucret; fs.appendFileSync(kayitYolu, `${g.kod} ucretlendirildi\n`); } fs.writeFileSync(ciktiYolu, `${satirlar.join("\n")}\n`); fs.appendFileSync(kayitYolu, `toplam ${toplam}\n`); return toplam; }
Bu gövdede beş satır dış dünyaya dokunur: bir okuma, bir saat sorgusu, iki kayıt ekleme ve bir yazma. Kalan satırlar ücret kurallarıdır. İkisi iç içe geçtiği için kurallara ulaşmanın tek yolu koşumun tamamını çalıştırmaktır.
Saf Çekirdek, Kirli Kabuk
İkinci düzen aynı işi iki parçaya böler. Çekirdek, kararı veren saf fonksiyondur: gönderileri, tarifeyi ve saati parametre olarak alır; yazılacak satırları, düşülecek kayıt satırlarını ve toplamı döndürür. Hiçbirini kendisi yapmaz.
// cekirdek.mjs — saf cekirdek: gonderiler, tarife ve saat disaridan verilir export function planla({ gonderiler, tarife, saat }) { const gece = saat >= 20 || saat < 6; const satirlar = []; const kayitlar = []; let toplam = 0; for (const g of gonderiler) { const kademe = tarife.kademeler.find((k) => g.agirlikGram <= k.ustAgirlikGram) ?? tarife.kademeler.at(-1); let ucret = Math.round(kademe.ucret * tarife.bolgeKatsayisi[g.bolge]); if (g.sozlesmeli) ucret = Math.round(ucret * 0.88); if (gece) ucret += tarife.geceEki; ucret = Math.max(tarife.asgariUcret, ucret); satirlar.push(`${g.kod},${ucret}`); kayitlar.push(`${g.kod} ucretlendirildi`); toplam += ucret; } return { satirlar, kayitlar: [...kayitlar, `toplam ${toplam}`], toplam }; }
Kritik ayrım döndürülen değerdedir. Çekirdek “şu satırlar yazılsın” diye bir tanım üretir; yazma işini yapmaz. Kayıt satırları da metin olarak döner, akışa basılmaz. Saat bir sayıdır, sorgulanan bir kaynak değildir.
Kabuk bu tanımı alır ve gerçekleştirir. Okuma, saat ve yazma yalnızca burada geçer.
// kabuk.mjs — kirli kabuk: okuma, saat ve yazma yalnizca bu dosyada import fs from "node:fs"; import { TARIFE } from "./tarife.mjs"; import { planla } from "./cekirdek.mjs"; export function gunlukKosum(girdiYolu, ciktiYolu, kayitYolu) { const gonderiler = JSON.parse(fs.readFileSync(girdiYolu, "utf8")); const saat = new Date(Date.now()).getUTCHours(); const plan = planla({ gonderiler, tarife: TARIFE, saat }); fs.writeFileSync(ciktiYolu, `${plan.satirlar.join("\n")}\n`); fs.appendFileSync(kayitYolu, `${plan.kayitlar.join("\n")}\n`); return plan.toplam; }
Kabuğun içinde tek bir iş kuralı yoktur: okur, çağırır, yazar. Karar veren her satır çekirdektedir, karar vermeyen her satır kabuktadır.
Testin Ödediği Bedel
Fark testte sayılabilir hâle gelir. Serpiştirilmiş düzende ücret kurallarına ulaşmanın tek
yolu gunlukKosum çağrısıdır; o çağrı da dosya okur, saate bakar ve dosya yazar. Bunların
dördü de denemede yerine sahtesi konarak susturulmak zorundadır.
// icice.test.mjs — her deneme dort sahte bagimlilik kurar import test from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; import { gunlukKosum } from "./icice.mjs"; const GONDERILER = [ { kod: "TR-4471", agirlikGram: 800, bolge: 1, sozlesmeli: true }, { kod: "TR-4472", agirlikGram: 3200, bolge: 2, sozlesmeli: true }, { kod: "TR-4473", agirlikGram: 12400, bolge: 3, sozlesmeli: false }, ]; function sahteOrtamda(gonderiler, saatUTC, is) { const asil = { oku: fs.readFileSync, yaz: fs.writeFileSync, ekle: fs.appendFileSync, saat: Date.now }; const yazilan = { csv: "", kayit: [] }; fs.readFileSync = () => JSON.stringify(gonderiler); fs.writeFileSync = (_, icerik) => { yazilan.csv = icerik; }; fs.appendFileSync = (_, icerik) => { yazilan.kayit.push(icerik); }; Date.now = () => Date.UTC(2024, 0, 15, saatUTC); try { return { sonuc: is(), yazilan }; } finally { fs.readFileSync = asil.oku; fs.writeFileSync = asil.yaz; fs.appendFileSync = asil.ekle; Date.now = asil.saat; } } test("gunduz kosumunda gece eki uygulanmaz", () => { // SAHTE ORTAM const { sonuc, yazilan } = sahteOrtamda(GONDERILER, 10, () => gunlukKosum("g.json", "c.csv", "k.log")); assert.equal(sonuc, 26272); assert.equal(yazilan.csv, "TR-4471,4990\nTR-4472,6567\nTR-4473,14715\n"); }); test("gece kosumunda her gonderiye sabit ek biner", () => { // SAHTE ORTAM const { sonuc } = sahteOrtamda(GONDERILER, 22, () => gunlukKosum("g.json", "c.csv", "k.log")); assert.equal(sonuc, 30173); }); test("indirim asgari ucret tabanini delemez", () => { // SAHTE ORTAM const tek = [{ kod: "TR-4477", agirlikGram: 500, bolge: 1, sozlesmeli: true }]; const { sonuc } = sahteOrtamda(tek, 10, () => gunlukKosum("g.json", "c.csv", "k.log")); assert.equal(sonuc, 4990); });
Ayrılmış düzende aynı üç kural doğrudan planla çağrısıyla sınanır. Sahte ortam yalnızca
kabuğun kendi denemesinde kurulur: kabuğun sınanacak tek davranışı, okuduğunu çekirdeğe
verip döndüğünü yazmasıdır.
// kabuk-duzen.test.mjs — ucret kurallari sahtesiz sinanir, sahte yalnizca kablolamada import test from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; import { TARIFE } from "./tarife.mjs"; import { planla } from "./cekirdek.mjs"; import { gunlukKosum } from "./kabuk.mjs"; const GONDERILER = [ { kod: "TR-4471", agirlikGram: 800, bolge: 1, sozlesmeli: true }, { kod: "TR-4472", agirlikGram: 3200, bolge: 2, sozlesmeli: true }, { kod: "TR-4473", agirlikGram: 12400, bolge: 3, sozlesmeli: false }, ]; test("gunduz kosumunda gece eki uygulanmaz", () => { const plan = planla({ gonderiler: GONDERILER, tarife: TARIFE, saat: 10 }); assert.equal(plan.toplam, 26272); assert.deepEqual(plan.satirlar, ["TR-4471,4990", "TR-4472,6567", "TR-4473,14715"]); }); test("gece kosumunda her gonderiye sabit ek biner", () => { assert.equal(planla({ gonderiler: GONDERILER, tarife: TARIFE, saat: 22 }).toplam, 30173); }); test("indirim asgari ucret tabanini delemez", () => { const tek = [{ kod: "TR-4477", agirlikGram: 500, bolge: 1, sozlesmeli: true }]; assert.equal(planla({ gonderiler: tek, tarife: TARIFE, saat: 10 }).toplam, 4990); }); test("kabuk okudugunu cekirdege verir, dondugunu yazar", () => { // SAHTE ORTAM const asil = { oku: fs.readFileSync, yaz: fs.writeFileSync, ekle: fs.appendFileSync, saat: Date.now }; let csv = ""; fs.readFileSync = () => JSON.stringify(GONDERILER); fs.writeFileSync = (_, icerik) => { csv = icerik; }; fs.appendFileSync = () => {}; Date.now = () => Date.UTC(2024, 0, 15, 10); try { assert.equal(gunlukKosum("g.json", "c.csv", "k.log"), 26272); assert.equal(csv, "TR-4471,4990\nTR-4472,6567\nTR-4473,14715\n"); } finally { fs.readFileSync = asil.oku; fs.writeFileSync = asil.yaz; fs.appendFileSync = asil.ekle; Date.now = asil.saat; } });
İki Düzenin Ölçülmesi
Ölçüm iki tabloya ayrılır. Birincisi kaynak dosyalarda dış dünyaya dokunan satırları sayar: dosya sistemi çağrısı ya da saat okuyan satırlar. İkincisi deneme dosyalarını sayar; sahte ortam kuran denemeler gövdelerindeki işaretten bulunur.
# Dis dunyaya dokunan satir: dosya sistemi cagrisi ya da saat okuyan satir. printf '%-22s %5s %10s\n' dosya satir "dis-dunya" for f in icice.mjs kabuk.mjs cekirdek.mjs; do printf '%-22s %5d %10d\n' "$f" "$(awk 'END{print NR}' "$f")" \ "$(grep -cE 'fs\.[a-zA-Z]|Date\.now' "$f")" done echo # Sahteli deneme: govdesinde `// SAHTE ORTAM` isareti tasiyan deneme. printf '%-22s %6s %7s %5s %6s\n' "deneme dosyasi" deneme sahteli satir gecen for f in icice.test.mjs kabuk-duzen.test.mjs; do gecen=$(node --test --test-reporter=tap "$f" 2>&1 | grep '^# pass' | tr -dc '0-9') printf '%-22s %6d %7d %5d %6s\n' "$f" "$(grep -c '^test(' "$f")" \ "$(grep -c 'SAHTE ORTAM' "$f")" "$(awk 'END{print NR}' "$f")" "$gecen" done
dosya satir dis-dunya icice.mjs 24 5 kabuk.mjs 13 4 cekirdek.mjs 19 0 deneme dosyasi deneme sahteli satir gecen icice.test.mjs 3 3 51 3 kabuk-duzen.test.mjs 4 1 48 4
Birinci tablodaki asıl sayı sondaki sıfırdır. Ücret kurallarını taşıyan on dokuz satırlık dosyanın dış dünyaya dokunan hiçbir satırı yoktur. Serpiştirilmiş düzende aynı kurallar, dışarıyla konuşan beş satırla aynı gövdededir; kuralı çalıştırmadan o beş satırdan kaçınmanın yolu yoktur.
İkinci tablo bunun testteki karşılığıdır. Ayrılmış düzen bir deneme fazla içerir, çünkü kabuğun kablolaması ayrıca sınanmaktadır; buna rağmen sahte ortam kuran deneme sayısı üçten bire inmiştir. Fark oranlarda daha nettir: serpiştirilmiş düzende denemelerin yüzde yüzü, ayrılmış düzende yüzde yirmi beşi sahte kurar.
Asıl kazanç ölçeklenmededir. Yeni bir ücret kuralı — hacim indirimi, kurumsal anlaşma, ikinci bir bölge kademesi — ayrılmış düzende sahte ortam kurmayan bir deneme daha ekler; sahteli deneme sayısı birde kalır. Serpiştirilmiş düzende her yeni kural denemesi dört sahteyi yeniden kurar ve bozar. Sahte bağımlılık, sınanan kurala değil, kurala ulaşmak için geçilmesi gereken yola ödenen bir bedeldir.
Sınırın Denetlenmesi
Bu düzenin uygulamada bozulma biçimi tektir: acele bir düzeltmede çekirdeğe bir kayıt satırı ya da bir saat sorgusu eklenir. İyi yanı, ihlalin ölçülebilir olmasıdır. Ölçüm betiğindeki ilk tablo bir kurala çevrilebilir — çekirdek dosyalarının “dış-dünya” sütunu sıfır kalmalıdır — ve bu koşul her değişiklikte yeniden sayılabilir.
Sınırın nereden geçtiğini belirleyen ölçüt, önceki derslerdekiyle aynıdır: gözlenebilirlik. Çekirdek kendi içinde birikeç değiştirir, dizi doldurur, yerel değişken atar; bunların hiçbiri dışarıdan görülmez. Dışarıdan görülen üç şey vardır ve üçü de kabuktadır: dosya sistemi, zaman ve kayıt akışı. Rastgelelik ile ağ çağrısı da aynı kümeye girer.
Kabuğun kendisi saf değildir ve saflaştırılamaz. Buradaki hedef onu küçültmektir: on üç satır, dördü dış dünyaya dokunan, hiçbir iş kuralı içermeyen bir dosya. Bir kusur belirdiğinde soru ikiye ayrılabilir hâle gelir — yanlış olan hesap mı, yoksa kablolama mı. Serpiştirilmiş düzende bu soru ayrılamaz.
Özet
- Girdi/çıktı, zaman ve kayıt akışı ortadan kaldırılamaz; kaldırılabilecek olan, bunların ücret kurallarıyla aynı gövdede durmasıdır.
- Saf çekirdek kararı verir ve ne yapılacağının tanımını döndürür; kirli kabuk o tanımı gerçekleştirir. Karar veren her satır çekirdekte, karar vermeyen her satır kabuktadır.
- Çekirdek dosyasının dış dünyaya dokunan satır sayısı sıfırdır; serpiştirilmiş düzende aynı kurallar dışarıyla konuşan beş satırla aynı gövdededir.
- Ayrılmış düzende deneme sayısı üçten dörde çıkarken sahte ortam kuran deneme sayısı üçten bire iner; yeni her ücret kuralı sahteli deneme sayısını artırmaz.
- Sınır ihlali ölçülebilir bir koşula çevrilir: çekirdek dosyalarında dosya sistemi çağrısı ya da saat okuyan satır sayısı sıfır kalmalıdır.
- Kabuk saflaştırılamaz, küçültülür: on üç satır, hiç iş kuralı yok. Böylece bir kusurda hesap ile kablolama birbirinden ayrı sorulabilir.
Sonraki Adım
Bu konu boyunca kurulan model tek bir birim tanıdı: fonksiyon. Kursun ilk konusu ise başka bir birim kurmuştu — durumu ve onu koruyan davranışı bir arada tutan nesne. İki model aynı alanı, gönderi ücretlendirmeyi, farklı biçimlerde böler. Son ders ikisini yan yana koyar: aynı problem hem nesne ağırlıklı hem fonksiyon ağırlıklı modellenir, sonra iki ayrı değişiklik istenir ve her birinin iki modelde kaç dosyaya dokunduğu sayılır. Çıkacak sonuç bir üstünlük ilanı değil, hangi ölçüte göre hangisinin seçildiğidir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.