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.