İçeriğe geç
academia.sh

Ders 01 / 25

Neden Çerçeve

Arayüzü elle güncellemenin ölçek sorunları; güncelleme yollarının sayısı, durumun iki kopyada tutulması, kaymanın kendini onaran biçimi ve bir arayüz çerçevesinin devraldığı iş.

İçindekiler

Tarayıcı ve Web Platformu kursu istasyon sayfasını programın erişebildiği bir veri yapısına dönüştürdü: düğüm oluşturuldu, dinleyici bağlandı, değişiklik ilgili ögeye yazıldı, sökülürken hepsi geri alındı. Ölçüm rozeti bir bileşene dönüştü, ama panodaki on beş rozetin ölçüm listesiyle eşzamanlı kalması hâlâ elle yazılmış güncelleme koduna bağlı. Bir kayıt eklendiğinde hangi düğümlerin değişeceğini kodun kendisi hesaplıyor.

Bu ders o hesabın neden büyüdükçe hata kaynağına dönüştüğünü ölçer. Sorunun adı beceriksiz kod değildir; buyurgan (imperative) güncellemenin yapısal bir özelliğidir ve pano büyüdükçe kaçınılmaz hâle gelir.

Elle Güncellemenin Üç İşi

Arayüzün her parçası için üç ayrı kod yolu yazılır.

İlk oluşturma düğümleri kurar, özniteliklerini yazar, dinleyicileri bağlar. Güncelleme, durum değiştiğinde var olan düğümlerin hangi alanlarının yeni değeri alacağını belirler. Sökme, parça kaldırılırken dinleyicileri kaldırır, zamanlayıcıları durdurur, gözlemcileri çözer.

Üçü de aynı görünümü betimler ama üçü ayrı yerde yazılıdır. Doğruluk koşulu şudur: ilk oluşturmanın ürettiği ağaç ile, aynı duruma güncelleme yoluyla ulaşılmış ağaç birbirinin aynısı olmalıdır. Bu koşulu hiçbir şey denetlemez. İlk oluşturmaya bir sınıf eklenip güncellemeye eklenmediğinde sayfa ilk yüklemede doğru, ilk etkileşimden sonra yanlış olur.

Güncelleme Yollarının Sayısı

Güncelleme kodunun büyüklüğü parça sayısıyla değil, durum alanı ile parça arasındaki bağlantı sayısıyla ölçülür. Bir alan kaç parçayı besliyorsa, o alanı değiştiren her işleyicinin o kadar yeri güncellemesi gerekir.

// elle-guncelleme.mjs — durum alani ile arayuz parcasi arasindaki guncelleme yollari
// Panodaki her arayuz parcasinin hangi durum alanlarini okudugunu bildiriyoruz.
const PANO = {
  "rozet.deger": ["olcumler"],
  "rozet.renk": ["olcumler", "esikler"],
  "tablo.satirlar": ["olcumler", "suzgec"],
  "tablo.sayac": ["olcumler", "suzgec"],
  "suzgec.secim": ["suzgec"],
  "baslik.zaman": ["sonGuncelleme"],
};

// Bir durum alani degistiginde elle yazilmasi gereken guncelleme yollari:
// o alani okuyan her parca icin bir yol.
function ozet(baslik, parcalar) {
  const tablo = new Map();
  for (const [parca, alanlar] of Object.entries(parcalar))
    for (const alan of alanlar) tablo.set(alan, [...(tablo.get(alan) ?? []), parca]);

  let toplam = 0;
  console.log(baslik);
  for (const [alan, liste] of [...tablo].sort()) {
    toplam += liste.length;
    console.log(`  ${alan.padEnd(14)} ${String(liste.length).padStart(2)} yol  ${liste.join(", ")}`);
  }
  console.log(`  ${String(Object.keys(parcalar).length).padStart(2)} parca, ` +
    `${String(tablo.size).padStart(2)} alan, ${String(toplam).padStart(2)} yol\n`);
  return { parca: Object.keys(parcalar).length, alan: tablo.size, yol: toplam };
}

const once = ozet("Pano — ilk hali", PANO);

