Ders 08 / 15
Şema Uyumluluk Denetimi
İki şema sürümünü karşılaştıran ve kırıcı değişikliği yayımdan önce durduran denetim: değişiklik türlerinin istek ve yanıt yönünde ayrı uyumluluk sınıflarına düşmesi, genişlet–daralt sırasının kapıyı yeşile çevirmesi ve denetimin uyumlu saydığı bir küme genişletmesinin tüketiciyi kırması.
İçindekiler
Önceki iki denetim de koşarak çalıştı: bir süreç kalkıyor, istek gidiyor, yanıt inceleniyordu. Sağlayıcının asıl sorusu ise değişikliği yayımlamadan önce sorulur ve koşum gerektirmez. Bir alan eklemek, bir alan kaldırmak, bir türü daraltmak, isteğe bağlı bir alanı zorunlu yapmak: bu dört değişiklik türünden hangisi eski tüketiciyi kırar?
Kırıcı değişikliğin tanımı Web API Tasarımı kursunda, maliyetinin ölçümü Uygulama Katmanı ve Servis Etkileşimi kursunda kuruldu; ikisi de burada tekrarlanmaz. Bu dersin konusu o bilginin otomatik bir denetime çevrilmesidir: iki şema sürümü karşılaştırılır, her değişiklik bir türe ve bir uyumluluk sınıfına yerleştirilir, kırıcı sınıfta bir değişiklik varsa takım kırmızı döner.
Şema Sürümleri ve Öneriler
Yayımlanan şema kitap yanıtını ve ayırma isteğini birlikte tarif ediyor. Üç öneri var:
A dört değişiklik türünü bir arada taşıyor, B aynı ihtiyacı genişlet–daralt sırasının
birinci adımıyla karşılıyor, C yalnız bir değer kümesini genişletiyor.
// sema.mjs — yayimlanan sema ve uc oneri; her alan tip, zorunluluk ve deger kumesi tasir const A = (tip, zorunlu, kume) => (kume ? { tip, zorunlu, kume } : { tip, zorunlu }); export const YAYIN = { yanit: { isbn: A("dizgi", true), baslik: A("dizgi", true), raftaAdet: A("sayi", true), durum: A("dizgi", true, ["rafta", "oduncte", "kayip"]), oduncGun: A("sayi", true), etiketler: A("dizi", false), }, istek: { uyeNo: A("dizgi", true), sube: A("dizgi", false), adet: A("sayi", false) }, }; // A: dort degisiklik turu bir arada. B: genislet adimi. C: yalniz kume genisler. export const ONERI = { A: { yanit: { ...YAYIN.yanit, sonGun: A("dizgi", false) }, istek: { ...YAYIN.istek, sube: A("dizgi", true), adet: A("tamsayi", false) }, }, B: { yanit: { ...YAYIN.yanit, sonGun: A("dizgi", false) }, istek: { ...YAYIN.istek } }, C: { yanit: { ...YAYIN.yanit, durum: A("dizgi", true, ["rafta", "oduncte", "kayip", "rafta-yeni"]) }, istek: { ...YAYIN.istek }, }, }; delete ONERI.A.yanit.etiketler; // alan kaldirma
Denetimin kendisi iki parçadır: farkı türlere ayıran bir karşılaştırma ve türleri sınıflara bağlayan bir tablo. Tablonun iki sütunu olması bu dersin ana ayrımıdır — aynı değişiklik türü istek yönünde ve yanıt yönünde farklı sınıfa düşer.
// uyumluluk.mjs — iki sema surumu arasindaki degisiklikleri turlerine ve siniflarina ayirir const DAR = { sayi: "tamsayi" }; export const SINIF = { "istege bagli alan ekleme": { yanit: "uyumlu", istek: "uyumlu" }, "zorunlu alan ekleme": { yanit: "uyumlu", istek: "kirici" }, "alan kaldirma": { yanit: "kirici", istek: "uyumlu" }, "tur daraltma": { yanit: "uyumlu", istek: "kirici" }, "tur genisletme": { yanit: "kirici", istek: "uyumlu" }, "zorunlu yapma": { yanit: "uyumlu", istek: "kirici" }, "istege bagli yapma": { yanit: "kirici", istek: "uyumlu" }, "kumeye deger ekleme": { yanit: "uyumlu", istek: "kirici" }, "kumeden deger cikarma": { yanit: "kirici", istek: "uyumlu" }, }; export function fark(eski, yeni) { const d = []; for (const [ad, k] of Object.entries(eski)) { const y = yeni[ad]; if (y === undefined) { d.push({ ad, tur: "alan kaldirma" }); continue; } if (y.tip !== k.tip) d.push({ ad, tur: DAR[k.tip] === y.tip ? "tur daraltma" : "tur genisletme" }); if (y.zorunlu && !k.zorunlu) d.push({ ad, tur: "zorunlu yapma" }); if (k.zorunlu && !y.zorunlu) d.push({ ad, tur: "istege bagli yapma" }); if ((y.kume ?? []).some((v) => !(k.kume ?? []).includes(v))) d.push({ ad, tur: "kumeye deger ekleme" }); if ((k.kume ?? []).some((v) => !(y.kume ?? []).includes(v))) d.push({ ad, tur: "kumeden deger cikarma" }); } for (const [ad, y] of Object.entries(yeni)) { if (ad in eski) continue; d.push({ ad, tur: y.zorunlu ? "zorunlu alan ekleme" : "istege bagli alan ekleme" }); } return d; } // Bir oneriyi iki yonde de degerlendirir; donen listenin her satiri siniflandirilmis bir degisikliktir. export const degerlendir = (yayin, oneri) => ["yanit", "istek"].flatMap((yon) => fark(yayin[yon], oneri[yon]).map((x) => ({ yon, ...x, sinif: SINIF[x.tur][yon] })));
Türlerin Sınıflara Dağılımı
// dagilim.mjs — degisiklik turlerinin uyumluluk siniflarina dagilimi ve uc onerinin sayimi import { YAYIN, ONERI } from "./sema.mjs"; import { SINIF, degerlendir } from "./uyumluluk.mjs"; console.log(`${"degisiklik turu".padEnd(26)}${"yanitta".padStart(9)}${"istekte".padStart(9)}`); for (const [tur, s] of Object.entries(SINIF)) { console.log(`${tur.padEnd(26)}${s.yanit.padStart(9)}${s.istek.padStart(9)}`); } for (const [ad, o] of Object.entries(ONERI)) { const d = degerlendir(YAYIN, o); console.log(`\noneri ${ad}: ${d.length} degisiklik, ${d.filter((x) => x.sinif === "kirici").length} kirici`); for (const x of d) console.log(` ${`${x.yon}.${x.ad}`.padEnd(17)}${x.tur.padEnd(26)}-> ${x.sinif}`); }
degisiklik turu yanitta istekte istege bagli alan ekleme uyumlu uyumlu zorunlu alan ekleme uyumlu kirici alan kaldirma kirici uyumlu tur daraltma uyumlu kirici tur genisletme kirici uyumlu zorunlu yapma uyumlu kirici istege bagli yapma kirici uyumlu kumeye deger ekleme uyumlu kirici kumeden deger cikarma kirici uyumlu oneri A: 4 degisiklik, 3 kirici yanit.etiketler alan kaldirma -> kirici yanit.sonGun istege bagli alan ekleme -> uyumlu istek.sube zorunlu yapma -> kirici istek.adet tur daraltma -> kirici oneri B: 1 degisiklik, 0 kirici yanit.sonGun istege bagli alan ekleme -> uyumlu oneri C: 1 degisiklik, 0 kirici yanit.durum kumeye deger ekleme -> uyumlu
Tablonun iki sütunu birbirinin aynası. Yanıtta bir alanı kaldırmak kırıcıdır, çünkü tüketici onu okuyordur; istekte bir alanı kaldırmak kırıcı değildir, çünkü eski istemci göndermeye devam eder ve alan yok sayılır. Aynı biçimde bir alanı zorunlu yapmak yanıtta zarar vermez — alan zaten hep gelecektir — istekte ise eski istemcinin gönderdiği bütün isteklerin reddedilmesine yol açar. Tür daraltmak yanıtta uyumludur, çünkü tamsayı hâlâ bir sayıdır; istekte kırıcıdır, çünkü eski istemci ondalık gönderebilir.
Bu simetri, sekiz satırlık tablonun ezberlenmesi gereken bir liste olmadığını gösterir. Tek bir soru vardır ve yön onu belirler: değişiklikten sonra karşı tarafın elindeki eski kod hâlâ geçerli mi. Yanıtta karşı taraf okur, istekte yazar.
Kabul Kapısı
Denetim, Kalite ve Test Temelleri kursunda tanımlanan kabul kapısı olarak çalışır: kırıcı sınıfta bir değişiklik varsa yayın durur.
// uyumluluk.test.mjs — yayin oncesi kapi: kirici degisiklik varsa takim kirmizi doner // Kullanim: ONERI=A|B|C node --test uyumluluk.test.mjs import { test } from "node:test"; import assert from "node:assert/strict"; import { YAYIN, ONERI } from "./sema.mjs"; import { degerlendir } from "./uyumluluk.mjs"; const AD = process.env.ONERI ?? "B"; test(`oneri ${AD}: yayimlanan semaya gore kirici degisiklik yok`, () => { const kirici = degerlendir(YAYIN, ONERI[AD]).filter((x) => x.sinif === "kirici"); assert.equal(kirici.length, 0, `kirici degisiklik: ${kirici.map((x) => `${x.yon}.${x.ad} ${x.tur}`).join(", ")}`); });
for o in A B C; do ONERI=$o node --test --test-reporter=tap uyumluluk.test.mjs | grep -E '^(ok|not ok)|kirici degisiklik:' | sed 's/^ *//' done
not ok 1 - oneri A: yayimlanan semaya gore kirici degisiklik yok kirici degisiklik: yanit.etiketler alan kaldirma, istek.sube zorunlu yapma, istek.adet tur daraltma ok 1 - oneri B: yayimlanan semaya gore kirici degisiklik yok ok 1 - oneri C: yayimlanan semaya gore kirici degisiklik yok
Yakalanan hata sınıfı, sözleşmenin yayımdan önce kırılmasıdır. Öneri A kırmızı döndü ve üç kırıcı değişikliği yön bilgisiyle birlikte adlandırdı. Düzeltme, aynı ihtiyacı genişlet–daralt sırasının birinci adımına çevirmektir: yeni alan eskilerin yanına eklenir, kaldırma ve zorunlu yapma üçüncü adıma bırakılır. Öneri B tam olarak budur ve kapı yeşile döner.
Bu denetimin önceki iki dersten farkı, kırılmayı hiçbir tüketici çalışmadan görmesidir. Sözleşme testi kırılmayı gösterebilmek için tüketicinin beklentisini ve sağlayıcının sürümünü birlikte koşturmak zorundaydı; buradaki denetim iki metni karşılaştırıyor. Karşılığında bildiği şey daha azdır: hangi tüketicinin kırıldığını söyleyemez, yalnız birinin kırılabileceğini söyler.
Denetimin Uyumlu Saydığı Kırılma
Öneri C kapıdan geçti. Kitap durumu için tanımlı değer kümesine yeni bir değer eklendi:
depoya yeni giren ve rafta duran kitaplar artık rafta-yeni olarak bildiriliyor. Eski
yanıtların hepsi hâlâ geçerli, hiçbir alan kaybolmadı, hiçbir tür değişmedi.
// kacan.mjs — denetimin uyumlu saydigi degisiklik tuketiciyi kiriyor import { YAYIN, ONERI } from "./sema.mjs"; import { degerlendir } from "./uyumluluk.mjs"; // Odunc servisinin karar dali: bilinmeyen durum degeri "rafta degil" sayilir. const oduncKarari = (k) => (k.durum === "rafta" && k.raftaAdet > 0 ? { verilir: true, ileti: `${k.isbn} verildi` } : { verilir: false, ileti: `rafta yok (durum ${k.durum}, raftaAdet ${k.raftaAdet})` }); const d = degerlendir(YAYIN, ONERI.C); console.log(`denetim: oneri C -> ${d.length} degisiklik, ${d.filter((x) => x.sinif === "kirici").length} kirici, kapi yesil`); const RAF = { isbn: "978-0262033848", durum: "rafta", raftaAdet: 2 }; const YENI = { ...RAF, durum: "rafta-yeni" }; // yeni gelen raf: kitap rafta ve odunc verilebilir for (const [ad, k] of [["eski deger", RAF], ["yeni deger", YENI]]) { const karar = oduncKarari(k); console.log(`${ad.padEnd(11)} -> verilir=${String(karar.verilir).padEnd(5)} ${karar.ileti}`); } console.log(`semadaki durum degeri ${ONERI.C.yanit.durum.kume.length}, tuketicinin odunc verilebilir saydigi 1`); console.log(`birim degisikligi semayi degistirmez -> ${degerlendir(YAYIN, YAYIN).length} degisiklik`);
denetim: oneri C -> 1 degisiklik, 0 kirici, kapi yesil eski deger -> verilir=true 978-0262033848 verildi yeni deger -> verilir=false rafta yok (durum rafta-yeni, raftaAdet 2) semadaki durum degeri 4, tuketicinin odunc verilebilir saydigi 1 birim degisikligi semayi degistirmez -> 0 degisiklik
Kaçan hata sınıfı, biçimi bozmadan anlamı değiştiren değişikliktir. Rafta duran bir kitap ödünç verilemez oldu; şema açısından hiçbir kural çiğnenmedi. Kümeye değer eklemek yanıt yönünde uyumlu sayılır, çünkü şema doğrulaması açısından öyledir — eski değerler geçerliliğini korur. Kırılan şey şema değil, tüketicinin o kümeyi tükettiğini varsayan kodudur. Bu varsayım hiçbir şemada yazılı değildir.
Son satır sınırı daha da net gösteriyor. Ödünç süresinin gün yerine hafta sayılmaya başlaması, alan adını da türünü de değiştirmez; iki şema birebir aynı kalır ve denetim sıfır değişiklik bildirir. Şemaya bakan hiçbir denetim birim, ölçek ya da anlam değişikliğini göremez.
Denetimin Bedeli
// bedel.mjs — semaya bakan denetimin bedeli: sifir surec, sifir istek import { createServer } from "node:http"; import { YAYIN, ONERI } from "./sema.mjs"; import { degerlendir } from "./uyumluluk.mjs"; const TEKRAR = 200, oneri = Object.keys(ONERI).length; const t0 = performance.now(); for (let i = 0; i < TEKRAR; i += 1) for (const o of Object.values(ONERI)) degerlendir(YAYIN, o); const t1 = performance.now(); // Karsilastirma noktasi: ayni makinede tek bir yerel istegin gidip gelmesi. const sunucu = createServer((_, y) => y.end("{}")); await new Promise((c) => sunucu.listen(0, "127.0.0.1", c)); const t2 = performance.now(); await (await fetch(`http://127.0.0.1:${sunucu.address().port}/`)).text(); const t3 = performance.now(); sunucu.close(); const alan = Object.keys(YAYIN.yanit).length + Object.keys(YAYIN.istek).length; console.log(`sema ${alan} alan (yanit ${Object.keys(YAYIN.yanit).length}, istek ${Object.keys(YAYIN.istek).length}), ${oneri} oneri`); console.log(`denetim ${TEKRAR * oneri} degerlendirme, 0 surec, 0 istek, 0 test verisi`); console.log(`bir degerlendirme tek bir yerel istekten kisa : ${(t1 - t0) / (TEKRAR * oneri) < t3 - t2}`); console.log(`oran en az elli kat : ${(t3 - t2) / ((t1 - t0) / (TEKRAR * oneri)) >= 50}`);
sema 9 alan (yanit 6, istek 3), 3 oneri denetim 600 degerlendirme, 0 surec, 0 istek, 0 test verisi bir degerlendirme tek bir yerel istekten kisa : true oran en az elli kat : true
Mutlak süreler makineye bağlıdır; oranın yönü değişmez. Bu denetim hiçbir süreç ayağa kaldırmıyor, hiçbir istek yollamıyor ve hiçbir test verisi hazırlamıyor: girdisi iki metin, çıktısı bir sınıflandırılmış liste. Tek bir yerel isteğin süresinde yüzlerce değerlendirme yapılabilir; bu yüzden denetim her değişiklik önerisinde koşturulabilir.
Koşumdan bağımsız bedel iki yerdedir. Birincisi, şemanın yayımlanan sürümünün bir yerde saklanması gerekliliğidir: karşılaştırma iki metin ister, tek metinle uyumluluk sorusu sorulamaz. İkincisi, sınıf tablosunun bakımıdır — dokuz satır elle yazılmıştır ve şema diline yeni bir kısıt eklendiğinde tabloya da satır eklenmelidir. Eksik satır sessiz kalır ve sessiz kalan bir denetim, olmayan bir denetimden ayırt edilemez.
Özet
- Şema uyumluluk denetimi iki şema sürümünü karşılaştırır, farkı değişiklik türlerine ayırır ve her türü bir uyumluluk sınıfına yerleştirir.
- Aynı tür istek ve yanıt yönünde farklı sınıfa düşer; tek soru, değişiklikten sonra karşı taraftaki eski kodun hâlâ geçerli olup olmadığıdır.
- Dört değişiklik türünü bir arada taşıyan öneri üç kırıcı değişiklikle kırmızı döndü; aynı ihtiyaç genişlet–daralt sırasının birinci adımına çevrildiğinde kapı yeşile döndü.
- Kaçan sınıf biçimi bozmadan anlamı değiştiren değişikliktir: değer kümesine eklenen bir değer uyumlu sayıldı, oysa rafta duran kitap ödünç verilemez oldu. Birim değişikliğinde iki şema birebir aynı kalır ve denetim sıfır değişiklik bildirir.
- Bedel: sıfır süreç, sıfır istek, sıfır test verisi; karşılığında yayımlanan şemanın saklanması ve dokuz satırlık sınıf tablosunun elle bakımı gerekir.
- Denetim hangi tüketicinin kırıldığını söyleyemez; yalnız birinin kırılabileceğini söyler.
Sonraki Adım
Üç denetim de sağlayıcının hazır olduğunu varsaydı: şema denetimi canlı servise istek yolladı, sözleşme testi sağlayıcının sürümünü ayağa kaldırdı, uyumluluk denetimi yayımlanan şemayı okudu. Oysa sözleşmenin en çok işe yaradığı an, sağlayıcının henüz yazılmadığı andır. Tüketici takımı beklemek zorunda değildir, çünkü sözleşme bellidir. Sonraki ders sözleşmeden yanıt üreten bir sahte sunucu kurar, ödünç servisini ona karşı geliştirir ve tek bir soruyu ölçer: sahte sunucu gerçeğinden kaç noktada sapıyor ve bu sapmaların kaçını buraya kadar kurulan denetimler kapatabiliyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.