İçeriğe geç
academia.sh

Ders 15 / 25

Bileşim Kalıpları

Sarmalayan bileşen ile yuva tabanlı bileşimin karşılaştırması; seçenek patlaması, özellik zincirinin ölçülmesi, kalıtım yerine bileşim ve iki kalıbın hangi koşulda doğru olduğu.

İçindekiler

Önceki konu tek bir bileşeni sonuna kadar götürdü: yerel durumu, yan etkilerinin temizlenmesi, türetilmiş değerlerinin bellenmesi, hata yalıtımı ve ağacın dışına çizim. Bileşen artık kendi başına eksiksiz bir birimdir. Ancak bir arayüz tek bileşenden oluşmaz. Kuzey Yamaç Ölçüm İstasyonu sayfasında on beş ölçüm rozeti, bir ölçüm tablosu ve bir süzgeç paneli var; üçü de aynı yapı taşlarından kuruluyor.

Bu ders, tek bileşen çözüldükten sonra kalan soruyu ele alır: iki bileşen bir araya getirilirken hangisi diğerinin neyini bilecek? Yanıtın iki ayrı kalıbı vardır ve aralarındaki seçim, bileşenin ömrü boyunca ne kadar değişeceğini belirler.

İki Kalıp

Bileşim (composition), bir bileşenin başka bileşenlerden kurulmasıdır. (Terim M14/K03 Düzen Sistemleri kursunda çizim katmanlarının birleştirilmesi için de kullanılmıştı; burada kastedilen bileşen ağacının kurulmasıdır.) İki kalıbı vardır.

Sarmalama. Bir bileşen, başka bir bileşeni içeride çağırır ve dışarıya kendi özellik kümesini sunar. (Bileşen Kavramı dersinde tanımlanan girdi özelliği, bu konu boyunca kısaca özellik diye anılır.) Çağıran, sarmalananı hiç görmez; yalnızca sarmalayanın özelliklerini doldurur. Yapı bileşenin içinde sabittir.

Yuva tabanlı bileşim. Bileşen, iç yapısında bir delik bırakır ve o deliğe konacak ağacı dışarıdan alır. Ne konacağını çağıran belirler; bileşen yalnızca nereye konacağını bilir. Web Bileşenleri konusundaki Şablonlar ve Yuvalar dersinde bu, tarayıcının kendi yuva atama algoritmasıyla gösterilmişti; çerçeve düzeyinde aynı fikir bileşenin bir özelliği olarak geçer: değeri metin ya da sayı değil, bir ağaç parçasıdır.

İkisi aynı çıktıyı üretebilir. Ayrıştıkları yer çıktı değil, değişim karşısındaki davranıştır.

Seçenek Patlaması

Ölçüm rozetiyle başlayalım. İlk sürümde rozet başlığı, değeri ve birimi çiziyor. Sonra eşik aşımını göstermesi isteniyor; sonra kaynağın adını, sonra eğilim okunu, sonra bir yenileme düğmesini, sonra bir not satırını.

Sarmalayan kalıpta her yeni istek bir mantıksal özellik ve bir dallanma ekler.

// bilesim.mjs — sarmalayan bilesen ile yuva tabanli bilesimin karsilastirmasi
const oge = (ad, ozn = {}, ...cocuklar) => ({ ad, ozn, cocuklar: cocuklar.flat().filter(Boolean) });
const metin = (deger) => ({ ad: "#metin", ozn: {}, cocuklar: [], metin: deger });

// Ortak cekirdek: basligi, degeri ve birimi cizen rozet.
const cekirdek = ({ baslik, deger, birim }, ...ek) =>
  oge("div", { class: "rozet" },
    oge("span", { class: "rozet-baslik" }, metin(baslik)),
    oge("output", { class: "rozet-deger" }, metin(`${deger} ${birim}`)),
    ...ek);

// (a) Sarmalayan bilesen: her yeni ihtiyac bir mantiksal ozellik ve bir dallanma ekler.
function rozetSarmalayanV1({ baslik, deger, birim, esikAsildi, kaynakGoster, kaynak }) {
  const ek = [];
  if (esikAsildi) ek.push(oge("span", { class: "rozet-uyari" }, metin("esik asildi")));
  if (kaynakGoster) ek.push(oge("small", { class: "rozet-kaynak" }, metin(kaynak)));
  return cekirdek({ baslik, deger, birim }, ...ek);
}

// Altinci ihtiyac geldiginde ayni govde yeniden acilir.
function rozetSarmalayanV2({ baslik, deger, birim, esikAsildi, kaynakGoster, kaynak,
                             egilim, yenileDugmesi, notMetni }) {
  const ek = [];
  if (esikAsildi) ek.push(oge("span", { class: "rozet-uyari" }, metin("esik asildi")));
  if (kaynakGoster) ek.push(oge("small", { class: "rozet-kaynak" }, metin(kaynak)));
  if (egilim) ek.push(oge("span", { class: "rozet-egilim" }, metin(egilim)));
  if (yenileDugmesi) ek.push(oge("button", { type: "button" }, metin("yenile")));
  if (notMetni) ek.push(oge("p", { class: "rozet-not" }, metin(notMetni)));
  return cekirdek({ baslik, deger, birim }, ...ek);
}

// (b) Yuva tabanli bilesim: bilesen deligi birakir, doldurmayi cagirana birakir.
const rozetYuvali = ({ baslik, deger, birim, ek = [] }) => cekirdek({ baslik, deger, birim }, ...ek);

// Iki yaklasimin ayni girdi icin urettigi agaclari karsilastir.
const yaz = (d, girinti = 0) => {
  const satir = d.metin !== undefined
    ? `${"  ".repeat(girinti)}"${d.metin}"`
    : `${"  ".repeat(girinti)}${d.ad}${d.ozn.class ? "." + d.ozn.class : ""}`;
  return [satir, ...d.cocuklar.flatMap((c) => yaz(c, girinti + 1))];
};

const sarmalanan = rozetSarmalayanV2({
  baslik: "Sıcaklık", deger: -4.2, birim: "°C",
  esikAsildi: true, kaynakGoster: true, kaynak: "istasyon-3",
  egilim: null, yenileDugmesi: false, notMetni: null,
});

const yuvali = rozetYuvali({
  baslik: "Sıcaklık", deger: -4.2, birim: "°C",
  ek: [
    oge("span", { class: "rozet-uyari" }, metin("esik asildi")),
    oge("small", { class: "rozet-kaynak" }, metin("istasyon-3")),
  ],
});

console.log("sarmalanan agac:");
console.log(yaz(sarmalanan).join("\n"));
console.log("\nyuvali agac:");
console.log(yaz(yuvali).join("\n"));
console.log("\nagaclar ayni mi:", JSON.stringify(sarmalanan) === JSON.stringify(yuvali));