// Iki istek: (1) olcum birimi degistirilebilsin — birim, satirlari da rozeti de
// etkiler; (2) pano bir ozet grafigi ve bir esik uyarisi da gostersin.
const SONRA = ozet("Pano — iki istek sonrasi", {
  ...PANO,
  "rozet.birim": ["birim"],
  "tablo.satirlar": ["olcumler", "suzgec", "birim"],
  "grafik.noktalar": ["olcumler", "suzgec", "birim"],
  "uyari.serit": ["olcumler", "esikler", "suzgec"],
});

const yuzde = (y, e) => `%${Math.round((y / e - 1) * 100)}`;
console.log(`parca ${yuzde(SONRA.parca, once.parca)}, ` +
  `alan ${yuzde(SONRA.alan, once.alan)}, yol ${yuzde(SONRA.yol, once.yol)} artti`);
Pano — ilk hali
  esikler         1 yol  rozet.renk
  olcumler        4 yol  rozet.deger, rozet.renk, tablo.satirlar, tablo.sayac
  sonGuncelleme   1 yol  baslik.zaman
  suzgec          3 yol  tablo.satirlar, tablo.sayac, suzgec.secim
   6 parca,  4 alan,  9 yol

Pano — iki istek sonrasi
  birim           3 yol  tablo.satirlar, rozet.birim, grafik.noktalar
  esikler         2 yol  rozet.renk, uyari.serit
  olcumler        6 yol  rozet.deger, rozet.renk, tablo.satirlar, tablo.sayac, grafik.noktalar, uyari.serit
  sonGuncelleme   1 yol  baslik.zaman
  suzgec          5 yol  tablo.satirlar, tablo.sayac, suzgec.secim, grafik.noktalar, uyari.serit
   9 parca,  5 alan, 17 yol

parca %50, alan %25, yol %89 artti

Parça sayısı yarı yarıya arttı, elle yazılacak güncelleme yolu neredeyse ikiye katlandı. Bunun nedeni, yeni parçaların var olan alanları da okumasıdır: grafik olcumler, suzgec ve birim alanlarına bağlanınca bu üç alanı değiştiren her işleyiciye bir satır daha eklenmesi gerekti.

Bağlantı sayısı en kötü durumda alan sayısı ile parça sayısının çarpımına yaklaşır. Bu çarpım, tek bir kişinin akılda tutabileceği bir sayı olmaktan hızla çıkar; sayfaya yeni bir parça ekleyen kişinin, o parçanın okuduğu her alanı kimin nerede değiştirdiğini bilmesi gerekir.

Durumun İki Kopyası

Elle güncellemede durum iki yerde bulunur: program değişkenlerinde ve ağacın kendisinde. Ağaçtaki kopya eksiktir ve kayıplıdır.

Ağaç yalnızca dizgi saklar. Bir sayı öznitelik olarak yazıldığında tipini kaybeder; -4.2 ile "-4.2" arasındaki farkı geri getirmek okuyan tarafın işidir. Ağaç, ekranda görünmeyen durumu hiç saklamaz: süzgeç panelindeki seçim bir sınıf adına indirgenmişse, seçimin gerekçesi ya da önceki değeri kaybolmuştur.

Buradan bir kural çıkar: ağaç bir depo değil, bir çıktıdır. Ağaçtan okuyarak karar veren kod, kendi ürettiği çıktıyı girdi olarak kullanır ve iki kopya arasındaki her ayrışmada yanlış karar verir.

Kaymanın Anatomisi

İki kopyanın ayrışmasına kayma denir. Aşağıdaki model kaymayı, durumdan hesaplanan beklenen metinle ağaçtaki metni karşılaştıran bir denetimle yakalar.

// kayma.mjs — elle guncellemede durum ile agacin ayrisma anini yakalar
// Kucuk bir agac modeli: her dugumun bir adi ve bir metni var.
const agac = {
  "tablo.sayac": { metin: "" },
  "tablo.satirlar": { metin: "" },
  "suzgec.secim": { metin: "" },
};
const yaz = (ad, metin) => { agac[ad].metin = metin; };

