İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat