Ders 18 / 30
Durum
Teslimat yaşam döngüsünün beş bayrakla ve iç içe koşullarla tutulması ile her durumun kendi geçişlerini bilen nesnelere çevrilmesinin karşılaştırılması: otuz durum-olay çiftinde kabul edilen geçersiz geçiş sayısı, çevrimsel karmaşıklık, temsil edilebilir birleşim sayısı, yeni durum eklendiğinde değişen satır sayısı ve kalıbın bedeli olarak geçiş hedefi hatasının gecikmeli görünmesi.
İçindekiler
Şablonda sıra sabitti; hangi adımın çalışacağı baştan belliydi. Teslimatın kendisinde ise hangi işlemin geçerli olduğu o anki duruma bağlıdır. Bir teslimat oluşturulmuş, toplanmış, yolda, dağıtımda, teslim edilmiş ya da iade edilmiş olabilir; toplanmamış bir gönderi dağıtıma çıkamaz, teslim edilmiş bir gönderi iade edilemez. Bugün bu kurallar beş bayrakla ve iç içe koşullarla denetleniyor.
Durum makinesi kavramı Yazılım Kalitesi ve Test müfredatında bir kez kurulmuştu: hata yaşam döngüsü orada durum ve geçiş tablosuyla anlatılmış, geçişler test durumu üretmek için kullanılmıştı. Burada yeni olan makinenin kendisi değil, nesnelerle ifade edilişi. Durum (state) kalıbı her durumu bir nesne yapar ve o nesneye yalnızca kendi izin verdiği geçişleri bildirir. Ölçülecek sayılar kabul edilen geçersiz geçiş sayısı, çevrimsel karmaşıklık ve temsil edilebilir bayrak birleşimi sayısıdır.
Bayraklarla Tutulan Yaşam Döngüsü
Beş bayrak ve beş olay var. Her olay kendi koşulunu kendisi kurar; durumun adı bayrakların sıralı denetiminden türetilir.
mkdir -p bayrak durum
// bayrak/teslimat.mjs — durum bes bayrakta, gecisler ic ice kosullarda export const teslimat = (kod) => { const d = { toplandi: false, yolda: false, dagitimda: false, teslim: false, iade: false }; const durumAdi = () => d.iade ? "iade" : d.teslim ? "teslim" : d.dagitimda ? "dagitimda" : d.yolda ? "yolda" : d.toplandi ? "toplandi" : "olusturuldu"; return { kod, durumAdi, olay(ad) { if (ad === "topla") { if (d.toplandi === false && d.teslim === false) { d.toplandi = true; return true; } return false; } if (ad === "yolaCik") { if (d.toplandi && d.yolda === false) { d.yolda = true; return true; } return false; } if (ad === "dagitimaVer") { if (d.yolda && d.dagitimda === false) { d.dagitimda = true; return true; } return false; } if (ad === "teslimEt") { if (d.yolda && d.teslim === false && d.iade === false) { d.teslim = true; return true; } return false; } if (ad === "iadeEt") { if (d.yolda && d.iade === false) { d.iade = true; return true; } return false; } return false; }, }; };
Durumun Nesneleştirilmesi
İkinci sürümde her durum bir nesnedir ve kendi geçiş kümesini taşır. Geçiş kümesi olay adından sonraki durumun adına giden bir eşlemedir.
// durum/durumlar.mjs — her durum yalnizca kendi gecislerini bilir const durum = (ad, gecisler) => ({ ad, izin: (olay) => Object.hasOwn(gecisler, olay), sonraki: (olay) => gecisler[olay], }); export const DURUMLAR = { olusturuldu: durum("olusturuldu", { topla: "toplandi" }), toplandi: durum("toplandi", { yolaCik: "yolda" }), yolda: durum("yolda", { dagitimaVer: "dagitimda", iadeEt: "iade" }), dagitimda: durum("dagitimda", { teslimEt: "teslim", iadeEt: "iade" }), teslim: durum("teslim", {}), iade: durum("iade", {}), };
// durum/teslimat.mjs — gecis karari durum nesnesine sorulur import { DURUMLAR } from "./durumlar.mjs"; export const teslimat = (kod) => { let su = DURUMLAR.olusturuldu; return { kod, durumAdi: () => su.ad, olay(ad) { if (su.izin(ad) === false) return false; su = DURUMLAR[su.sonraki(ad)]; return true; }, }; };
Teslimat nesnesi artık hangi olayın hangi durumda geçerli olduğunu bilmiyor; kararı elindeki durum nesnesine soruyor. Bu, sorma–söyle ilkesinin durum makinesindeki karşılığıdır: bilgi kararın verildiği yerdedir.
Otuz Çiftin Tek Tek Denenmesi
Ölçüm, altı durumun her birinde beş olayın da denenmesiyle yapılır: otuz çift. Beklenti tablosu ve her duruma götüren yol ayrı bir dosyada durur.
// tablo.mjs — gecerli gecis tablosu ve her duruma goturen yol export const OLAYLAR = ["topla", "yolaCik", "dagitimaVer", "teslimEt", "iadeEt"]; export const YOL = { olusturuldu: [], toplandi: ["topla"], yolda: ["topla", "yolaCik"], dagitimda: ["topla", "yolaCik", "dagitimaVer"], teslim: ["topla", "yolaCik", "dagitimaVer", "teslimEt"], iade: ["topla", "yolaCik", "iadeEt"], }; export const GECERLI = { olusturuldu: { topla: "toplandi" }, toplandi: { yolaCik: "yolda" }, yolda: { dagitimaVer: "dagitimda", iadeEt: "iade" }, dagitimda: { teslimEt: "teslim", iadeEt: "iade" }, teslim: {}, iade: {}, };
// kosum.mjs — otuz durum-olay ciftini iki surumde de dener import { teslimat as bayrakTeslimat } from "./bayrak/teslimat.mjs"; import { teslimat as durumTeslimat } from "./durum/teslimat.mjs"; import { GECERLI, OLAYLAR, YOL } from "./tablo.mjs"; function dene(uret, etiket) { let kabulEdilenGecersiz = 0; let reddedilenGecerli = 0; const satirlar = []; for (const [durumAdi, yol] of Object.entries(YOL)) { const isaret = []; for (const olay of OLAYLAR) { const t = uret("TS-1"); for (const o of yol) t.olay(o); const kabul = t.olay(olay); const gecerli = Object.hasOwn(GECERLI[durumAdi], olay); if (kabul && gecerli === false) kabulEdilenGecersiz += 1; if (kabul === false && gecerli) reddedilenGecerli += 1; isaret.push(kabul === gecerli ? "." : kabul ? "K" : "R"); } satirlar.push(` ${durumAdi.padEnd(12)} ${isaret.join(" ")}`); } console.log(`${etiket} (${OLAYLAR.join(" ")}):`); for (const s of satirlar) console.log(s); console.log(` ${etiket.padEnd(12)} kabul edilen gecersiz=${kabulEdilenGecersiz} reddedilen gecerli=${reddedilenGecerli}`); } dene(bayrakTeslimat, "bayrak"); dene(durumTeslimat, "durum");
bayrak (topla yolaCik dagitimaVer teslimEt iadeEt): olusturuldu . . . . . toplandi . . . . . yolda . . . K . dagitimda . . . . . teslim . . . . K iade . . K . . bayrak kabul edilen gecersiz=3 reddedilen gecerli=0 durum (topla yolaCik dagitimaVer teslimEt iadeEt): olusturuldu . . . . . toplandi . . . . . yolda . . . . . dagitimda . . . . . teslim . . . . . iade . . . . . durum kabul edilen gecersiz=0 reddedilen gecerli=0
Üçe karşı sıfır. Bayrak sürümü hiçbir geçerli geçişi reddetmiyor, yani kabaca bozuk değil;
fazla izin veriyor. Üç K işareti üç somut hatadır: dağıtıma çıkmamış bir gönderi teslim
edilebiliyor, teslim edilmiş bir gönderi iade edilebiliyor, iade edilmiş bir gönderi dağıtıma
verilebiliyor. Üçünün kaynağı aynı: her olay kendi koşulunu kendisi kuruyor ve hiçbiri
“şu anda hangi durumdayım” sorusunu tek bir yerden sormuyor.
Karmaşıklık ve Temsil Uzayı
// karmasiklik.mjs — karar noktasi sayarak cevrimsel karmasiklik verir import { readFileSync, readdirSync } from "node:fs"; const KARAR = /\bif\b|&&|\|\||\?|\bcase\b|\bwhile\b|\bfor\b/g; for (const dizin of ["bayrak", "durum"]) { let karar = 0; let satir = 0; const dosyalar = readdirSync(dizin).sort(); for (const d of dosyalar) { const metin = readFileSync(`${dizin}/${d}`, "utf8").replace(/^\/\/.*$/gm, ""); karar += (metin.match(KARAR) ?? []).length; satir += metin.split("\n").filter((s) => s.trim().length > 0).length; } console.log(`${dizin.padEnd(7)} dosya=${dosyalar.length} satir=${satir} karar noktasi=${karar} cevrimsel karmasiklik=${karar + 1}`); }
bayrak dosya=1 satir=47 karar noktasi=21 cevrimsel karmasiklik=22 durum dosya=2 satir=26 karar noktasi=1 cevrimsel karmasiklik=2
Yirmi ikiye karşı iki. Fark, kararların koddan veriye taşınmasından gelir: geçiş kuralları artık dallanma değil, eşleme girdisidir. İkinci sayı bunun neden hataya yer bırakmadığını gösterir.
// bedel.mjs — temsil uzayi ve gecis hedefi yanlis yazildiginda ortaya cikan gecikme import { cpSync, readFileSync, writeFileSync } from "node:fs"; import { DURUMLAR } from "./durum/durumlar.mjs"; const BAYRAK = 5; console.log(`bayrak: temsil edilebilir birlesim=${2 ** BAYRAK} gecerli durum=${Object.keys(DURUMLAR).length}`); console.log(`durum : temsil edilebilir birlesim=${Object.keys(DURUMLAR).length} gecerli durum=${Object.keys(DURUMLAR).length}`); cpSync("durum", "durum-hatali", { recursive: true }); writeFileSync( "durum-hatali/durumlar.mjs", readFileSync("durum/durumlar.mjs", "utf8").replace('dagitimaVer: "dagitimda"', 'dagitimaVer: "dagitimdaa"'), ); const t = (await import("./durum-hatali/teslimat.mjs")).teslimat("TS-3"); let olayNo = 0; for (const o of ["topla", "yolaCik", "dagitimaVer", "teslimEt"]) { olayNo += 1; try { const kabul = t.olay(o); console.log(`olay ${olayNo} ${o.padEnd(12)} kabul=${kabul ? 1 : 0}`); } catch (e) { console.log(`olay ${olayNo} ${o.padEnd(12)} ${e.constructor.name} firlatildi`); break; } }
bayrak: temsil edilebilir birlesim=32 gecerli durum=6 durum : temsil edilebilir birlesim=6 gecerli durum=6 olay 1 topla kabul=1 olay 2 yolaCik kabul=1 olay 3 dagitimaVer kabul=1 olay 4 teslimEt TypeError firlatildi
Otuz ikiye karşı altı. Beş bağımsız bayrak otuz iki birleşim üretir ve bunların yirmi altısı hiçbir gerçek duruma karşılık gelmez; yolda olmadan dağıtımda olmak ya da hem teslim hem iade olmak temsil edilebilir. Durum nesnesi tek bir değişken tuttuğu için temsil uzayı geçerli durum kümesine eşittir.
Çıktının son dört satırı bedelin ilk kalemidir. Geçiş hedefi bir harf yanlış yazıldığında
dagitimaVer olayı kabul edilir, hata ancak bir sonraki olayda ortaya çıkar. Geçişler veri
olduğu için hedef adları kod değildir ve yanlış ad yazıldığı yerde değil, kullanıldığı yerde
patlar. Bedelin ikinci kalemi dosya sayısıdır: bir dosyaya karşı iki dosya, ve bir olayın kabul
edilip edilmediğini anlamak için ikisini de okumak gerekir.
Yeni Durum Eklendiğinde
Adres teyidi bekleyen gönderiler için beklemede durumu ve iki yeni olay isteniyor:
yoldaki gönderi beklet ile bekletiliyor, devamEt ile yeniden yola çıkıyor. Betik değişikliği
iki sürüme de uygular, değişen satırları sayar ve aynı olay dizisini ikisinde de koşar.
// yenidurum.mjs — "beklemede" durumunu iki surume de ekler ve degisimi olcer import { cpSync, readFileSync, writeFileSync } from "node:fs"; cpSync("bayrak", "bayrak-yeni", { recursive: true }); cpSync("durum", "durum-yeni", { recursive: true }); const degistir = (yol, arayan, yeni) => { const eski = readFileSync(yol, "utf8"); writeFileSync(yol, eski.replace(arayan, yeni)); }; degistir( "bayrak-yeni/teslimat.mjs", " const d = { toplandi: false, yolda: false, dagitimda: false, teslim: false, iade: false };", " const d = { toplandi: false, yolda: false, dagitimda: false, teslim: false, iade: false, beklemede: false };", ); degistir( "bayrak-yeni/teslimat.mjs", " d.iade ? \"iade\" : d.teslim ? \"teslim\" : d.dagitimda ? \"dagitimda\" : d.yolda ? \"yolda\" : d.toplandi ? \"toplandi\" : \"olusturuldu\";", " d.iade ? \"iade\" : d.teslim ? \"teslim\" : d.beklemede ? \"beklemede\" : d.dagitimda ? \"dagitimda\" : d.yolda ? \"yolda\" : d.toplandi ? \"toplandi\" : \"olusturuldu\";", ); degistir( "bayrak-yeni/teslimat.mjs", ' if (ad === "dagitimaVer") {\n if (d.yolda && d.dagitimda === false) {', ` if (ad === "beklet") { if (d.yolda && d.beklemede === false && d.dagitimda === false) { d.beklemede = true; return true; } return false; } if (ad === "devamEt") { if (d.beklemede) { d.beklemede = false; return true; } return false; } if (ad === "dagitimaVer") { if (d.yolda && d.dagitimda === false && d.beklemede === false) {`, ); degistir( "durum-yeni/durumlar.mjs", ' yolda: durum("yolda", { dagitimaVer: "dagitimda", iadeEt: "iade" }),', ` yolda: durum("yolda", { dagitimaVer: "dagitimda", iadeEt: "iade", beklet: "beklemede" }), beklemede: durum("beklemede", { devamEt: "yolda", iadeEt: "iade" }),`, ); const fark = (a, b) => { const x = readFileSync(a, "utf8").split("\n"); const y = readFileSync(b, "utf8").split("\n"); const kume = new Set(x.map((s) => s.trim())); return y.filter((s) => s.trim().length > 0 && kume.has(s.trim()) === false).length; }; console.log(`bayrak: eklenen/degisen satir=${fark("bayrak/teslimat.mjs", "bayrak-yeni/teslimat.mjs")}`); console.log(`durum : eklenen/degisen satir=${fark("durum/durumlar.mjs", "durum-yeni/durumlar.mjs")}`); const b = (await import("./bayrak-yeni/teslimat.mjs")).teslimat("TS-2"); const c = (await import("./durum-yeni/teslimat.mjs")).teslimat("TS-2"); for (const t of [b, c]) { const iz = []; for (const o of ["topla", "yolaCik", "beklet", "dagitimaVer", "devamEt", "dagitimaVer", "teslimEt"]) { iz.push(`${o}=${t.olay(o) ? "1" : "0"}`); } console.log(`${t.durumAdi().padEnd(10)} ${iz.join(" ")}`); }
bayrak: eklenen/degisen satir=9 durum : eklenen/degisen satir=2 teslim topla=1 yolaCik=1 beklet=1 dagitimaVer=0 devamEt=1 dagitimaVer=1 teslimEt=1 teslim topla=1 yolaCik=1 beklet=1 dagitimaVer=0 devamEt=1 dagitimaVer=1 teslimEt=1
Dokuz satıra karşı iki satır, ve iki sürüm aynı olay dizisinde aynı kabul dizisini ve aynı son
durumu veriyor. Dokuz satırın bir kısmı var olan bir koşulun içine giriyor — dagitimaVer
koşuluna üçüncü bir denetim eklendi — yani yeni durum eski geçişlerin davranışını da riske
atıyor. Durum sürümünde eski geçiş satırlarına dokunulmadı.
Özet
- Durum kalıbı her durumu kendi geçiş kümesini bilen bir nesne yapar; teslimat nesnesi hangi olayın geçerli olduğunu bilmez, elindeki durum nesnesine sorar.
- Otuz durum-olay çiftinin denenmesinde bayrak sürümü 3 geçersiz geçişi kabul etti, durum sürümü 0; iki sürüm de hiçbir geçerli geçişi reddetmedi.
- Çevrimsel karmaşıklık 22’den 2’ye indi; temsil edilebilir birleşim sayısı 32’den 6’ya indi, yani 26 anlamsız bayrak birleşimi ortadan kalktı.
- Yeni bir durum ve iki olay eklendiğinde bayrak sürümünde 9, durum sürümünde 2 satır değişti; iki sürüm aynı olay dizisinde aynı sonucu verdi.
- Bedel: dosya sayısı 1’den 2’ye çıktı ve geçiş hedefi yanlış yazıldığında hata yazıldığı olayda değil bir sonraki olayda ortaya çıktı.
Sonraki Adım
Durum nesnesi bir soruyu tek bir yerden yanıtlıyor: bu olay burada geçerli mi. Kitaplığın ücret düzeltmeleri tarafında ise soru tek bir yere sorulamıyor. Bir gönderiye sözleşmeli müşteri indirimi, hacim indirimi, kampanya indirimi ve elle girilen düzeltme sırayla uygulanabilir; kuralların bir kısmı gönderiye uymaz ve atlanır, bir kısmı uygulandıktan sonra zinciri keser. Bugün bu kurallar tek gövdede iç içe koşullarla duruyor ve sıraları gövdenin içine gömülü. Sonraki ders bu gövdenin karar noktalarını sayar, kural sırası değiştiğinde düzenlenen satırları ölçer, sonra isteği işleyiciler arasında aktaran bir zincir kurup aynı sayıları yeniden hesaplar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.