// Olcum: ihtiyac sayisi arttikca iki yaklasimin yuzeyi nasil buyuyor?
const govde = (f) => f.toString().split("\n").length;
const mantiksal = (f) => (f.toString().match(/\bif \(/g) ?? []).length;
console.log("\nyaklasim                 gövde satırı  dallanma  çağıran tarafın seçenekleri");
for (const [ad, f, ihtiyac] of [
  ["sarmalayan (2 ihtiyaç)", rozetSarmalayanV1, 2],
  ["sarmalayan (5 ihtiyaç)", rozetSarmalayanV2, 5],
  ["yuva tabanlı", rozetYuvali, 0],
]) {
  console.log(
    `${ad.padEnd(24)} ${String(govde(f)).padStart(12)} ${String(mantiksal(f)).padStart(9)} ` +
      `${(ihtiyac ? 2 ** ihtiyac : "sınırsız").toString().padStart(28)}`
  );
}
sarmalanan agac:
div.rozet
  span.rozet-baslik
    "Sıcaklık"
  output.rozet-deger
    "-4.2 °C"
  span.rozet-uyari
    "esik asildi"
  small.rozet-kaynak
    "istasyon-3"

yuvali agac:
div.rozet
  span.rozet-baslik
    "Sıcaklık"
  output.rozet-deger
    "-4.2 °C"
  span.rozet-uyari
    "esik asildi"
  small.rozet-kaynak
    "istasyon-3"

agaclar ayni mi: true

yaklasim                 gövde satırı  dallanma  çağıran tarafın seçenekleri
sarmalayan (2 ihtiyaç)              6         2                            4
sarmalayan (5 ihtiyaç)             10         5                           32
yuva tabanlı                        1         0                     sınırsız

İki ağaç birebir aynı. Verilen bir durumda hangi kalıbın kullanıldığı çıktıdan anlaşılmaz; fark yalnızca kaynak metinde ve değişim maliyetindedir.

Tablo bu maliyeti veriyor. Beş mantıksal özellikli sarmalayan bileşen 32 ayrı özellik birleşimi tanımlar. Bu birleşimlerin tümü anlamlı değildir — yenileme düğmesi ile not satırı bir arada belki hiç istenmez — ama bileşen hepsini kabul eder ve hiçbirini sınamamıştır. Beş dallanma, beş özelliğin birbirini nasıl etkilediğine dair beş sessiz karardır: eklenme sırası sabittir, çağıran değiştiremez.

Yuva tabanlı sürümde gövde tek satırdır ve dallanma yoktur. Çağıranın seçenekleri sayılamaz, çünkü çağıran istediği ağacı verebilir. Bileşen bu ağaç hakkında hiçbir varsayım yapmaz; yalnızca çekirdeğin sonuna ekler.

Asıl ayrım altıncı istekte görünür. Sarmalayan kalıpta altıncı istek bileşenin gövdesini yeniden açar: yeni bir özellik, yeni bir dallanma, yeniden gözden geçirilecek bir dosya. Yuva tabanlı kalıpta bileşene dokunulmaz; yeni düğüm çağıran tarafta kurulur.

Özellik Zinciri

Seçenek patlaması bileşenin kendi gövdesinde birikir. İkinci maliyet ise ağaç boyunca yayılır: bir değeri kullanan bileşen, o değeri üreten bileşenin uzağındaysa aradaki her katman değeri taşımak zorundadır. Buna özellik zinciri (prop drilling) denir.

Ölçüm tablosunda birim sistemi tercihini sayfa düzeyi biliyor, biçimlendirmeyi ise hücre yapıyor. Arada tablo, gövde ve satır var.

// ozellik-zinciri.mjs — ara katmanlarin yalnizca tasidigi ozelliklerin sayilmasi
const oge = (ad, ozn = {}, ...cocuklar) => ({ ad, ozn, cocuklar: cocuklar.flat().filter(Boolean) });
const metin = (deger) => ({ ad: "#metin", ozn: {}, cocuklar: [], metin: deger });
const bicim = (c, sistem) => (sistem === "si" ? `${c.toFixed(1)} °C` : `${(c * 1.8 + 32).toFixed(1)} °F`);

// Her bilesen aldigi ozellikleri ve bunlarin kacini kendisi kullandigini bildirir.
const defter = [];
const kaydet = (ad, alinan, kullanilan) =>
  defter.push({ ad, alinan, kullanilan, tasinan: alinan.filter((a) => !kullanilan.includes(a)) });

// (a) Zincirli akis: birim sistemi ust katmandan en alta kadar her imzada yeniden yazilir.
function zDeger({ santigrat, birimSistemi }) {
  kaydet("Değer", ["santigrat", "birimSistemi"], ["santigrat", "birimSistemi"]);
  return oge("output", {}, metin(bicim(santigrat, birimSistemi)));
}
function zSatir({ baslik, santigrat, birimSistemi }) {
  kaydet("Satır", ["baslik", "santigrat", "birimSistemi"], ["baslik"]);
  return oge("tr", {}, oge("th", {}, metin(baslik)), oge("td", {}, zDeger({ santigrat, birimSistemi })));
}
function zGovde({ olcumler, birimSistemi }) {
  kaydet("Gövde", ["olcumler", "birimSistemi"], ["olcumler"]);
  return oge("tbody", {}, ...olcumler.map((o) => zSatir({ ...o, birimSistemi })));
}
function zTablo({ baslik, olcumler, birimSistemi }) {
  kaydet("Tablo", ["baslik", "olcumler", "birimSistemi"], ["baslik"]);
  return oge("table", {}, oge("caption", {}, metin(baslik)), zGovde({ olcumler, birimSistemi }));
}

// (b) Yuva tabanli akis: deger dugumu, birim sisteminin bilindigi yerde kurulur;
// ara katmanlar hazir dugumu tasir, veriyi degil.
function yDeger({ santigrat, birimSistemi }) {
  kaydet("Değer/yuva", ["santigrat", "birimSistemi"], ["santigrat", "birimSistemi"]);
  return oge("output", {}, metin(bicim(santigrat, birimSistemi)));
}
function ySatir({ baslik, hucre }) {
  kaydet("Satır/yuva", ["baslik", "hucre"], ["baslik", "hucre"]);
  return oge("tr", {}, oge("th", {}, metin(baslik)), oge("td", {}, hucre));
}
function yGovde({ satirlar }) {
  kaydet("Gövde/yuva", ["satirlar"], ["satirlar"]);
  return oge("tbody", {}, ...satirlar);
}
function yTablo({ baslik, govde }) {
  kaydet("Tablo/yuva", ["baslik", "govde"], ["baslik", "govde"]);
  return oge("table", {}, oge("caption", {}, metin(baslik)), govde);
}

const olcumler = [{ baslik: "Üst yamaç", santigrat: -4.2 }, { baslik: "Alt yamaç", santigrat: 1.6 }];
const sistem = "imperyal";

const zincirli = zTablo({ baslik: "Kuzey Yamaç", olcumler, birimSistemi: sistem });
const yuvali = yTablo({
  baslik: "Kuzey Yamaç",
  govde: yGovde({
    satirlar: olcumler.map((o) =>
      ySatir({ baslik: o.baslik, hucre: yDeger({ santigrat: o.santigrat, birimSistemi: sistem }) })),
  }),
});

const yaz = (d, g = 0) => [
  d.metin !== undefined ? `${"  ".repeat(g)}"${d.metin}"` : `${"  ".repeat(g)}${d.ad}`,
  ...d.cocuklar.flatMap((c) => yaz(c, g + 1)),
];
console.log("zincirli akışın ürettiği ağaç:");
console.log(yaz(zincirli).join("\n"));
console.log("iki ağaç aynı mı:", JSON.stringify(zincirli) === JSON.stringify(yuvali));

const ozet = new Map();
for (const k of defter) ozet.set(k.ad, k);
console.log("\nbileşen       aldığı  kullandığı  yalnızca taşıdığı");
for (const [ad, k] of ozet)
  console.log(`${ad.padEnd(13)} ${String(k.alinan.length).padStart(6)} ${String(k.kullanilan.length).padStart(11)}  ${k.tasinan.join(", ") || "-"}`);

const topla = (ek) => [...ozet.values()].filter((k) => k.ad.includes("/yuva") === ek).reduce((t, k) => t + k.tasinan.length, 0);
console.log(`\nzincirli akışta yalnızca taşınan özellik sayısı: ${topla(false)}`);
console.log(`yuva tabanlı akışta yalnızca taşınan özellik sayısı: ${topla(true)}`);
zincirli akışın ürettiği ağaç:
table
  caption
    "Kuzey Yamaç"
  tbody
    tr
      th
        "Üst yamaç"
      td
        output
          "24.4 °F"
    tr
      th
        "Alt yamaç"
      td
        output
          "34.9 °F"
iki ağaç aynı mı: true

bileşen       aldığı  kullandığı  yalnızca taşıdığı
Tablo              3           1  olcumler, birimSistemi
Gövde              2           1  birimSistemi
Satır              3           1  santigrat, birimSistemi
Değer              2           2  -
Değer/yuva         2           2  -
Satır/yuva         2           2  -
Gövde/yuva         1           1  -
Tablo/yuva         2           2  -

zincirli akışta yalnızca taşınan özellik sayısı: 5
yuva tabanlı akışta yalnızca taşınan özellik sayısı: 0

Zincirli akışta üç ara katmanın hiçbiri birim sistemini kullanmıyor; üçü de yalnızca aşağıya geçiriyor. Sayı beş, çünkü ölçüm verisi de aynı yolu izliyor. Yuva tabanlı akışta bu sayı sıfır: değer düğümü, birim sisteminin zaten bilindiği yerde kuruluyor ve ara katmanlar hazır düğümü taşıyor.

Zincirin gerçek bedeli imzalarda değil, bağımlılıktadır. Satır bileşeni birim sistemini imzasında taşıdığı için, birim sisteminin varlığından haberdar olmuş olur. Bu özellik kaldırıldığında ya da adı değiştiğinde, onu hiç kullanmayan üç bileşen de değişmek zorundadır. Yuva tabanlı akışta satır bileşeni bir hücre alır; hücrenin neye göre biçimlendirildiğini bilmez ve bilmediği şey değiştiğinde etkilenmez.

Bu, Bağlam Aktarımı dersindeki mekanizmanın neden var olduğunu da açıklar. Bağlam, zinciri kesmenin ikinci yoludur: değer ağacın bir noktasında sağlanır, isteyen düğüm doğrudan okur. Yuva tabanlı bileşim ile bağlam birbirinin yerine geçmez — biri yapıyı, diğeri veriyi aşağı taşır. Ölçüm tablosunda hücrenin nasıl görüneceği bir yapı kararıdır ve yuvaya aittir; birim sistemi tercihi ise sayfanın her yerinde okunan bir veridir ve bağlama aittir.

Kalıtım Yerine Bileşim

Sarmalama kalıbının ucundaki bir uygulama, bileşenleri kalıtımla türetmektir: temel rozet sınıfı, ondan türeyen uyarı rozeti, ondan türeyen eğilimli uyarı rozeti. Bu yol arayüz ağaçlarında iki nedenle tıkanır.

Birincisi, kalıtım tek eksenlidir. Uyarı gösteren rozetle kaynağı gösteren rozet ayrı dallardaysa, ikisini birden isteyen üçüncü bir rozet için ortak bir ata yoktur; her yeni birleşim yeni bir sınıf ister. Beş bağımsız istek 32 sınıfa çıkar. Yuva tabanlı bileşimde aynı 32 birleşim tek bileşenin 32 farklı çağrısıdır.

İkincisi, kalıtım gövdenin içine erişim verir; bileşim vermez. Türeyen sınıf atasının iç yapısına yaslanır, ata değişince türeyen kırılır. Bileşimde bileşenin dışarıya söz verdiği tek şey özellik yüzeyidir; iç yapısı sözleşmenin parçası değildir.

Sarmalama Ne Zaman Doğrudur

Yuva tabanlı bileşimin üstünlüğü koşulsuz değildir. Sarmalama, bileşenin kararı sahiplenmesi gerektiği yerde doğrudur.

Süzgeç panelindeki eşik alanı buna örnektir. Alanın etiketi, hata iletisi ve geçersizlik bildirimi arasındaki ilişki sabit olmalıdır: etiket alana bağlanmalı, hata iletisi alana ek açıklama olarak iliştirilmeli, geçersizlik durumu her ikisine de yansımalıdır. Bu ilişki çağırana bırakılırsa her çağrıda yeniden kurulur ve bir yerde unutulur. Bileşen burada deliği bırakmaz; yapıyı kendi içinde kilitler ve dışarıya yalnızca etiket metniyle hata iletisini alan bir yüzey sunar.

Ölçüt şudur: yapı doğruluk taşıyorsa sarmalanır, yalnızca görünüm taşıyorsa yuvaya bırakılır. Rozetin sonuna ne eklendiği bir görünüm kararıdır; etiketin alana nasıl bağlandığı bir doğruluk kararıdır. Bir bileşende ikisi bir arada bulunabilir: rozet çekirdeği kilitli, sonu açıktır.

Özet

  • Bileşimin iki kalıbı vardır: sarmalayan bileşen yapıyı içeride sabitler, yuva tabanlı bileşim yapının bir bölümünü çağırandan alır.
  • İki kalıp aynı ağacı üretebilir; ayrım çıktıda değil, yeni bir istek geldiğinde hangi dosyanın açıldığındadır.
  • Mantıksal özelliklerle kurulan sarmalayan bileşende seçenek sayısı üstel büyür; beş bağımsız istek 32 birleşim ve beş dallanma demektir.
  • Özellik zinciri, ara katmanların kullanmadığı özellikleri taşımasıdır; ölçüsü “yalnızca taşınan özellik” sayısıdır ve yuva tabanlı bileşimde sıfıra iner.
  • Yuva yapıyı, bağlam veriyi aşağı taşır; ikisi birbirinin yerine geçmez.
  • Doğruluk taşıyan yapı sarmalanır, yalnızca görünüm taşıyan yapı yuvaya bırakılır.

Sonraki Adım

Bu ders yapının yeniden kullanımını çözdü: aynı ağacı farklı içeriklerle kurmanın yolu belli. Ancak bileşenlerde yeniden kullanılan tek şey yapı değildir. Süzgeç panelindeki arama alanı girdiyi geciktirerek tetikliyor, ölçüm tablosu sıralama tercihini adres çubuğuyla eşitliyor, rozet aralıklı olarak yenileniyor. Üçünün de içinde durum, yan etki ve temizlik var; üçü de birbirinden bağımsız yazılmış ve üçü de aynı hataları tekrar ediyor. Bu mantık bir bileşene bağlı olduğu sürece taşınamaz. Sonraki ders, durumu ve yan etkiyi işaretleme üretmeden paketleyen birimi kurar ve bu birimlerin birbirine eklenmesinin hangi kurala bağlı olduğunu 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