İçeriğe geç
academia.sh

Ders 04 / 18

Bileşen Tabanlı Mimari

Birimin halkadan yayınlanabilir bileşene taşınması: her bileşenin dışa açtığı ve kapsadığı ad sayısının sayılması, aynı dosyaların iki bölünmesinde birlikte yayın oranının karşılaştırılması, yüzeyi atlayan bir tüketicinin dışa açılan ad sayısını nasıl yükselttiğinin ölçülmesi ve yeni bir yeteneğin düzenlettiği, eklettiği, yeniden yayınlattığı parçaların sayılması.

İçindekiler

Bir önceki dersin bütün ölçüleri halka üzerindeydi ve halka bir kural birimidir. Hangi dosya kümesinin birlikte paketlendiği, hangisinin ayrı sürümlenebildiği ve bir tüketicinin tek bir yetenek için kaç ada bağlandığı o ölçülerde görünmüyordu.

Bu ders birimi değiştirir. Bileşen (component) burada ayrı paketlenen, ayrı sürümlenen ve dışa açtığı adları bildirilmiş bir dosya kümesidir; Bileşen Tabanlı Geliştirme kursundaki bileşen ise ekranda bir parçayı çizen bir görünüm işleviydi — aynı sözcüğün iki ayrı birimi. Ölçüler de değişir: bir bileşenin dışa açtığı ad sayısı, kapsadığı ad sayısı, iki bileşenin birlikte yayınlanma oranı ve yeni bir yeteneğin düzenlettiği dosya sayısı.

Yayın Birimi Olarak Bileşen

Bileşen sınırının nereden geçeceği Tasarım İlkeleri kursundaki Bileşen Uyum İlkeleri ve Bileşen Bağlaşım İlkeleri derslerinde kurulmuştu: yeniden kullanım–yayın eşdeğerliği, ortak kapanış, ortak yeniden kullanım ve kararlı bağımlılıklar. Burada o ilkeler yeniden anlatılmıyor, bir düzen olarak uygulanıyor.

Düzenin taşıyıcı kararı şudur: her bileşen dışa açtığı adları tek bir dosyada bildirir, geri kalanı kendi içinde tutar. Bu karar iki sayıyı ayırır. Dışa açılan ad, bileşenin dışındaki bir dosyanın o bileşenden ithal ettiği addır. Kapsanan ad, bileşenin ihraç ettiği ama dışından kimsenin ithal etmediği addır. İkincisi serbestlik payıdır: kapsanan bir adı değiştirmek hiçbir tüketiciyi kırmaz.

Kitaplığın Bileşenleri

Kitaplık beş bileşene bölünüyor. Üçü ücretlendirme bağlamındadır (ortak, bolge, ucret), biri teslimat operasyonundadır (rota), biri de ikisini tek bir istekte birleştirir (masa). Her bileşenin dis.mjs dosyası o bileşenin yüzeyini bildirir.

mkdir -p bilesenler/ortak bilesenler/bolge bilesenler/ucret bilesenler/rota bilesenler/masa
// bilesenler/ortak/dis.mjs — ortak bileseni: para birimi cevrimi
export const kurus = (lira) => Math.round(lira * 100);
export const yazi = (k) => `${(k / 100).toFixed(2)} TL`;
// bilesenler/bolge/esleme.mjs — bolge bileseni ici: posta kodu esdegeri ve katsayilar
export const POSTA_BOLGE = { 34: "yakin", "06": "orta", 35: "orta", 65: "uzak" };
export const KATSAYI = { yakin: 1, orta: 1.35, uzak: 1.8 };
export const bolgeBul = (postaKodu) => POSTA_BOLGE[postaKodu.slice(0, 2)] ?? "uzak";
// bilesenler/bolge/dis.mjs — bolge bileseninin disa actigi yuzey
import { KATSAYI, bolgeBul } from "./esleme.mjs";

export const bolge = (postaKodu) => bolgeBul(postaKodu);
export const katsayi = (postaKodu) => KATSAYI[bolgeBul(postaKodu)];
// bilesenler/ucret/tarife.mjs — ucret bileseni ici: kademeler, asgari tutar, yuvarlama
import { kurus } from "../ortak/dis.mjs";

export const KADEMELER = [[1, 49.9], [5, 84.9], [15, 149.9], [30, 249.9]]
  .map(([ustAgirlik, lira]) => ({ ustAgirlik, ucret: kurus(lira) }));
export const ASGARI = kurus(39.9);
export const yuvarla = (k) => Math.round(k / 50) * 50;
// bilesenler/ucret/kural.mjs — ucret bileseni ici: ucret kurali
import { ASGARI, KADEMELER, yuvarla } from "./tarife.mjs";

export const ucret = (gonderi, katsayi) => {
  const kademe = KADEMELER.find((k) => gonderi.agirlik <= k.ustAgirlik);
  if (kademe === undefined) throw new RangeError("agirlik kademesi yok");
  return Math.max(yuvarla(kademe.ucret * katsayi), ASGARI);
};
// bilesenler/ucret/dis.mjs — ucret bileseninin disa actigi yuzey
import { katsayi } from "../bolge/dis.mjs";
import { yazi } from "../ortak/dis.mjs";
import { ucret } from "./kural.mjs";

export const teklifVer = (gonderi) => {
  const tutar = ucret(gonderi, katsayi(gonderi.postaKodu));
  return { tutar, gosterim: yazi(tutar) };
};
// bilesenler/rota/tasiyici.mjs — rota bileseni ici: tasiyici listesi ve kapsamlari
export const TASIYICILAR = [
  { ad: "kuzey", kapsam: ["yakin", "orta"], aktarma: 1 },
  { ad: "genel", kapsam: ["yakin", "orta", "uzak"], aktarma: 2 },
];
export const uygunlar = (b) => TASIYICILAR.filter((t) => t.kapsam.includes(b));
// bilesenler/rota/dis.mjs — rota bileseninin disa actigi yuzey
import { bolge } from "../bolge/dis.mjs";
import { uygunlar } from "./tasiyici.mjs";

export const rotaKur = (gonderi) => {
  const secilen = uygunlar(bolge(gonderi.postaKodu))
    .sort((x, y) => x.aktarma - y.aktarma)[0];
  if (secilen === undefined) throw new RangeError("uygun tasiyici yok");
  return { tasiyici: secilen.ad, aktarma: secilen.aktarma };
};
// bilesenler/masa/dis.mjs — masa bileseni: dis istegi iki bilesene dagitir
import { rotaKur } from "../rota/dis.mjs";
import { teklifVer } from "../ucret/dis.mjs";

export const istek = (gonderi) => ({ ...teklifVer(gonderi), ...rotaKur(gonderi) });

Dışa Açılan ve Kapsanan Adlar

Araç her dosyanın ihraç ettiği adları ve ithal satırlarındaki adları okur, sonra aynı dosyaları iki bölünmeye göre gruplar: yeteneğe göre (dizin adı) ve role göre (dis.mjs yüzey, kural.mjs kural, geri kalanı veri). Dosyalar ve ithaller iki bölünmede aynıdır; değişen yalnız sınırın nereden geçtiğidir.

// yuzey-olc.mjs — her birimin ihrac ettigi, disa actigi ve kapsadigi ad sayisini olcer
import { readdirSync, readFileSync } from "node:fs";
import { join, normalize } from "node:path";

const IHRAC = /^export const (\w+)/gm;
const ITHAL = /^import\s*\{([^}]*)\}\s*from\s+"([^"]+)"/gm;
export const BOLUNME = {
  yetenek: (f) => f.dizin,
  rol: (f) => (f.ad === "dis.mjs" ? "arayuz" : f.ad === "kural.mjs" ? "kural" : "veri"),
};

export function oku(kok) {
  const dosyalar = [];
  for (const dizin of readdirSync(kok).sort()) {
    for (const ad of readdirSync(join(kok, dizin)).sort()) {
      const metin = readFileSync(join(kok, dizin, ad), "utf8");
      dosyalar.push({
        yol: `${dizin}/${ad}`, dizin, ad,
        ihrac: [...metin.matchAll(IHRAC)].map((m) => m[1]),
        ithal: [...metin.matchAll(ITHAL)].map((m) => ({
          adlar: m[1].split(",").map((s) => s.trim()).filter((s) => s !== ""),
          hedef: normalize(join(dizin, m[2])),
        })),
      });
    }
  }
  return dosyalar;
}