const durum = {
  olcumler: [
    { ad: "Sıcaklık", tur: "sicaklik", deger: -4.2 },
    { ad: "Bağıl nem", tur: "nem", deger: 72 },
    { ad: "Rüzgâr", tur: "ruzgar", deger: 11.4 },
    { ad: "Kar derinliği", tur: "kar", deger: 38 },
  ],
  suzgec: "tumu",
  esik: 50,
};

// Durumdan beklenen metinler. Denetim bunlari agactakiyle karsilastirir.
const gorunenler = (d) =>
  d.suzgec === "tumu" ? d.olcumler : d.olcumler.filter((o) => o.deger >= d.esik);
const beklenen = (d) => ({
  "tablo.sayac": `${gorunenler(d).length} / ${d.olcumler.length} ölçüm`,
  "tablo.satirlar": gorunenler(d).map((o) => o.ad).join(" | "),
  "suzgec.secim": d.suzgec,
});

// Ilk kurulum: her dugum elle yaziliyor.
for (const [ad, metin] of Object.entries(beklenen(durum))) yaz(ad, metin);

// Olay isleyicileri agaca dogrudan yaziyor. Ikisi de eksiksiz gorunuyor.
function suzgecDegisti(yeni) {
  durum.suzgec = yeni;
  yaz("suzgec.secim", yeni);
  yaz("tablo.satirlar", gorunenler(durum).map((o) => o.ad).join(" | "));
  yaz("tablo.sayac", `${gorunenler(durum).length} / ${durum.olcumler.length} ölçüm`);
}
function olcumEklendi(olcum) {
  durum.olcumler.push(olcum);
  yaz("tablo.satirlar", gorunenler(durum).map((o) => o.ad).join(" | "));
  // "tablo.sayac" burada yazilmiyor: ekleme yolunu yazan kisi sayaci gormedi.
}

function denetle(etiket) {
  const b = beklenen(durum);
  const kayanlar = Object.keys(b).filter((ad) => agac[ad].metin !== b[ad]);
  console.log(`${etiket}: ${kayanlar.length ? "kayma var" : "tutarli"}`);
  for (const ad of kayanlar)
    console.log(`  ${ad}\n    agac    : ${agac[ad].metin}\n    beklenen: ${b[ad]}`);
}

denetle("kurulum");
suzgecDegisti("esikUstu");
denetle("suzgec degisti");
olcumEklendi({ ad: "Basınç", tur: "basinc", deger: 964 });
denetle("olcum eklendi");
suzgecDegisti("esikUstu");
denetle("suzgec yeniden secildi");
kurulum: tutarli
suzgec degisti: tutarli
olcum eklendi: kayma var
  tablo.sayac
    agac    : 1 / 4 ölçüm
    beklenen: 2 / 5 ölçüm
suzgec yeniden secildi: tutarli

Çıktının üç yeri okunmalıdır.

Birincisi, hata ekleme yolunun eksikliğinden doğar, ekleme kodunun yanlışlığından değil. olcumEklendi yazdığı satırı doğru yazar; yazmadığı satır sorundur. Bu tür bir eksiklik derleyici, tip denetimi ya da birim testi tarafından yakalanmaz, çünkü ortada çağrılmayan bir işlev yoktur.

İkincisi, kayma kendini onarır. Süzgeç yeniden seçildiğinde sayaç doğru değeri alır ve tutarlılık geri gelir. Kendini onaran hata, yeniden üretilmesi en zor hata türüdür: kullanıcının anlattığı sırayı birebir izlemeyen hiç kimse onu göremez.

Üçüncüsü, aynı metin üç ayrı yerde hesaplanmıştır — ilk kurulumda, süzgeç işleyicisinde ve denetimde. Bu tekrar, kaymanın kaynağıdır. Metin tek bir yerde hesaplansaydı eksik güncelleme olanaksız olurdu.

Sıra, Kimlik ve Temizlik

Liste güncellemesi elle yazıldığında üç sorun daha eklenir.

