Ders 05 / 16
Çevrimsel Karmaşıklığı Azaltma
Dallanma sayısının ölçülmesi ve düşürülmesi: karar noktalarını sayan bir ölçerin yazılması, ücretlendirme fonksiyonunun iç içe geçmiş ve sadeleştirilmiş sürümlerinde çevrimsel karmaşıklığın karşılaştırılması, koruma cümlesi ve arama tablosu dönüşümleriyle dallanmanın veriye taşınması.
İçindekiler
Biçim kararları kodun göze nasıl göründüğünü belirliyor, yapısını değil. Düzgün girintilenmiş bir fonksiyon da okunması güç olabilir: iç içe geçmiş koşullar, birden çok çıkış noktası ve birleşik mantıksal ifadeler girintiyle düzelmez. Bu güçlüğün ölçüsü, denetim akışının kaç yere ayrıldığıdır.
Çevrimsel karmaşıklık (cyclomatic complexity) bu sayıdır. Bir fonksiyonun denetim
akış grafiğinde birbirinden bağımsız kaç yol bulunduğunu verir ve tek bir kuralla
hesaplanır: karar noktası sayısı artı bir. Karar noktası, akışı ikiye ayıran her yapıdır —
if, for, while, case, catch, &&, ||, ?? ve üçlü koşul. Sayı iki şeye
karşılık gelir: fonksiyonu okuyan kişinin aklında tutması gereken durum sayısı ve tüm
dalları geçen bir sınama kümesinin alt sınırı.
Kararların Tek Fonksiyonda Toplandığı Sürüm
Ücretlendirme fonksiyonu zamanla üç iş daha üstlendi: girdi doğrulama, müşteri türüne göre indirim ve bölgeye göre katsayı. Hepsi aynı fonksiyonda, iç içe.
mkdir -p karmasik sade
// karmasik/ucret.mjs — dogrulama, indirim ve bolge kararlari ic ice const HACIMSEL_BOLEN = 3000; export function ucretHesapla(gonderi, tarife, musteri) { if (gonderi.agirlikKg > 0) { if (gonderi.enCm > 0 && gonderi.boyCm > 0 && gonderi.yukseklikCm > 0) { const hacimsel = (gonderi.enCm * gonderi.boyCm * gonderi.yukseklikCm) / HACIMSEL_BOLEN; const agirlik = gonderi.agirlikKg > hacimsel ? gonderi.agirlikKg : hacimsel; let ucret = 0; for (const kademe of tarife.agirlikKademeleri) { if (ucret === 0 && agirlik <= kademe.ustSinirKg) { ucret = kademe.kiloBasiUcret * agirlik; } } if (ucret === 0) { throw new RangeError("kademe disi agirlik"); } if (musteri.tur === "sozlesmeli") { ucret = ucret * 0.85; } else if (musteri.tur === "kurumsal") { ucret = ucret * 0.92; } if (gonderi.bolgeKodu === 3 || gonderi.bolgeKodu === 4) { ucret = ucret * 1.8; } else if (gonderi.bolgeKodu === 2) { ucret = ucret * 1.35; } return ucret < tarife.asgariUcret ? tarife.asgariUcret : ucret; } else { throw new RangeError("olculer pozitif olmali"); } } else { throw new RangeError("agirlik pozitif olmali"); } }
Sadeleştirilmiş Sürüm
İkinci sürüm üç dönüşüm uygular. Koruma cümlesi: doğrulama koşulları ters çevrilip başa alınır, geçerli olmayan girdi erkenden atılır ve asıl hesap girinti düzeyinin dibinden çıkar. Arama tablosu: indirim ve bölge kararları koşul zincirinden veri yapısına taşınır. Ayıklama: kalan işler kendi fonksiyonlarına alınır.
// sade/ucret.mjs — ayni davranis, kararlar tabloya ve koruma cumlelerine tasinmis const HACIMSEL_BOLEN = 3000; const INDIRIM_ORANLARI = { sozlesmeli: 0.15, kurumsal: 0.08 }; const BOLGE_KATSAYILARI = { 1: 1, 2: 1.35, 3: 1.8, 4: 1.8 }; function olculeriDogrula(gonderi) { if (gonderi.agirlikKg <= 0) throw new RangeError("agirlik pozitif olmali"); const boyutlar = [gonderi.enCm, gonderi.boyCm, gonderi.yukseklikCm]; if (boyutlar.some((o) => o <= 0)) throw new RangeError("olculer pozitif olmali"); } function ucretlendirilenAgirlikKg(gonderi) { const hacimsel = (gonderi.enCm * gonderi.boyCm * gonderi.yukseklikCm) / HACIMSEL_BOLEN; return Math.max(gonderi.agirlikKg, hacimsel); } function kademeUcreti(agirlikKg, tarife) { const kademe = tarife.agirlikKademeleri.find((k) => agirlikKg <= k.ustSinirKg); if (kademe === undefined) throw new RangeError("kademe disi agirlik"); return kademe.kiloBasiUcret * agirlikKg; } export function ucretHesapla(gonderi, tarife, musteri) { olculeriDogrula(gonderi); const agirlik = ucretlendirilenAgirlikKg(gonderi); const indirim = 1 - (INDIRIM_ORANLARI[musteri.tur] ?? 0); const katsayi = BOLGE_KATSAYILARI[gonderi.bolgeKodu] ?? 1; return Math.max(kademeUcreti(agirlik, tarife) * indirim * katsayi, tarife.asgariUcret); }
Karşılaştırmanın anlamlı olması için iki sürümün aynı davranışı sürdürdüğü gösterilir. Yedi durum sınanır: dört geçerli hesap ve üç hata.
// karsilastir.mjs — iki surumun ayni girdilerde ayni sonucu verdigi dogrulanir import { ucretHesapla as karmasik } from "./karmasik/ucret.mjs"; import { ucretHesapla as sade } from "./sade/ucret.mjs"; const tarife = { agirlikKademeleri: [{ ustSinirKg: 1, kiloBasiUcret: 38 }, { ustSinirKg: 5, kiloBasiUcret: 26 }, { ustSinirKg: 10, kiloBasiUcret: 21 }, { ustSinirKg: 30, kiloBasiUcret: 17 }], asgariUcret: 52, }; const kutu = { enCm: 30, boyCm: 24, yukseklikCm: 18 }; const DURUMLAR = [ ["sozlesmeli, bolge 2", { ...kutu, agirlikKg: 2.4, bolgeKodu: 2 }, "sozlesmeli"], ["kurumsal, bolge 3", { ...kutu, agirlikKg: 6, bolgeKodu: 3 }, "kurumsal"], ["bireysel, bolge 1", { enCm: 10, boyCm: 10, yukseklikCm: 10, agirlikKg: 0.4, bolgeKodu: 1 }, "bireysel"], ["kurumsal, bolge 4", { ...kutu, agirlikKg: 12, bolgeKodu: 4 }, "kurumsal"], ["agirlik sifir", { ...kutu, agirlikKg: 0, bolgeKodu: 1 }, "bireysel"], ["en sifir", { ...kutu, enCm: 0, agirlikKg: 2, bolgeKodu: 1 }, "bireysel"], ["kademe disi", { ...kutu, agirlikKg: 40, bolgeKodu: 1 }, "bireysel"], ]; const sonuc = (fn, gonderi, tur) => { try { return fn(gonderi, tarife, { tur }).toFixed(2); } catch (hata) { return `hata: ${hata.message}`; } }; for (const [ad, gonderi, tur] of DURUMLAR) { const a = sonuc(karmasik, gonderi, tur); const b = sonuc(sade, gonderi, tur); console.log(`${ad.padEnd(20)} ${a === b ? "ayni " : "FARKLI"} ${a}`); }
sozlesmeli, bolge 2 ayni 128.89 kurumsal, bolge 3 ayni 208.66 bireysel, bolge 1 ayni 52.00 kurumsal, bolge 4 ayni 337.82 agirlik sifir ayni hata: agirlik pozitif olmali en sifir ayni hata: olculer pozitif olmali kademe disi ayni hata: kademe disi agirlik
Ölçer
Ölçer kaynağı fonksiyonlara ayırır, her gövdede karar noktası örüntülerini sayar ve karmaşıklığı karar sayısının bir fazlası olarak verir. Modül satırı, fonksiyon başına hesaplanan değerlerin toplamıdır.
// karmasiklik-olc.mjs — her fonksiyonun karar noktalarini ve cevrimsel karmasikligini sayar import { readFileSync } from "node:fs"; // Denetim akisini dallandiran her yapi bir karar noktasidir. const KARAR = [ ["if", /\bif\s*\(/g], ["for", /\bfor\s*\(/g], ["while", /\bwhile\s*\(/g], ["case", /\bcase\s+/g], ["catch", /\bcatch\s*\(/g], ["&&", /&&/g], ["||", /\|\|/g], ["??", /\?\?/g], ["?:", /\?(?!\?)[^:\n]*:/g], ]; function fonksiyonlar(kaynak) { const cikti = []; let acik = null, derinlik = 0; for (const satir of kaynak.split("\n")) { if (acik === null) { const m = satir.match(/^(?:export\s+)?function\s+(\w+)/); if (m === null) continue; [acik, derinlik] = [{ ad: m[1], govde: [] }, 0]; } acik.govde.push(satir); derinlik += (satir.match(/{/g) ?? []).length - (satir.match(/}/g) ?? []).length; if (derinlik === 0) { cikti.push({ ad: acik.ad, govde: acik.govde.join("\n") }); acik = null; } } return cikti; } for (const dosya of process.argv.slice(2)) { console.log(dosya); let toplam = 0; for (const { ad, govde } of fonksiyonlar(readFileSync(dosya, "utf8"))) { const sayilar = KARAR.map(([etiket, oruntu]) => [etiket, (govde.match(oruntu) ?? []).length]); const karar = sayilar.reduce((t, [, n]) => t + n, 0); const dokum = sayilar.filter(([, n]) => n > 0).map(([e, n]) => `${e}:${n}`).join(" "); toplam += karar + 1; console.log(` ${ad.padEnd(24)} karar=${String(karar).padEnd(3)}M=${String(karar + 1).padEnd(3)}${dokum || "-"}`); } console.log(` ${"= modul".padEnd(24)} toplam M=${toplam}`); }
node karmasiklik-olc.mjs karmasik/ucret.mjs sade/ucret.mjs
karmasik/ucret.mjs ucretHesapla karar=15 M=16 if:8 for:1 &&:3 ||:1 ?::2 = modul toplam M=16 sade/ucret.mjs olculeriDogrula karar=2 M=3 if:2 ucretlendirilenAgirlikKg karar=0 M=1 - kademeUcreti karar=1 M=2 if:1 ucretHesapla karar=2 M=3 ??:2 = modul toplam M=9
Tek fonksiyonda toplanan on beş karar noktası, on altı bağımsız yol demektir. Sadeleşmiş sürümde en yüksek değer üçtür ve modül toplamı dokuza inmiştir. İki sayı ayrı şeyler söylüyor: modül toplamı işin bütününde kaç dal bulunduğunu, fonksiyon başına düşen değer ise bir kişinin tek seferde ne kadarını aklında tutmak zorunda olduğunu.
Dallar Aynı Fonksiyonda Çarpılır
Toplam neden dokuza inebiliyor? Bir kısmı dallanmanın veriye taşınmasından geliyor: indirim zinciri iki koşuldan iki tablo satırına, bölge zinciri iki koşuldan dört tablo satırına dönüştü. Kalan kazanç ise bölmenin kendisinden.
Aynı fonksiyonda duran dallar çarpılır. Karmaşık sürümde doğrulama geçtikten sonra indirim üç sonuç veriyor (sözleşmeli, kurumsal, ikisi de değil), bölge üç sonuç veriyor (uzak, komşu, aynı şehir), asgari ücret karşılaştırması iki sonuç veriyor. Bunlar birbirinden bağımsız olduğu için fonksiyonun sonunu belirleyen birleşim sayısı 3 × 3 × 2 = 18’dir; hepsi tek bir gövdenin içinde bulunur.
Aynı üç karar ayrı fonksiyonlara dağıtıldığında birleşim sayısı toplanır: her fonksiyon
kendi dallarıyla ayrı ayrı sınanır, çünkü aralarındaki bağ yalnız bir değerin geçirilmesidir.
Tablo dönüşümü bu adımı daha da ileri götürür: INDIRIM_ORANLARI ve BOLGE_KATSAYILARI
birer veri satırı olduğu için hiç dal üretmezler. Yeni bir müşteri türü eklemek karmaşık
sürümde bir else if dalı, sadeleşmiş sürümde bir tablo satırı ekler; birincisi
karmaşıklığı bir arttırır, ikincisi arttırmaz.
Sayının Söylemediği
Çevrimsel karmaşıklık ölçüsünün sınırları var ve bunları bilmeden kullanmak yanlış kararlar ürettirir.
Sayı ile anlaşılırlık aynı şey değildir. Uzun bir switch bloğu yüksek bir değer
üretir ama tek bakışta okunabilir; iki iç içe döngü içindeki tek koşul daha düşük bir
değer üretir ve daha zor izlenir. Ölçü, akışın kaça ayrıldığını sayar; ayrılan yolların
birbirine ne kadar benzediğini saymaz.
Sayı bölerek düşürülür ama bölme bedavaya gelmez. On beş karar noktasını on beş fonksiyona dağıtan bir kod, ölçünün gözünde kusursuzdur ve okunması güçtür: okuyan kişi bu kez çağrı zincirini izlemek zorunda kalır. Önceki dersin ölçütü burada devreye girer; bölme yerleri soyutlama düzeyine göre seçilir, sayıyı düşürmek için seçilmez.
Yüksek değer bir kusur değil, bir işarettir. Ölçüm bir fonksiyonu işaretlediğinde
sorulacak soru “bu nasıl bölünür” değil, “bu kararların hepsi gerçekten burada mı
verilmeli” sorusudur. Karmaşık sürümdeki sekiz if bunun örneğiydi: üçü doğrulamaya,
ikisi indirime, ikisi bölgeye aitti ve hiçbiri ücret hesabının kendisi değildi.
Özet
- Çevrimsel karmaşıklık, karar noktası sayısının bir fazlasıdır ve fonksiyonun denetim
akışındaki bağımsız yol sayısını verir; karar noktaları
if,for,while,case,catch,&&,||,??ve üçlü koşuldur. - İç içe sürümde tek fonksiyon on beş karar noktası ve M = 16 taşıyordu; koruma cümlesi, arama tablosu ve ayıklama dönüşümlerinden sonra en yüksek fonksiyon değeri 3, modül toplamı 9 oldu.
- İki sürüm yedi durumda da aynı sonucu üretiyor; ölçüm davranış değişmeden yapıldı.
- Aynı gövdedeki bağımsız dallar çarpılır (3 × 3 × 2 = 18 birleşim), ayrı fonksiyonlara dağıtıldığında toplanır; tabloya taşınan karar hiç dal üretmez.
- Ölçü akışın kaça ayrıldığını sayar, yolların anlaşılırlığını saymaz; yüksek değer bölünmesi gereken bir yeri değil, sorgulanması gereken bir karar yığılmasını gösterir.
Sonraki Adım
Bölge katsayıları koşul zincirinden bir tabloya taşındı ve karmaşıklık düştü. Bu dönüşüm her zaman doğru mudur? Tablo, anahtara karşılık bir değer döndürür; ya karşılık bir değer değil de bir davranışsa, ya her tarife türü ücreti başka bir yöntemle hesaplıyorsa? O noktada üçüncü bir seçenek belirir: çok biçimlilik. Sonraki ders aynı davranışın koşul zinciri, arama tablosu ve çok biçimlilikle üç sürümünü yazar ve seçimi ölçülebilir bir soruya bağlar: yeni bir tarife türü eklendiğinde kaç dosyaya, kaç satıra dokunulur.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.