if (import.meta.filename === process.argv[1]) {
  const kok = process.argv[2];
  const dosyalar = oku(kok);
  const toplamIhrac = dosyalar.reduce((n, f) => n + f.ihrac.length, 0);
  console.log(`${kok}/  dosya = ${dosyalar.length}, toplam ihrac = ${toplamIhrac}`);
  for (const [bad, birimi] of Object.entries(BOLUNME)) {
    const ihrac = new Map();
    const disa = new Map();
    for (const f of dosyalar) {
      ihrac.set(birimi(f), (ihrac.get(birimi(f)) ?? 0) + f.ihrac.length);
      disa.set(birimi(f), disa.get(birimi(f)) ?? new Set());
    }
    for (const f of dosyalar) {
      for (const i of f.ithal) {
        const hedef = dosyalar.find((d) => d.yol === i.hedef);
        if (hedef === undefined || birimi(hedef) === birimi(f)) continue;
        for (const n of i.adlar) disa.get(birimi(hedef)).add(n);
      }
    }
    console.log(`  ${bad} bolunmesi (${ihrac.size} birim)`);
    let td = 0;
    for (const b of [...ihrac.keys()].sort()) {
      const d = disa.get(b).size;
      td += d;
      console.log(`    ${b.padEnd(8)} ihrac ${String(ihrac.get(b)).padStart(2)}`
        + `  disa acilan ${String(d).padStart(2)}  kapsanan ${String(ihrac.get(b) - d).padStart(2)}`);
    }
    console.log(`    ${"toplam".padEnd(8)} ihrac ${String(toplamIhrac).padStart(2)}`
      + `  disa acilan ${String(td).padStart(2)}  kapsanan ${String(toplamIhrac - td).padStart(2)}`);
  }
}
node yuzey-olc.mjs bilesenler
bilesenler/  dosya = 9, toplam ihrac = 16
  yetenek bolunmesi (5 birim)
    bolge    ihrac  5  disa acilan  2  kapsanan  3
    masa     ihrac  1  disa acilan  0  kapsanan  1
    ortak    ihrac  2  disa acilan  2  kapsanan  0
    rota     ihrac  3  disa acilan  1  kapsanan  2
    ucret    ihrac  5  disa acilan  1  kapsanan  4
    toplam   ihrac 16  disa acilan  6  kapsanan 10
  rol bolunmesi (3 birim)
    arayuz   ihrac  7  disa acilan  1  kapsanan  6
    kural    ihrac  1  disa acilan  1  kapsanan  0
    veri     ihrac  8  disa acilan  6  kapsanan  2
    toplam   ihrac 16  disa acilan  8  kapsanan  8

On altı ihraç edilen addan yetenek bölünmesinde 6’sı dışa açılıyor ve 10’u kapsanıyor; rol bölünmesinde 8’i dışa açılıyor, 8’i kapsanıyor. Rol bölünmesinde veri birimi sekiz adından altısını dışa açmak zorundadır, çünkü kuralı ve yüzeyi ondan ayıran sınır tam o adların geçtiği yerdedir. ucret bileşeni ise beş adından dördünü içinde tutuyor; dışarıya yalnız teklifVer çıkıyor. masa bileşeninin dışa açılan adı 0: ona bağlı başka bir bileşen yok, yani Bileşen Bağlaşım İlkeleri dersindeki kararsızlık ölçüsünün üst ucundadır.

Yüzeyi Atlayan Tüketici

Dışa açılan ad sayısı bileşenin tek başına özelliği değil, bileşen ile tüketicilerinin ortak özelliğidir. Aşağıdaki blok ağacın bir kopyasına yüzeyleri atlayıp komşularının içine bağlanan bir bileşen ekliyor.

cp -r bilesenler derin-agac
mkdir -p derin-agac/derin
cat > derin-agac/derin/dis.mjs <<'EOF'
// derin-agac/derin/dis.mjs — yuzeyi atlayip komsularin icine baglanan tuketici
import { KATSAYI, bolgeBul } from "../bolge/esleme.mjs";
import { yazi } from "../ortak/dis.mjs";
import { uygunlar } from "../rota/tasiyici.mjs";
import { ucret } from "../ucret/kural.mjs";