Sıra. Listenin ortasına bir kayıt girdiğinde hangi düğümün nereye taşınacağını çağıran kod hesaplar. Kolay yol, listeyi silip yeniden kurmaktır; bu, odaklanmış girdiyi kaybeder, kaydırma konumunu sıfırlar ve süren geçişleri keser.

Kimlik. Bir düğüm hangi kayda karşılık geliyor sorusunun yanıtı ağaçta yazılı değildir. Dizinle eşleştirilen düğümler, listenin başına ekleme yapıldığında bir kayarak yanlış kayıtla eşleşir. Bu bağ elle kurulduğunda genellikle bir veri özniteliğine yazılır ve her sıralamada güncellenmesi gerekir.

Temizlik. Kaldırılan her düğümün dinleyicisi kaldırılmalıdır; kaldırılmazsa Bellek İncelemesi dersindeki kopmuş düğümler birikir. Sökme kodunun oluşturma kodunun tersini tam olarak yapması gerekir ve bu simetriyi de hiçbir şey denetlemez.

Çerçevenin Devraldığı İş

Buradaki sorunların ortak kaynağı tektir: arayüz, nasıl değiştirileceğiyle tanımlanmıştır. Kodun her satırı bir geçişi anlatır; hiçbir satır sonucu anlatmaz.

Arayüz çerçevesi (framework), bu tanımı tersine çeviren altyapıdır. Yazılan şey, verili bir durumda arayüzün ne olması gerektiğidir; iki durum arasındaki farkı bulmak ve ağaca uygulamak çerçevenin işidir. Çerçeve karşılığında dört sorumluluk devralır:

  • durumdan görünüm tanımı üretmek,
  • iki görünüm tanımı arasındaki farkı hesaplamak,
  • farkı ağaca en az işlemle yazmak,
  • kaldırılan parçaların dinleyicilerini ve kaynaklarını sökmek.

Bu kursta çerçeve sözcüğü yalnızca bu anlamda kullanılır; Ağ Modelleri ve Protokoller kursundaki bağlantı katmanı çerçevesiyle ve gömülü içerik çerçevesiyle ilgisi yoktur.

Devir bedelsiz değildir. Fark hesabı çalışma zamanında yapılır ve maliyeti vardır; çerçeve kendi kurallarını dayatır; hata ayıklarken araya bir katman girer. Bu bedelin ne zaman ödendiği kursun son konusunda ölçülür. Kazanç ise bu derste sayılan şeydir: güncelleme yolları ortadan kalkar, çünkü artık tek bir yol vardır.

Özet

  • Elle güncelleme her parça için ilk oluşturma, güncelleme ve sökme kodunu ayrı ayrı yazmayı gerektirir; üçünün aynı görünümü betimlediğini hiçbir şey denetlemez.
  • Güncelleme kodunun büyüklüğü parça sayısıyla değil, durum alanı ile parça arasındaki bağlantı sayısıyla artar; bu sayı en kötü durumda alan ve parça sayısının çarpımıdır.
  • Ağaç bir depo değil çıktıdır: dizgi dışında tip saklamaz, görünmeyen durumu hiç saklamaz.
  • Kayma, aynı metnin birden çok yerde hesaplanmasından doğar; kendini onaran biçimi onu yeniden üretilmesi zor bir hataya dönüştürür.
  • Liste güncellemesi elle yazıldığında sıra, kimlik ve temizlik sorunları eklenir.
  • Arayüz çerçevesi, “nasıl değiştirileceği” tanımını “ne olması gerektiği” tanımıyla değiştirir ve fark hesabını üstlenir.

Sonraki Adım

Çerçevenin devraldığı işin ilk maddesi, durumdan bir görünüm tanımı üretmektir. Bu, kulağa geldiğinden daha katı bir gerekliliktir: tanımı üreten kodun aynı durum için her zaman aynı sonucu vermesi, ağacın o anki hâline hiç bakmaması ve yazarken hiçbir şeyi değiştirmemesi gerekir. Sonraki ders bu üretimi bir işlev olarak kurar, tek yönlü akışın ne anlama geldiğini gösterir ve görünümün tamamını her seferinde yeniden üretmenin doğru ama pahalı olduğunu ölçer.

İ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