Ders 01 / 14
Yapısal Programlama
Paradigmanın bir sözdizimi değil bir bölme kararı olduğunun gösterilmesi: aynı ücretlendirme akışının bayraklı ve bölünmüş iki sürümünde bağımsız yol sayısının, çıkış noktalarının ve dal kapsamı için gereken durum sayısının ölçülmesi.
İçindekiler
Önceki kurs, davranışı koruyarak yapıyı değiştirmenin güvencesini kurdu: testler yeşilken ayıklama, taşıma ve yeniden adlandırma yapılabiliyordu. O kurs boyunca verilen her karar ortak bir varsayımı paylaşıyordu. Kodun nasıl yazıldığı sorgulandı; hangi modelle düşünüldüğü sorgulanmadı. Bir fonksiyonun uzunluğu, bir adın açıklığı, bir yinelemenin gereksizliği tartışıldı — ama programın fonksiyonlardan oluştuğu hiç tartışılmadı.
Bu kurs o varsayımı açar. Bir paradigma (paradigm) bir sözdizimi değildir; bir programın hangi birimlere bölüneceğini belirleyen bir karardır. Birim yordam olabilir, durum taşıyan bir nesne olabilir, girdiden çıktıya bir eşleme olabilir. Seçim, kodun görünüşünü değil, değişikliğin nereye kadar yayıldığını belirler. Yapısal programlama bu kararların ilkidir ve diğerlerinin üzerine kurulduğu zemindir.
Bölme Kararı Nerede Verilir
Programlama Temelleri kursunda yapısal programlamanın üç yapısı tanıtıldı: sıra, seçim ve yineleme. Bu üçünün her hesaplanabilir programı yazmaya yettiği biçimsel bir sonuçtur. Buradaki soru yeterlilik değil, disiplin: aynı üç yapıyla yazılmış iki program, birimlere bölünüş biçimleri yüzünden çok farklı bakım maliyeti taşır.
Ayrımı görünür kılan ölçüt, bir birime kaç ayrı yoldan girilebildiğidir. Tek giriş–tek çıkış (single entry, single exit) kısıtı, bir bloğa yalnız başından girilmesini ve yalnız sonundan çıkılmasını söyler. Kısıtın değeri gizemli değil, aritmetiktir: bir kod noktasındaki değerler hakkında akıl yürütmek için oraya varan tüm yolların incelenmesi gerekir, ve yol sayısı her karar noktasında katlanır.
Erken çıkış — koruma cümlesi, döngüden çıkma — bu kısıtın denetimli gevşetilmesidir; hedefi sabittir. Denetimsiz olan, akışın bayrak değişkenleriyle yönlendirilmesidir: bayrak, bir kararın sonucunu kodun ilerideki bir noktasına taşır ve iki nokta arasındaki her satır o kararın etkisi altına girer.
Aynı Akışın İki Sürümü
Bu kurs boyunca aynı kod tabanı geliştirilecek: bir gönderi ücretlendirme ve yönlendirme
kitaplığı. Bir gonderi, ağırlık, hacim, teslim ili, beyan edilen değer ve sözleşmeli
müşteri bilgisi taşır. Bir tarife, ağırlık kademelerini, bölge katsayılarını ve asgari
ücreti tutar. Ücret, doğrulama ile başlayan ve asgari ücret alt sınırıyla biten bir
zincirin sonucudur.
İlk sürüm bu zinciri tek gövdede, bayraklarla yazar.
// ucret-bayrakli.mjs — ucretlendirme akisinin tek govdede, bayraklarla yazilmis surumu export const BOLGELER = { 34: 1, 6: 1, 35: 2, 1: 2, 21: 3, 65: 3 }; export function ucretHesapla(gonderi, tarife) { let hata = null; let gecersiz = false; if (gonderi.agirlikGram <= 0) { hata = "agirlik pozitif olmali"; gecersiz = true; } if (!gecersiz && gonderi.hacimCm3 <= 0) { hata = "hacim pozitif olmali"; gecersiz = true; } if (!gecersiz && BOLGELER[gonderi.teslimIli] === undefined) { hata = "teslim ili bilinmiyor"; gecersiz = true; } if (gecersiz) return { durum: "reddedildi", sebep: hata }; const hacimAgirligi = gonderi.hacimCm3 / 3; let ucretliAgirlik = gonderi.agirlikGram; if (hacimAgirligi > ucretliAgirlik) ucretliAgirlik = hacimAgirligi; let taban = 0; let bulundu = false; for (const [ustSinir, ucret] of tarife.kademeler) { if (!bulundu && ucretliAgirlik <= ustSinir) { taban = ucret; bulundu = true; } } if (!bulundu) { const son = tarife.kademeler[tarife.kademeler.length - 1]; const fazlaKilo = Math.ceil((ucretliAgirlik - son[0]) / 1000); taban = son[1] + fazlaKilo * tarife.kiloBasiEk; } const bolge = BOLGELER[gonderi.teslimIli]; let ucret = Math.round(taban * tarife.bolgeKatsayisi[bolge]); if (gonderi.deger > 100000) ucret += Math.round(gonderi.deger * 0.01); if (gonderi.sozlesmeli === true) ucret = Math.round(ucret * 0.9); if (ucret < tarife.asgariUcret) ucret = tarife.asgariUcret; return { durum: "hesaplandi", bolge, ucretliAgirlik, ucret }; }
İki bayrak var: gecersiz ve bulundu. İkisi de bir kararın sonucunu ileriye taşıyor.
gecersiz bayrağı üç ayrı if tarafından yazılıyor ve üçü de aynı değişkeni okuyor;
bulundu bayrağı döngünün içinde yazılıp döngüden sonra okunuyor.
İkinci sürüm aynı zinciri adımlarına böler. Her adım tek girişlidir ve kendi kararını kendi içinde bitirir.
// ucret-yapisal.mjs — ayni akis, her birimi tek girisli ve tek cikisli yordamlara bolunmus export const BOLGELER = { 34: 1, 6: 1, 35: 2, 1: 2, 21: 3, 65: 3 }; export function dogrula(gonderi) { let sebep = null; if (gonderi.agirlikGram <= 0) sebep = "agirlik pozitif olmali"; else if (gonderi.hacimCm3 <= 0) sebep = "hacim pozitif olmali"; else if (BOLGELER[gonderi.teslimIli] === undefined) sebep = "teslim ili bilinmiyor"; return sebep; } export function ucretliAgirlik(gonderi) { return Math.max(gonderi.agirlikGram, gonderi.hacimCm3 / 3); } export function kademeUcreti(agirlik, tarife) { const kademe = tarife.kademeler.find(([ustSinir]) => agirlik <= ustSinir); if (kademe !== undefined) return kademe[1]; const [sonSinir, sonUcret] = tarife.kademeler[tarife.kademeler.length - 1]; return sonUcret + Math.ceil((agirlik - sonSinir) / 1000) * tarife.kiloBasiEk; } export function bolgeyeUyarla(taban, bolge, tarife) { return Math.round(taban * tarife.bolgeKatsayisi[bolge]); } export function eklentileriUygula(ucret, gonderi, tarife) { let sonuc = ucret; if (gonderi.deger > 100000) sonuc += Math.round(gonderi.deger * 0.01); if (gonderi.sozlesmeli === true) sonuc = Math.round(sonuc * 0.9); return Math.max(sonuc, tarife.asgariUcret); } export function ucretHesapla(gonderi, tarife) { const sebep = dogrula(gonderi); if (sebep !== null) return { durum: "reddedildi", sebep }; const bolge = BOLGELER[gonderi.teslimIli]; const agirlik = ucretliAgirlik(gonderi); const taban = kademeUcreti(agirlik, tarife); const ucret = eklentileriUygula(bolgeyeUyarla(taban, bolge, tarife), gonderi, tarife); return { durum: "hesaplandi", bolge, ucretliAgirlik: agirlik, ucret }; }
Her iki bayrak da kayboldu. gecersiz, dogrula yordamının dönüş değerine dönüştü;
bulundu, bir arama işleminin undefined sonucuna. Bayrak, adı olmayan bir dönüş
değeridir; ada kavuştuğunda değişken olmaktan çıkar.
Bölme Davranışı Değiştirmedi
İddianın ilk yarısı, iki sürümün aynı işi yaptığıdır. Bu ölçülür.
// esdegerlik.mjs — iki surumun ayni girdi izgarasinda ayni sonucu verdigini dogrular import assert from "node:assert/strict"; import { ucretHesapla as bayrakli } from "./ucret-bayrakli.mjs"; import { ucretHesapla as yapisal } from "./ucret-yapisal.mjs"; export const TARIFE = { kademeler: [[1000, 4500], [5000, 7000], [10000, 11000]], kiloBasiEk: 1800, bolgeKatsayisi: { 1: 1, 2: 1.25, 3: 1.6 }, asgariUcret: 5000, }; const izgara = []; for (const agirlikGram of [500, 4000, 12000]) for (const hacimCm3 of [900, 30000]) for (const teslimIli of [34, 35, 21, 99]) for (const deger of [0, 500000]) for (const sozlesmeli of [false, true]) izgara.push({ agirlikGram, hacimCm3, teslimIli, deger, sozlesmeli }); let ayrilan = 0; for (const gonderi of izgara) { try { assert.deepEqual(bayrakli(gonderi, TARIFE), yapisal(gonderi, TARIFE)); } catch { ayrilan += 1; } } const sonuclar = new Set(izgara.map((g) => JSON.stringify(yapisal(g, TARIFE)))); console.log(`girdi sayisi = ${izgara.length}`); console.log(`ayrilan sonuc = ${ayrilan}`); console.log(`farkli sonuc sinifi = ${sonuclar.size}`);
node esdegerlik.mjs
girdi sayisi = 96 ayrilan sonuc = 0 farkli sonuc sinifi = 48
Doksan altı girdinin hiçbirinde ayrılma yok. Karşılaştırma bundan sonra davranış üzerinden değil, yapı üzerinden yapılabilir.
Bağımsız Yol Sayısı
Bir yordamın bağımsız yol (independent path) sayısı, karar noktası sayısının bir fazlasıdır; bu ölçüte çevrimsel karmaşıklık (cyclomatic complexity) denir. Sayı, o yordamın tüm dallarını geçmek için gereken en az yol adedini verir.
// olcum.mjs — her yordamin karar noktalarini sayar ve son karara ulasma bedelini bildirir import { readFileSync } from "node:fs"; const KARAR = /\b(if|for|while|case|catch)\b|&&|\|\||\?\?/g; function yordamlar(kaynak) { const bulunan = []; const baslik = /(?:export\s+)?function\s+(\w+)\s*\(/g; let m; while ((m = baslik.exec(kaynak)) !== null) { const bas = kaynak.indexOf("{", m.index); let derinlik = 0; let son = bas; for (; son < kaynak.length; son += 1) { if (kaynak[son] === "{") derinlik += 1; else if (kaynak[son] === "}") { derinlik -= 1; if (derinlik === 0) break; } } const govde = kaynak.slice(bas, son + 1); bulunan.push({ ad: m[1], karar: (govde.match(KARAR) ?? []).length, cikis: (govde.match(/\breturn\b/g) ?? []).length, }); } return bulunan; } for (const dosya of ["ucret-bayrakli.mjs", "ucret-yapisal.mjs"]) { const liste = yordamlar(readFileSync(dosya, "utf8")); console.log(dosya); for (const y of liste) { console.log(` ${y.ad.padEnd(18)} bagimsiz yol=${y.karar + 1} cikis=${y.cikis}`); } console.log(` yordam=${liste.length}` + ` en buyuk birim=${Math.max(...liste.map((y) => y.karar + 1))}` + ` son karar oncesi saglanacak karar=${Math.max(...liste.map((y) => y.karar - 1))}`); }
node olcum.mjs
ucret-bayrakli.mjs ucretHesapla bagimsiz yol=15 cikis=2 yordam=1 en buyuk birim=15 son karar oncesi saglanacak karar=13 ucret-yapisal.mjs dogrula bagimsiz yol=4 cikis=1 ucretliAgirlik bagimsiz yol=1 cikis=1 kademeUcreti bagimsiz yol=2 cikis=2 bolgeyeUyarla bagimsiz yol=1 cikis=1 eklentileriUygula bagimsiz yol=3 cikis=1 ucretHesapla bagimsiz yol=2 cikis=2 yordam=6 en buyuk birim=4 son karar oncesi saglanacak karar=2
Toplam karar sayısı ikisinde de yakın: 15’e karşı 13. Bölme, kararları yok etmedi; onları dağıttı. Değişen, tek bir birimde yoğunlaşan miktar: 15’ten 4’e indi.
Son satırdaki ikinci sayı daha belirleyici. Bayraklı sürümde en son karara — asgari ücret
alt sınırına — ulaşan bir çalıştırma, kendisinden önceki 13 kararın sonucunu taşıyarak
oraya varır. Bölünmüş sürümde aynı karar eklentileriUygula yordamının içindedir ve
önünde yalnız 2 karar vardır. Bu fark, sonraki bölümün konusudur.
Çıkış sayısı sütunu, tek çıkış kısıtının nerede gevşetildiğini de gösterir. kademeUcreti
ve ucretHesapla ikişer çıkışa sahiptir; ikisi de koruma cümlesidir ve hedefleri
yordamın sonudur. Bayraklı sürümde de iki çıkış vardır, ama aralarındaki 30 satır iki
bayrağın etkisi altındadır.
Dal Kapsamı İçin Gereken Durum Sayısı
Ölçütün bakıma yansıyan karşılığı, dal kapsamı (branch coverage) için kaç durum
kurulması gerektiğidir. Bayraklı sürümün tek girişi vardır; her dala ancak eksiksiz bir
gonderi nesnesi kurularak ulaşılır.
// bayrakli.test.mjs — tek govdeli surumun her dalina uctan uca gonderi kurarak ulasilir import { test } from "node:test"; import assert from "node:assert/strict"; import { ucretHesapla } from "./ucret-bayrakli.mjs"; const TARIFE = { kademeler: [[1000, 4500], [5000, 7000], [10000, 11000]], kiloBasiEk: 1800, bolgeKatsayisi: { 1: 1, 2: 1.25, 3: 1.6 }, asgariUcret: 5000, }; const G = (o) => ({ agirlikGram: 4000, hacimCm3: 900, teslimIli: 34, deger: 0, sozlesmeli: false, ...o }); test("agirlik sifir", () => assert.equal(ucretHesapla(G({ agirlikGram: 0 }), TARIFE).durum, "reddedildi")); test("hacim sifir", () => assert.equal(ucretHesapla(G({ hacimCm3: 0 }), TARIFE).durum, "reddedildi")); test("bilinmeyen il", () => assert.equal(ucretHesapla(G({ teslimIli: 99 }), TARIFE).durum, "reddedildi")); test("hacim agirligi baskin", () => assert.equal(ucretHesapla(G({ hacimCm3: 30000 }), TARIFE).ucretliAgirlik, 10000)); test("kademe disi agirlik", () => assert.equal(ucretHesapla(G({ agirlikGram: 12000 }), TARIFE).ucret, 14600)); test("degerli gonderi", () => assert.equal(ucretHesapla(G({ deger: 500000 }), TARIFE).ucret, 12000)); test("sozlesmeli musteri", () => assert.equal(ucretHesapla(G({ sozlesmeli: true }), TARIFE).ucret, 6300)); test("asgari ucret devreye girer", () => assert.equal(ucretHesapla(G({ agirlikGram: 100 }), TARIFE).ucret, 5000));
Bölünmüş sürümde her birim doğrudan çağrılabilir. Girdi, çoğu durumda bir sayıdır.
// yapisal.test.mjs — bolunmus surumun dallari, birimler dogrudan cagrilarak kapsanir import { test } from "node:test"; import assert from "node:assert/strict"; import { dogrula, ucretliAgirlik, kademeUcreti, bolgeyeUyarla, eklentileriUygula, ucretHesapla } from "./ucret-yapisal.mjs"; const TARIFE = { kademeler: [[1000, 4500], [5000, 7000], [10000, 11000]], kiloBasiEk: 1800, bolgeKatsayisi: { 1: 1, 2: 1.25, 3: 1.6 }, asgariUcret: 5000, }; test("dogrula dort dali", () => { assert.equal(dogrula({ agirlikGram: 0 }), "agirlik pozitif olmali"); assert.equal(dogrula({ agirlikGram: 500, hacimCm3: 0 }), "hacim pozitif olmali"); assert.equal(dogrula({ agirlikGram: 500, hacimCm3: 900, teslimIli: 99 }), "teslim ili bilinmiyor"); assert.equal(dogrula({ agirlikGram: 500, hacimCm3: 900, teslimIli: 34 }), null); }); test("ucretli agirlik iki dali", () => { assert.equal(ucretliAgirlik({ agirlikGram: 4000, hacimCm3: 900 }), 4000); assert.equal(ucretliAgirlik({ agirlikGram: 4000, hacimCm3: 30000 }), 10000); }); test("kademe ucreti iki dali", () => { assert.equal(kademeUcreti(4000, TARIFE), 7000); assert.equal(kademeUcreti(12000, TARIFE), 14600); }); test("bolgeye uyarlama", () => assert.equal(bolgeyeUyarla(7000, 3, TARIFE), 11200)); test("eklentiler uc dali", () => { assert.equal(eklentileriUygula(7000, { deger: 500000, sozlesmeli: false }, TARIFE), 12000); assert.equal(eklentileriUygula(7000, { deger: 0, sozlesmeli: true }, TARIFE), 6300); assert.equal(eklentileriUygula(4500, { deger: 0, sozlesmeli: false }, TARIFE), 5000); }); test("akis iki dali", () => { assert.equal(ucretHesapla({ agirlikGram: 0 }, TARIFE).durum, "reddedildi"); assert.equal(ucretHesapla( { agirlikGram: 4000, hacimCm3: 900, teslimIli: 34, deger: 0, sozlesmeli: false }, TARIFE).ucret, 7000); });
Aşağıdaki komut her iki kümeyi de kapsam ölçümüyle çalıştırır ve yalnız sayı satırlarını gösterir; süre alanları her koşumda değiştiği için ayıklanmıştır.
for f in bayrakli.test.mjs yapisal.test.mjs; do node --test --experimental-test-coverage $f 2>&1 | grep -E '^ℹ (tests|pass|fail) |^ℹ (file|ucret-|all files)' done
ℹ tests 8 ℹ pass 8 ℹ fail 0 ℹ file | line % | branch % | funcs % | uncovered lines ℹ ucret-bayrakli.mjs | 100.00 | 100.00 | 100.00 | ℹ all files | 100.00 | 100.00 | 100.00 | ℹ tests 6 ℹ pass 6 ℹ fail 0 ℹ file | line % | branch % | funcs % | uncovered lines ℹ ucret-yapisal.mjs | 100.00 | 100.00 | 100.00 | ℹ all files | 100.00 | 100.00 | 100.00 |
İki sürüm de tam dal kapsamına ulaşıyor. Fark, kapsamı elde etmenin bedelinde. Bayraklı
sürüm için sekiz durum kuruldu ve sekizinin de beş alanlı eksiksiz bir gonderi
nesnesine ihtiyacı var; yardımcı G işlevi tam da bu tekrarı gizlemek için yazıldı.
Bölünmüş sürümde on dört doğrulama var ve bunlardan yalnız biri eksiksiz bir gönderi
kuruyor; geri kalanı iki alanlı nesnelerle ya da doğrudan sayılarla çalışıyor.
Bedelin asıl görüneceği yer, kuralın değişmesidir. Asgari ücret kuralı değiştiğinde
bayraklı sürümün ilgili durumları, o karara ulaşmak için gereken 13 önceki kararı hâlâ
sağlamak zorundadır; bölünmüş sürümde eklentileriUygula için yazılmış üç satır yeterlidir.
Yapısal Bölmenin Bıraktığı Boşluk
Bölünmüş sürüm bir şeyi çözmedi. dogrula yordamı gönderinin geçerli olduğunu söyler, ama
bu bilgi hiçbir yerde saklanmaz. ucretliAgirlik, kademeUcreti ve eklentileriUygula
yordamlarının her biri, kendisine verilen gönderinin doğrulanmış olduğunu varsayar.
Varsayım koda yazılmamıştır; çağrı sırasına dayanır.
Aynı gönderi nesnesi doğrulamadan geçirilmeden eklentileriUygula yordamına verilirse,
hiçbir şey engellemez. Negatif ağırlıklı bir gönderi de bir ücret üretir. Kural — “ücret
hesaplanan her gönderi doğrulanmıştır” — bir değişmez (invariant) olmasına rağmen
kodun hiçbir noktasında korunmuyor; yalnız ucretHesapla yordamının içindeki sıralamada
yaşıyor.
Veri ile onu işleyen yordamların ayrı durması, yapısal bölmenin tanımı gereğidir. Bu ayrım küçük programlarda sade, değişmez taşıyan verilerde ise korumasızdır. Sonraki dersin konusu tam olarak bu boşluktur.
Özet
- Paradigma bir sözdizimi değil, programın hangi birimlere bölüneceğine dair bir karardır; yapısal yanıt yordamdır.
- Bayrak değişkeni, bir kararın sonucunu ileriye taşır ve aradaki her satırı o kararın etkisi altına sokar; adlandırılmış bir dönüş değeri aynı işi taşımadan yapar.
- Aynı akışın iki sürümü 96 girdinin tamamında aynı sonucu verdi; karşılaştırma davranış üzerinden değil yapı üzerinden yapıldı.
- Bölme toplam karar sayısını düşürmedi (15’e karşı 13); tek bir birimdeki yoğunluğu 15’ten 4’e, son karara ulaşmak için sağlanacak önceki karar sayısını 13’ten 2’ye indirdi.
- İki sürüm de tam dal kapsamına ulaştı; bayraklı sürümde sekiz durumun sekizi de eksiksiz bir gönderi kurmak zorundayken bölünmüş sürümde on dört doğrulamanın yalnız biri kuruyor.
- Yapısal bölme, “ücret hesaplanan her gönderi doğrulanmıştır” değişmezini koruyamaz; bu kural yalnız çağrı sırasında yaşar.
Sonraki Adım
Değişmezin çağrı sırasına emanet edilmesi, onu koruyacak bir sahibi olmadığı anlamına gelir. Sonraki ders, veriyi ve onu koruyan davranışı tek bir birimde toplayan aracı ele alır: kapsülleme. Ölçüm de buna göre değişir — soru artık kaç yol olduğu değil, aynı işlem dizisi altında kaç nesnenin bozuk duruma düştüğüdür.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.