export const istek = (gonderi) => {
  const b = bolgeBul(gonderi.postaKodu);
  const tutar = ucret(gonderi, KATSAYI[b]);
  return { tutar, gosterim: yazi(tutar), tasiyici: uygunlar(b)[0].ad };
};
EOF
node yuzey-olc.mjs derin-agac | head -9
node -e "import('./derin-agac/derin/dis.mjs').then(m => console.log('derin tuketici ' + JSON.stringify(m.istek({ agirlik: 3, postaKodu: '06800' }))))"
derin-agac/  dosya = 10, toplam ihrac = 17
  yetenek bolunmesi (6 birim)
    bolge    ihrac  5  disa acilan  4  kapsanan  1
    derin    ihrac  1  disa acilan  0  kapsanan  1
    masa     ihrac  1  disa acilan  0  kapsanan  1
    ortak    ihrac  2  disa acilan  2  kapsanan  0
    rota     ihrac  3  disa acilan  2  kapsanan  1
    ucret    ihrac  5  disa acilan  2  kapsanan  3
    toplam   ihrac 17  disa acilan 10  kapsanan  7
derin tuketici {"tutar":11450,"gosterim":"114.50 TL","tasiyici":"kuzey"}

Tüketici doğru çalışıyor ve yüzeyden geçen yolla aynı tutarı üretiyor: 11450 kuruş. Değişen şey sınırdır. Dışa açılan ad 6’dan 10’a çıktı, kapsanan ad 10’dan 7’ye indi; bolge bileşeninin kapsanan adı 3’ten 1’e düştü. Tek bir dosya, dokunmadığı üç bileşenin serbestlik payını harcadı. Ortak yeniden kullanım ilkesinin ölçülebilir karşılığı budur: derin ithal, tüketiciyi bileşenin sürüm sözleşmesinin dışında kalan adlara bağlar.

Birlikte Yayın Oranı

İkinci ölçü yayınla ilgilidir. Bir dosya değiştiğinde onu ithal eden her dosya yeniden sınanır ve yayınlanır, bu yüzden bir değişikliğin yeniden yayınlattığı birim kümesi ithal grafiğinden çıkarılabilir. İki birimin birlikte yayın oranı, ikisini birlikte yayınlatan dosya sayısının en az birini yayınlatan dosya sayısına oranıdır.

// yayin-olc.mjs — bir dosya degisince hangi birimlerin yeniden yayinlanmasi gerektigini sayar
import { BOLUNME, oku } from "./yuzey-olc.mjs";

const [kok, hedef] = process.argv.slice(2);
const dosyalar = oku(kok);
const ithalEden = new Map(dosyalar.map((f) => [f.yol, []]));
for (const f of dosyalar) {
  for (const i of f.ithal) if (ithalEden.has(i.hedef)) ithalEden.get(i.hedef).push(f.yol);
}

function kapanis(yol) {
  const gorulen = new Set([yol]);
  for (const y of gorulen) for (const k of ithalEden.get(y)) gorulen.add(k);
  return gorulen;
}

for (const [bad, birimi] of Object.entries(BOLUNME)) {
  const birimOf = new Map(dosyalar.map((f) => [f.yol, birimi(f)]));
  const etkilenen = dosyalar.map((f) => new Set([...kapanis(f.yol)].map((y) => birimOf.get(y))));
  const birimler = [...new Set(birimOf.values())].sort();
  console.log(`${kok}/ ${bad} bolunmesi (${birimler.length} birim)`);
  for (const b of birimler) {
    console.log(`  ${b.padEnd(8)} dosya ${dosyalar.filter((f) => birimi(f) === b).length}`
      + `  yeniden yayinlatan dosya ${etkilenen.filter((e) => e.has(b)).length} / ${dosyalar.length}`);
  }
  let toplam = 0, cift = 0, enYuksek = ["yok", 0];
  for (let i = 0; i < birimler.length; i += 1) {
    for (let j = i + 1; j < birimler.length; j += 1) {
      const [a, b] = [birimler[i], birimler[j]];
      const oran = etkilenen.filter((e) => e.has(a) && e.has(b)).length
        / etkilenen.filter((e) => e.has(a) || e.has(b)).length;
      toplam += oran;
      cift += 1;
      if (oran > enYuksek[1]) enYuksek = [`${a}+${b}`, oran];
    }
  }
  console.log(`  cift ${cift}, ortalama birlikte yayin orani ${(toplam / cift).toFixed(2)}`
    + `, en yuksek ${enYuksek[0]} ${enYuksek[1].toFixed(2)}`);
  if (hedef !== undefined) {
    const kume = new Set([...kapanis(hedef)].map((y) => birimOf.get(y)));
    const dosyaSayisi = dosyalar.filter((f) => kume.has(birimi(f))).length;
    console.log(`  ${hedef} degisince: ${kume.size} / ${birimler.length} birim,`
      + ` ${dosyaSayisi} / ${dosyalar.length} dosya  (${[...kume].sort().join(", ")})`);
  }
}
node yayin-olc.mjs bilesenler
bilesenler/ yetenek bolunmesi (5 birim)
  bolge    dosya 2  yeniden yayinlatan dosya 2 / 9
  masa     dosya 1  yeniden yayinlatan dosya 9 / 9
  ortak    dosya 1  yeniden yayinlatan dosya 1 / 9
  rota     dosya 2  yeniden yayinlatan dosya 4 / 9
  ucret    dosya 3  yeniden yayinlatan dosya 6 / 9
  cift 10, ortalama birlikte yayin orani 0.27, en yuksek masa+ucret 0.67
