İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat