İçeriğe geç
academia.sh

Ders 21 / 34

Geriye Dönük Uyumluluk

Aynı değişikliğin istek ve yanıt yönünde ters sınıflandırılması, iki şema sürümünü karşılaştırıp kırıcı değişiklikleri saptayan bir fark alıcı ve sürüm numarasının tartışmadan farktan türetilmesi.

İçindekiler

Bir önceki ders iki sürümü yan yana yayımladı ama sürüm numarasının neden ikiye çıktığını sormadı. uye alanının nesneye dönmesi kırıcı sayıldı, sube alanına yeni bir seçenek eklemek konuşulmadı. Bu sorular sezgiye bırakıldığında her ekip başka yanıt verir; sürüm numarası kanıya dayanır ve “bu küçük bir değişiklik” cümlesiyle atlanır.

Bu ders kırıcılığı tanımlar, bir fark alıcı yazar ve sürüm numarasını farktan türetir. Tanımın çıkış noktası basit bir gözlemdir: aynı değişiklik, istek yönünde uyumluyken yanıt yönünde kırıcı olabilir.

Yönün Belirlediği Kırıcılık

Bir sözleşmenin iki yönü vardır. İstemci gövde gönderir, sunucu gövde üretir. İstemcinin gönderdiği için sunucu bir kabul kümesi tanımlar; sunucunun ürettiği için de bir üretim kümesi.

Sunucunun kabul ettiği daralırsa eski istemciler dışarıda kalır. Kabul edilenler genişlerse eski istemciler etkilenmez; gönderdikleri hâlâ kümenin içindedir. Yani istek yönünde daralma kırıcı, genişleme uyumludur.

Sunucunun ürettiği genişlerse eski istemciler tanımadıkları bir şeyle karşılaşır. Ürettiği daralırsa istemciler etkilenmez; okudukları alanlar hâlâ yerindedir. Yani yanıt yönünde genişleme kırıcı, daralma uyumludur.

İki yönün kuralı tam olarak birbirinin tersidir. Bu ters ilişki, /isbn alanının kaldırılması gibi tek bir değişikliğin istekte uyumlu, yanıtta kırıcı olmasının nedenidir: istekten kaldırılan alanı eski istemci göndermeye devam eder, sunucu yok sayar; yanıttan kaldırılan alanı ise eski istemci aramaya devam eder ve bulamaz.

Aşağıdaki iki şema, ödünç kaynağının bir sürüm aralığındaki hâlini verir. Her alan üç şey bildirir: tipi, zorunlu olup olmadığı ve varsa alabileceği değerler.

// semalar.mjs — odunc kaynaginin iki sema surumu
// Alan: { tip, zorunlu, degerler }.  degerler yoksa deger kumesi sinirsizdir.
export const V1 = {
  surum: "1.4.2",
  istek: {
    "/uye":       { tip: "dizgi", zorunlu: true },
    "/isbn":      { tip: "dizgi", zorunlu: true },
    "/sube":      { tip: "dizgi", zorunlu: false, degerler: ["merkez", "sahil"] },
    "/gunSayisi": { tip: "sayi",  zorunlu: false },
  },
  yanit: {
    "/id":         { tip: "dizgi", zorunlu: true },
    "/uye":        { tip: "dizgi", zorunlu: true },
    "/isbn":       { tip: "dizgi", zorunlu: true },
    "/iadeTarihi": { tip: "dizgi", zorunlu: true },
    "/durum":      { tip: "dizgi", zorunlu: true, degerler: ["acik", "kapali"] },
  },
};

export const V2 = {
  istek: {
    "/uye":       { tip: "dizgi",   zorunlu: true },
    "/kalemler":  { tip: "dizi",    zorunlu: true },                                    // yeni zorunlu alan
    "/sube":      { tip: "dizgi",   zorunlu: true, degerler: ["merkez", "sahil", "tepe"] },
    "/gunSayisi": { tip: "tamsayi", zorunlu: false },                                   // tip daraldi
    "/not":       { tip: "dizgi",   zorunlu: false },                                   // yeni istege bagli alan
  },
  yanit: {
    "/id":         { tip: "dizgi", zorunlu: true },
    "/uye":        { tip: "nesne", zorunlu: true },                                     // tip degisti
    "/kalemler":   { tip: "dizi",  zorunlu: true },                                     // yeni alan
    "/iadeTarihi": { tip: "dizgi", zorunlu: false },                                    // artik bulunmayabilir
    "/durum":      { tip: "dizgi", zorunlu: true, degerler: ["acik", "kapali", "gecikmis"] },
  },
};

Fark Alıcı

Fark alıcı iki şemayı gezer, her farkı bulur ve yön kuralını uygular. Tip değişiklikleri için küçük bir alt tip ilişkisi gerekir: her tam sayı bir sayıdır, tersi doğru değildir. Aralarında böyle bir ilişki olmayan tipler her iki yönde de kırıcıdır.

// fark.mjs — iki sema surumunu karsilastirir, kirici degisiklikleri saptar, surumu turetir
// Kullanim: node fark.mjs [hosgoru]   ("hosgoru": istemciler bilinmeyen degeri hos gorur)

import { V1, V2 } from "./semalar.mjs";

const HOSGORU = process.argv[2] === "hosgoru";

// Tip kafesi: tamsayi, sayi'nin alt tipidir. Alt tip, ust tipin kabul ettiginin bir bolumudur.
const ALT_TIPLER = { tamsayi: ["sayi"], tarih: ["dizgi"] };
const altTipMi = (a, b) => a === b || (ALT_TIPLER[a] ?? []).includes(b);

// Iki yon, iki kural. Istek: sunucunun kabul ettigi daralirsa kirici.
// Yanit: sunucunun urettigi genislerse kirici.
function karsilastir(eski, yeni, yon) {
  const kirici = yon === "istek"
    ? { daralma: true, genisleme: false, zorunluEkleme: true, zorunluKalkma: false, kaldirma: false }
    : { daralma: false, genisleme: true, zorunluEkleme: false, zorunluKalkma: true, kaldirma: true };
  const bulgular = [];
  const ekle = (sinif, ad, aciklama) => bulgular.push({ sinif, yon, ad, aciklama });

  for (const [ad, e] of Object.entries(eski)) {
    const y = yeni[ad];
    if (!y) { ekle(kirici.kaldirma ? "kirici" : "uyumlu", ad, "alan kaldırıldı"); continue; }
    if (!e.zorunlu && y.zorunlu) ekle(kirici.zorunluEkleme ? "kirici" : "uyumlu", ad, "isteğe bağlıydı, zorunlu oldu");
    if (e.zorunlu && !y.zorunlu) ekle(kirici.zorunluKalkma ? "kirici" : "uyumlu", ad, "zorunluydu, isteğe bağlı oldu");
    if (e.tip !== y.tip) {
      const daralma = altTipMi(y.tip, e.tip), genisleme = altTipMi(e.tip, y.tip);
      const sinif = daralma ? (kirici.daralma ? "kirici" : "uyumlu")
        : genisleme ? (kirici.genisleme ? "kirici" : "uyumlu")
        : "kirici";                                       // ilgisiz tipler her yonde kirici
      ekle(sinif, ad, `tip ${e.tip} → ${y.tip}` + (daralma ? " (daralma)" : genisleme ? " (genişleme)" : " (ilgisiz)"));
    }
    const eD = e.degerler, yD = y.degerler;
    if (eD && yD) {
      const kalkan = eD.filter((d) => !yD.includes(d)), gelen = yD.filter((d) => !eD.includes(d));
      if (kalkan.length) ekle(kirici.daralma ? "kirici" : "uyumlu", ad, `değer kümesi daraldı: ${kalkan.join(",")} kalktı`);
      if (gelen.length) {
        const kiriciMi = kirici.genisleme && !(yon === "yanit" && HOSGORU);
        ekle(kiriciMi ? "kirici" : "uyumlu", ad, `değer kümesi genişledi: ${gelen.join(",")} eklendi`);
      }
    }
  }
  for (const [ad, y] of Object.entries(yeni)) {
    if (eski[ad]) continue;
    const kiriciMi = y.zorunlu && kirici.zorunluEkleme;
    ekle(kiriciMi ? "kirici" : "ekleme", ad, y.zorunlu ? "zorunlu alan eklendi" : "isteğe bağlı alan eklendi");
  }
  return bulgular;
}

const bulgular = [...karsilastir(V1.istek, V2.istek, "istek"), ...karsilastir(V1.yanit, V2.yanit, "yanit")];
const SIRA = { kirici: 0, ekleme: 1, uyumlu: 2 };
bulgular.sort((a, b) => SIRA[a.sinif] - SIRA[b.sinif] || a.yon.localeCompare(b.yon));

console.log(`hoşgörü bildirimi: ${HOSGORU ? "var" : "yok"}\n`);
console.log("sınıf    yön     alan          değişiklik");
for (const b of bulgular) console.log(`${b.sinif.padEnd(8)} ${b.yon.padEnd(7)} ${b.ad.padEnd(13)} ${b.aciklama}`);

const sayim = { kirici: 0, ekleme: 0, uyumlu: 0 };
for (const b of bulgular) sayim[b.sinif]++;
const [ana, ikincil, yama] = V1.surum.split(".").map(Number);
const sonraki = sayim.kirici ? `${ana + 1}.0.0` : sayim.ekleme ? `${ana}.${ikincil + 1}.0` : `${ana}.${ikincil}.${yama + 1}`;

console.log(`\nkırıcı: ${sayim.kirici}   ekleme: ${sayim.ekleme}   uyumlu: ${sayim.uyumlu}`);
console.log(`mevcut sürüm: ${V1.surum}   türetilen sürüm: ${sonraki}`);
console.log(`kırıcılar ayıklansaydı: ${ana}.${ikincil + 1}.0`);
hoşgörü bildirimi: yok

sınıf    yön     alan          değişiklik
kirici   istek   /sube         isteğe bağlıydı, zorunlu oldu
kirici   istek   /gunSayisi    tip sayi → tamsayi (daralma)
kirici   istek   /kalemler     zorunlu alan eklendi
kirici   yanit   /uye          tip dizgi → nesne (ilgisiz)
kirici   yanit   /isbn         alan kaldırıldı
kirici   yanit   /iadeTarihi   zorunluydu, isteğe bağlı oldu
kirici   yanit   /durum        değer kümesi genişledi: gecikmis eklendi
ekleme   istek   /not          isteğe bağlı alan eklendi
ekleme   yanit   /kalemler     zorunlu alan eklendi
uyumlu   istek   /isbn         alan kaldırıldı
uyumlu   istek   /sube         değer kümesi genişledi: tepe eklendi

kırıcı: 7   ekleme: 2   uyumlu: 2
mevcut sürüm: 1.4.2   türetilen sürüm: 2.0.0
kırıcılar ayıklansaydı: 1.5.0

Son iki satır ters ilişkiyi doğrudan gösteriyor. /isbn alanının kaldırılması istekte uyumlu, yanıtta kırıcı. /sube alanının değer kümesine tepe eklenmesi istekte uyumlu; /durum alanının değer kümesine gecikmis eklenmesi yanıtta kırıcı. Aynı işlem, karşı yönde karşıt sınıfta.

ekleme satırlarındaki /kalemler alanı da bu kuralın sonucudur: yanıta zorunlu bir alan eklemek uyumludur, çünkü eski istemci o alanı zaten okumuyordu. İsteğe zorunlu alan eklemek ise kırıcıdır ve tabloda öyle görünüyor.

Sürüm numarası artık tartışılan değil, türetilen bir değerdir. Yedi kırıcı değişiklik ana sürümü yükseltir. Son satır ayrı bir seçenek gösterir: kırıcılar yayından ayıklanıp yalnız eklemeler çıkarılsaydı sürüm 1.5.0 olurdu. Bu iki satır, “sürümü ne yapalım” tartışmasını “yayına neyi koyacağız” tartışmasına çevirir; ikincisi yanıtlanabilir bir sorudur.