bilesenler/ rol bolunmesi (3 birim)
  arayuz   dosya 5  yeniden yayinlatan dosya 9 / 9
  kural    dosya 1  yeniden yayinlatan dosya 3 / 9
  veri     dosya 3  yeniden yayinlatan dosya 4 / 9
  cift 3, ortalama birlikte yayin orani 0.39, en yuksek arayuz+veri 0.44

Ortalama birlikte yayın oranı yetenek bölünmesinde 0.27, rol bölünmesinde 0.39. Daha keskin olan sayı tablodadır: iki bölünmede de bir birim dokuz dosyanın dokuzunda yeniden yayınlanıyor, ama yetenek bölünmesinde o birim 1 dosyalık masa, rol bölünmesinde 5 dosyalık arayuz. Her değişiklikte yeniden yayınlanan parça beş katına çıkıyor. bolge bileşeni ise dokuz dosyanın yalnız ikisinde yeniden yayınlanıyor; ortak kapanış ilkesinin karşılığı bu satırdır.

Yeni Bir Yetenek

Üçüncü ölçü uzantı maliyetidir. Ücretlendirmeye yakıt ek ücreti ekleniyor: oran kendi dosyasına giriyor, kural onu uyguluyor.

cp -r bilesenler yakitli
cat > yakitli/ucret/yakit.mjs <<'EOF'
// yakitli/ucret/yakit.mjs — ucret bileseni ici: yakit ek ucreti orani
export const YAKIT_ORANI = 0.06;
export const yakitEkle = (k) => k * (1 + YAKIT_ORANI);
EOF
cat > yakitli/ucret/kural.mjs <<'EOF'
// yakitli/ucret/kural.mjs — ucret bileseni ici: ucret kurali, yakit ek ucretiyle
import { ASGARI, KADEMELER, yuvarla } from "./tarife.mjs";
import { yakitEkle } from "./yakit.mjs";

export const ucret = (gonderi, katsayi) => {
  const kademe = KADEMELER.find((k) => gonderi.agirlik <= k.ustAgirlik);
  if (kademe === undefined) throw new RangeError("agirlik kademesi yok");
  return Math.max(yuvarla(yakitEkle(kademe.ucret * katsayi)), ASGARI);
};
EOF
diff -rq bilesenler yakitli
node yuzey-olc.mjs yakitli | head -8
node yayin-olc.mjs yakitli ucret/kural.mjs | grep degisince
node -e "import('./bilesenler/masa/dis.mjs').then(m => console.log('yakitsiz ' + JSON.stringify(m.istek({ agirlik: 3, postaKodu: '06800' }))))"
node -e "import('./yakitli/masa/dis.mjs').then(m => console.log('yakitli  ' + JSON.stringify(m.istek({ agirlik: 3, postaKodu: '06800' }))))"
Files bilesenler/ucret/kural.mjs and yakitli/ucret/kural.mjs differ
Only in yakitli/ucret: yakit.mjs
yakitli/  dosya = 10, toplam ihrac = 18
  yetenek bolunmesi (5 birim)
    bolge    ihrac  5  disa acilan  2  kapsanan  3
    masa     ihrac  1  disa acilan  0  kapsanan  1
    ortak    ihrac  2  disa acilan  2  kapsanan  0
    rota     ihrac  3  disa acilan  1  kapsanan  2
    ucret    ihrac  7  disa acilan  1  kapsanan  6
    toplam   ihrac 18  disa acilan  6  kapsanan 12
  ucret/kural.mjs degisince: 2 / 5 birim, 5 / 10 dosya  (masa, ucret)
  ucret/kural.mjs degisince: 2 / 3 birim, 6 / 10 dosya  (arayuz, kural)
yakitsiz {"tutar":11450,"gosterim":"114.50 TL","tasiyici":"kuzey","aktarma":1}
yakitli  {"tutar":12150,"gosterim":"121.50 TL","tasiyici":"kuzey","aktarma":1}

Değişiklik bir dosyayı düzenledi ve bir dosya ekledi. ucret bileşeninin ihraç ettiği ad 5’ten 7’ye çıktı ama dışa açılan adı 1’de kaldı: yeni iki ad kapsanan tarafa yazıldı, yüzey değişmedi ve hiçbir tüketici düzenlenmedi. Yeniden yayınlanan parça yetenek bölünmesinde 5 birimin 2’si ve 10 dosyanın 5’i, rol bölünmesinde 3 birimin 2’si ve 10 dosyanın 6’sı. Teslimat operasyonu tarafına dokunulmadı: iki koşumda tasiyici ve aktarma alanları aynı kaldı, yalnız tutar 11450’den 12150’ye çıktı.

Bedelin sayısı da burada. Beş bileşen beş yüzey dosyası ve beş sürüm demektir; tek ad ihraç eden masa için bile ayrı bir yayın birimi tutuluyor. Yüzeyi daraltmak kapsanan ad sayısını yükseltir, bileşen sayısını artırmak yayınlanacak birim sayısını yükseltir — sürdürülebilirlik ile dağıtımın basitliği arasındaki ödünleşim bu iki sayıdır.

Özet

  • Bileşen burada ayrı paketlenen, ayrı sürümlenen ve dışa açtığı adları bir yüzey dosyasında bildirilmiş bir dosya kümesidir; kapsanan ad sayısı o bileşenin serbestlik payıdır.
  • Aynı dokuz dosya yeteneğe göre bölündüğünde 16 addan 6’sı dışa açılıyor, role göre bölündüğünde 8’i; ucret beş adından dördünü, veri sekiz adından yalnız ikisini içinde tutuyor.
  • Yüzeyleri atlayan tek bir tüketici bileşeni dışa açılan adı 6’dan 10’a çıkardı ve bolge bileşeninin kapsanan adını 3’ten 1’e indirdi; ürettiği tutar yüzeyden geçen yolla aynıydı.
  • Ortalama birlikte yayın oranı yeteneğe göre 0.27, role göre 0.39; her değişiklikte yeniden yayınlanan birim yetenek bölünmesinde 1 dosyalık, rol bölünmesinde 5 dosyalık.
  • Yakıt ek ücreti 1 dosya düzenletti, 1 dosya eklettirdi, dışa açılan adı 6’da bıraktı ve yetenek bölünmesinde 5 birimin 2’sini, rol bölünmesinde 3 birimin 2’sini yeniden yayınlattı.

Sonraki Adım

Bileşen sınırı yeniden kullanım birimini verdi: ucret bileşenine yeni bir yetenek eklendiğinde yüzey değişmedi ve dört bileşene dokunulmadı. Ama bu ölçülerin hepsi yayın zamanındadır. Bu birimlerden hangisi program koşarken takılıp çıkarılabilir? masa bileşeni iki bileşeni adıyla ithal ediyor, dolayısıyla üçüncü bir yeteneği eklemek için yine düzenlenmesi gerekir; bileşen sınırı bu soruyu yanıtlamıyor. Sonraki ders birimleri iki sınıfa böler: değişmeyen ve tek başına koşabilen bir çekirdek ile ona kayıtla takılan eklentiler. Ölçüler de değişir: çekirdeğin eklentiler hakkında bildiği ad sayısı, yeni bir eklentinin çekirdekte düzenlettiği satır sayısı ve eklentisiz çekirdeğin koşup koşmadığı.

İ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