Hoşgörü Bildirimi

Yanıttaki değer kümesinin genişlemesi, listedeki en tartışmalı kalemdir. gecikmis durumunun eklenmesi eski istemcileri bozar mı? Yanıt, sözleşmede ne yazdığına bağlıdır. Sözleşme “bilinmeyen bir durum değeri gelirse istemci onu tanınmayan olarak ele almalıdır” diyorsa, genişleme kırıcı değildir; demiyorsa kırıcıdır.

node fark.mjs hosgoru | tail -4
kırıcı: 6   ekleme: 2   uyumlu: 3
mevcut sürüm: 1.4.2   türetilen sürüm: 2.0.0
kırıcılar ayıklansaydı: 1.5.0

Kırıcı sayısı yediden altıya iniyor. Buradaki ders, hoşgörünün bir sözleşme maddesi olmasıdır. Sunucu tarafında alınan bir karar değildir; istemcilerin gerçekten öyle yazılmış olmasını gerektirir ve bu ancak sözleşmede önceden duyurulmuşsa beklenebilir. Sözleşmeye sonradan eklenen bir hoşgörü maddesi, kendisi kırıcı bir değişikliktir.

Farkın Söylemediği

Fark alıcı neyin bozulabileceğini söyler, neyin bozulacağını değil. Ayrımı görmek için aynı şemalar gerçek bir eski istemciyle sınanır.

// sinama.mjs — siniflandirmayi eski istemciyi calistirarak sinar
// Eski istemci v1 govdesi yollar ve v1 yanitini okur; sunucu v2 semasindadir.
import { V1, V2 } from "./semalar.mjs";

const ESKI_ISTEK = { "/uye": "U-1001", "/isbn": "978-0262033848", "/sube": "sahil", "/gunSayisi": 14.5 };
const YENI_YANIT = { "/id": "O-1", "/uye": { kimlik: "U-1001" }, "/kalemler": [{}], "/durum": "gecikmis" };

const tipi = (d) =>
  Array.isArray(d) ? "dizi" : typeof d === "object" ? "nesne"
    : typeof d === "number" ? (Number.isInteger(d) ? "tamsayi" : "sayi") : "dizgi";

// 1) Eski istemcinin govdesi yeni semaya sokulur.
const istekHatalari = [];
for (const [ad, k] of Object.entries(V2.istek)) {
  const d = ESKI_ISTEK[ad];
  if (d === undefined) { if (k.zorunlu) istekHatalari.push(`${ad}: zorunlu alan gönderilmemiş`); continue; }
  if (k.tip !== tipi(d) && !(k.tip === "sayi" && tipi(d) === "tamsayi")) istekHatalari.push(`${ad}: ${tipi(d)} gönderildi, ${k.tip} bekleniyor`);
  if (k.degerler && !k.degerler.includes(d)) istekHatalari.push(`${ad}: ${d} kabul edilmiyor`);
}

// 2) Yeni yanit, eski istemcinin bekledigi semayla okunur.
const yanitHatalari = [];
for (const [ad, k] of Object.entries(V1.yanit)) {
  const d = YENI_YANIT[ad];
  if (d === undefined) { if (k.zorunlu) yanitHatalari.push(`${ad}: beklenen alan yanıtta yok`); continue; }
  if (k.tip !== tipi(d)) yanitHatalari.push(`${ad}: ${tipi(d)} geldi, ${k.tip} bekleniyordu`);
  if (k.degerler && !k.degerler.includes(d)) yanitHatalari.push(`${ad}: ${d} tanınmayan değer`);
}

console.log("— eski istemcinin gövdesi yeni sunucuda —");
for (const h of istekHatalari) console.log(`  ${h}`);
console.log(`  reddedilen alan sayısı: ${istekHatalari.length}`);

console.log("\n— yeni sunucunun yanıtı eski istemcide —");
for (const h of yanitHatalari) console.log(`  ${h}`);
console.log(`  okunamayan alan sayısı: ${yanitHatalari.length}`);
— eski istemcinin gövdesi yeni sunucuda —
  /kalemler: zorunlu alan gönderilmemiş
  /gunSayisi: sayi gönderildi, tamsayi bekleniyor
  reddedilen alan sayısı: 2

— yeni sunucunun yanıtı eski istemcide —
  /uye: nesne geldi, dizgi bekleniyordu
  /isbn: beklenen alan yanıtta yok
  /iadeTarihi: beklenen alan yanıtta yok
  /durum: gecikmis tanınmayan değer
  okunamayan alan sayısı: 4

Fark alıcı istek yönünde üç kırıcı değişiklik bulmuştu; bu istemcide yalnız ikisi gerçekleşti. Farkı yaratan /sube alanıdır: isteğe bağlıyken zorunlu olmuştur, ama bu istemci onu zaten gönderiyordu. Değişiklik sözleşme gereği kırıcıdır — sürüm numarasını yükseltir — ama bu istemci için maliyeti sıfırdır.

Sözleşme ile maliyet ayrı ölçülerdir. Sürüm numarası sözleşmeye bakar ve tek bir istemci etkilenmese bile yükselir; yayın planı ise maliyete bakar. Tasarım Sistemleri kursundaki göç maliyeti hesabı da bu ayrımı kurmuştu: kırıcılık sözleşmenin, etkilenen çağrı yeri sayısı gerçekliğin ölçüsüdür.

Yanıt yönündeki dört kalem ise tam olarak gerçekleşmiştir. Bu, yönler arasındaki başka bir asimetriyi gösterir: istek yönündeki kırıcılık istemcinin ne gönderdiğine bağlıdır, yanıt yönündeki kırıcılık istemcinin ne okuduğuna. İstemciler genellikle şemadaki her alanı gönderme zorunda değildir ama okudukları alanlar sabittir; bu yüzden yanıt yönündeki kırıcı değişiklikler daha yüksek oranda gerçek maliyete dönüşür.

Özet

  • Sözleşmenin iki yönü ters kurallara tabidir: sunucunun kabul ettiği daralırsa istek yönünde kırıcı, ürettiği genişlerse yanıt yönünde kırıcıdır.
  • Aynı değişiklik iki yönde karşıt sınıfa düşer; alanın istekten kaldırılması uyumlu, yanıttan kaldırılması kırıcıdır.
  • Yanıta zorunlu alan eklemek uyumludur, isteğe zorunlu alan eklemek kırıcıdır; tip daralması istekte, tip genişlemesi yanıtta kırıcıdır.
  • Sürüm numarası fark sonucundan türetilir; kırıcılar yayından ayıklandığında türetilen numara da değişir, böylece tartışma sürümden yayın kapsamına taşınır.
  • Bilinmeyen değere hoşgörü bir sözleşme maddesidir ve önceden duyurulmuş olmalıdır; sonradan eklenmesi kendisi kırıcı bir değişikliktir.
  • Fark alıcı neyin bozulabileceğini söyler; hangi istemcinin gerçekten bozulacağı ayrı bir ölçümdür ve yanıt yönündeki kırıcılıklar daha yüksek oranda gerçek maliyete dönüşür.

Sonraki Adım

Fark alıcı yedi kırıcı değişiklik saptadı ve sürümü 2.0.0’a çıkardı, ama bir soruyu yanıtsız bıraktı: eski sürüm ne zamana kadar yaşayacak? Sürüm numarasını yükseltmek eski istemcileri kendiliğinden taşımaz; taşınmaları için önce durumdan haberdar olmaları, sonra da geçiş için süre bulmaları gerekir. Sonraki ders bu süreyi bir politikaya çevirir: kullanımdan kaldırmayı yanıtın kendisinde duyuran başlıkları ekler, kullanım telemetrisini modelleyip geçiş süresinin sonunda hâlâ eski sürümde kalan tüketici oranını hesaplar ve kapatma tarihinin bu orana göre nasıl seçileceğini gösterir.

İ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