Ders 09 / 19
Kansız Model Eleştirisi
Davranışsız veri sınıflarının alan modeli bağlamında ölçülmesi: beş kuralın kaç ayrı serviste yazıldığının ve beş kural için kaç dosya düzenlemesi gerektiğinin sayılması, aynı kuralı yazan servislerin 36 gönderide kaç kez ayrı sonuç verdiğinin sayılması ve alan uzmanının fiillerinden kaçının gönderiye sorulabildiğinin karşılaştırılması.
İçindekiler
Bu konudaki dersler kimliği, değeri, tutarlılık sınırını, davranışın yerini, yaratımı ve olayı tek tek ölçtü. Hepsinin ortak varsayımı, alan nesnesinin bir davranışı olduğuydu. Yaygın bir düzen bu varsayımı kaldırır: alan sınıfları yalnız alan taşır, bütün kurallar servislere yazılır ve sınıf bir veri biçiminden fazlası olmaz.
Alan taşıyıp kural taşımayan bu düzene anemik model (anemic model) denir; dersin başlığındaki “kansız model” aynı düzenin katalogdaki adıdır ve gövdede tek biçim olarak anemik model kullanılır. Düzenin nesne sorumluluğu açısından eleştirisi Programlama Paradigmaları kursundaki Nesne Sorumluluğu dersinde yapıldı ve burada tekrarlanmaz. Bu ders aynı düzeni alan modeli bağlamında ölçer: bir kural kaç ayrı serviste yazılıdır, beş kural kaç dosya düzenlemesi ister, aynı kuralı yazan servisler aynı gönderide aynı sonuca varır mı, uzmanın cümlesindeki fiillerin kaçı alan nesnesine sorulabilir.
Beş Kural ve Üç Servis
Ücretlendirme uzmanının bir cümlesi beş kural taşıyor: ücrete esas ağırlık gerçek ağırlıkla hacimden türetilen ağırlığın büyüğüdür, sözleşmesi olan müşteride sözleşme indirimi uygulanır, sözleşme ve hacim indirimlerinin toplamı indirim tavanını aşamaz, ücret asgari ücretin altına inmez, beyan değeri olan gönderiye sigorta primi eklenir.
Kitaplıkta bu kuralları kullanan üç servis var: fatura satırı, dönem sonu mutabakatı ve toplu fiyatlama listesi. Ölçüm verisi otuz altı gönderi kaydıdır; on ikisinde sözleşme numarası boş olduğu hâlde bir indirim oranı yazılıdır ve ikinci kuralın gözlenebilmesi buna bağlıdır.
mkdir -p anemik alan
// veri.mjs — ucretlendirme baglaminin tarife tablosu ve 36 gonderi kaydi export const TARIFE = { standart: { yakin: 1800, orta: 2400, uzak: 3200 }, agir: { yakin: 1400, orta: 1900, uzak: 2600 }, }; export const KAYITLAR = []; let i = 0; for (const [agirlikGram, hacimDesimetreKup] of [[700, 2], [2400, 9], [6000, 18], [18000, 60]]) for (const [sozlesmeNo, sozlesmeIndirimi] of [[null, 0.22], ["S-77", 0.22], ["S-12", 0.3]]) for (const hacimIndirimi of [0, 0.12, 0.2]) { i += 1; KAYITLAR.push({ gonderiNo: `G-6${100 + i}`, agirlikGram, hacimDesimetreKup, ucretBolgesi: ["yakin", "orta", "uzak"][i % 3], tarifeKodu: agirlikGram >= 10000 ? "agir" : "standart", beyanDegeriKurus: i % 3 === 0 ? 0 : 60000 * (1 + (i % 4)), sozlesmeNo, sozlesmeIndirimi, hacimIndirimi, }); }
Anemik Düzen
Birinci düzende gönderi kaydı olduğu gibi taşınır; hiçbir kural onunla birlikte durmaz.
// anemik/gonderi.mjs — yalniz alan tasir; hicbir kural burada degil export const gonderi = (k) => ({ ...k });
Fatura servisi beş kuralın beşini de kendi içinde yazar.
// anemik/fatura.mjs — bes kuralin besini de kendisi yazar export const faturaSatiri = (g, tarife) => { const esasGram = Math.max(g.agirlikGram, g.hacimDesimetreKup * 200); const sozlesme = g.sozlesmeNo === null ? 0 : g.sozlesmeIndirimi; const indirim = Math.min(0.4, sozlesme + g.hacimIndirimi); const ham = Math.round((esasGram / 1000) * tarife[g.tarifeKodu][g.ucretBolgesi] * (1 - indirim)); const ucret = Math.max(3990, ham); const prim = Math.round(g.beyanDegeriKurus * 0.002); return { esasGram, sozlesme, indirim, asgari: ucret > ham, prim, tutar: ucret + prim }; };
Mutabakat servisi aynı beş kuralı yazar, tavanı yalnız sözleşme indirimine uygular.
// anemik/mutabakat.mjs — ayni bes kural; tavan yalniz sozlesme indirimine uygulanmis export const mutabakatSatiri = (g, tarife) => { const esasGram = Math.max(g.agirlikGram, g.hacimDesimetreKup * 200); const sozlesme = g.sozlesmeNo === null ? 0 : g.sozlesmeIndirimi; const indirim = Math.min(0.4, sozlesme) + g.hacimIndirimi; const ham = Math.round((esasGram / 1000) * tarife[g.tarifeKodu][g.ucretBolgesi] * (1 - indirim)); const ucret = Math.max(3990, ham); const prim = Math.round(g.beyanDegeriKurus * 0.002); return { esasGram, sozlesme, indirim, asgari: ucret > ham, prim, tutar: ucret + prim }; };
Toplu fiyatlama servisi sigorta primini hesaplamaz ve indirim oranını kayıttan olduğu gibi alır; sözleşme koşulu bu dosyada yok.
// anemik/toplu-fiyatlama.mjs — ayni kurallar bir kez daha; sozlesme kosulu yazilmamis export const listeFiyati = (g, tarife) => { const esasGram = Math.max(g.agirlikGram, g.hacimDesimetreKup * 200); const sozlesme = g.sozlesmeIndirimi; const indirim = Math.min(0.4, sozlesme + g.hacimIndirimi); const ham = Math.round((esasGram / 1000) * tarife[g.tarifeKodu][g.ucretBolgesi] * (1 - indirim)); const ucret = Math.max(3990, ham); return { esasGram, sozlesme, indirim, asgari: ucret > ham, kilogramFiyati: Math.round(ucret / (esasGram / 1000)) }; };
Davranışı Taşıyan Model
İkinci düzende beş kural gönderinin kendisindedir; alanlar kapalıdır, dışa yalnız kural sonuçları açılır.
// alan/gonderi.mjs — bes kural gonderide; disa yalniz kural sonuclari acilir const HACIM_KATSAYISI = 200; const INDIRIM_TAVANI = 0.4; const ASGARI_UCRET_KURUS = 3990; const SIGORTA_ORANI = 0.002; export const gonderi = (k) => { const esasAgirlikGram = () => Math.max(k.agirlikGram, k.hacimDesimetreKup * HACIM_KATSAYISI); const sozlesmeIndirimOrani = () => (k.sozlesmeNo === null ? 0 : k.sozlesmeIndirimi); const uygulanacakIndirimOrani = () => Math.min(INDIRIM_TAVANI, sozlesmeIndirimOrani() + k.hacimIndirimi); const ham = (tarife) => Math.round((esasAgirlikGram() / 1000) * tarife[k.tarifeKodu][k.ucretBolgesi] * (1 - uygulanacakIndirimOrani())); const ucretKurus = (tarife) => Math.max(ASGARI_UCRET_KURUS, ham(tarife)); const sigortaPrimiKurus = () => Math.round(k.beyanDegeriKurus * SIGORTA_ORANI); return Object.freeze({ gonderiNo: k.gonderiNo, esasAgirlikGram, sozlesmeIndirimOrani, uygulanacakIndirimOrani, ucretKurus, sigortaPrimiKurus, asgariUcretUygulandiMi: (tarife) => ucretKurus(tarife) > ham(tarife), tutarKurus: (tarife) => ucretKurus(tarife) + sigortaPrimiKurus(), }); };
Aynı üç servis artık hesaplamaz, sorar.
// alan/fatura.mjs — kural yazmaz, gonderiye sorar export const faturaSatiri = (g, tarife) => ({ esasGram: g.esasAgirlikGram(), sozlesme: g.sozlesmeIndirimOrani(), indirim: g.uygulanacakIndirimOrani(), asgari: g.asgariUcretUygulandiMi(tarife), prim: g.sigortaPrimiKurus(), tutar: g.tutarKurus(tarife), });
// alan/mutabakat.mjs — tavani kendisi yorumlamaz, uygulanacak orani alir export const mutabakatSatiri = (g, tarife) => ({ esasGram: g.esasAgirlikGram(), sozlesme: g.sozlesmeIndirimOrani(), indirim: g.uygulanacakIndirimOrani(), asgari: g.asgariUcretUygulandiMi(tarife), prim: g.sigortaPrimiKurus(), tutar: g.tutarKurus(tarife), });
// alan/toplu-fiyatlama.mjs — kilogram fiyati kendi isi, kurallar gonderinin export const listeFiyati = (g, tarife) => ({ esasGram: g.esasAgirlikGram(), sozlesme: g.sozlesmeIndirimOrani(), indirim: g.uygulanacakIndirimOrani(), asgari: g.asgariUcretUygulandiMi(tarife), kilogramFiyati: Math.round(g.ucretKurus(tarife) / (g.esasAgirlikGram() / 1000)), });
Kural Kaç Dosyada Yazılı
Birinci ölçü her kuralı iki dizinde arar ve kaç dosyada göründüğünü sayar. Kalıplar kuralı hem sayı değeriyle hem adlandırılmış sabitle bulur; adı değişen bir sabit taramadan kaçmaz. Aynı betik uzmanın beş fiilinden kaçının gönderiye tek çağrıyla sorulabildiğini de sayar.
// kural-olc.mjs — kural kac dosyada yazili, bes kural kac duzenleme ister, fiil nereye sorulur import { readdirSync, readFileSync } from "node:fs"; import { KAYITLAR } from "./veri.mjs"; import { gonderi } from "./anemik/gonderi.mjs"; import { gonderi as alanGonderi } from "./alan/gonderi.mjs"; const KURAL = [ ["ucrete esas agirlik", /hacimDesimetreKup \* (?:200|HACIM_KATSAYISI)/], ["sozlesme indirimi", /sozlesmeNo === null \? 0/], ["indirim tavani", /Math\.min\((?:0\.4|INDIRIM_TAVANI)/], ["asgari ucret", /Math\.max\((?:3990|ASGARI_UCRET_KURUS)/], ["sigorta primi", /\* (?:0\.002|SIGORTA_ORANI)/], ]; for (const dizin of ["anemik", "alan"]) { const dosyalar = readdirSync(dizin).sort().map((d) => [d, readFileSync(`${dizin}/${d}`, "utf8")]); let toplam = 0; console.log(`${dizin} (${dosyalar.length} dosya)`); for (const [ad, kalip] of KURAL) { const yerler = dosyalar.filter(([, icerik]) => kalip.test(icerik)).map(([d]) => d); toplam += yerler.length; console.log(` ${ad.padEnd(20)} yazili dosya ${yerler.length} ${yerler.join(" ")}`); } console.log(` 5 kural icin toplam dosya duzenlemesi = ${toplam}`); } const FIIL = [ ["ucrete esas agirligi bulur", "esasAgirlikGram"], ["sozlesme indirimini uygular", "sozlesmeIndirimOrani"], ["indirim tavanini asmaz", "uygulanacakIndirimOrani"], ["asgari ucretin altina inmez", "ucretKurus"], ["sigorta primini ekler", "sigortaPrimiKurus"], ]; for (const [ad, nesne] of [["anemik", gonderi(KAYITLAR[0])], ["alan", alanGonderi(KAYITLAR[0])]]) { const sorulabilen = FIIL.filter(([, yontem]) => typeof nesne[yontem] === "function"); console.log(`${ad}: uzmanin ${FIIL.length} fiilinden ${sorulabilen.length}'i gonderiye sorulabiliyor`); }
node kural-olc.mjs
anemik (4 dosya) ucrete esas agirlik yazili dosya 3 fatura.mjs mutabakat.mjs toplu-fiyatlama.mjs sozlesme indirimi yazili dosya 2 fatura.mjs mutabakat.mjs indirim tavani yazili dosya 3 fatura.mjs mutabakat.mjs toplu-fiyatlama.mjs asgari ucret yazili dosya 3 fatura.mjs mutabakat.mjs toplu-fiyatlama.mjs sigorta primi yazili dosya 2 fatura.mjs mutabakat.mjs 5 kural icin toplam dosya duzenlemesi = 13 alan (4 dosya) ucrete esas agirlik yazili dosya 1 gonderi.mjs sozlesme indirimi yazili dosya 1 gonderi.mjs indirim tavani yazili dosya 1 gonderi.mjs asgari ucret yazili dosya 1 gonderi.mjs sigorta primi yazili dosya 1 gonderi.mjs 5 kural icin toplam dosya duzenlemesi = 5 anemik: uzmanin 5 fiilinden 0'i gonderiye sorulabiliyor alan: uzmanin 5 fiilinden 5'i gonderiye sorulabiliyor
Anemik düzende beş kural on üç dosya düzenlemesiyle yazılmış, alan modelinde beşle. Bir kuralın metni değiştiğinde anemik düzende iki ya da üç dosya, alan modelinde bir dosya düzenlenir.
Sözleşme indirimi satırındaki iki, bir kazanç değildir. Toplu fiyatlama servisi de indirim uyguluyor, dolayısıyla sözleşme koşulunu uygulamak zorundadır; gereken sayı on üç değil on dörttür. Kuralın kaç yerde yazıldığını sayan bir ölçü, bir yerde hiç yazılmamış olmasını yinelemenin azlığından ayırt edemez: eksik yer bir yokluk olarak değil, düşük bir sayı olarak görünür. Bunu ancak sonucu karşılaştıran bir ölçü ayırt eder.
Son iki satır aynı soruyu dil tarafından sorar. Uzmanın beş fiili anemik gönderiye sorulamıyor, çünkü o nesnenin hiçbir yöntemi yoktur. Fiillerin karşılığı servislerin içindeki üç satırdır ve o satırların alan sözlüğünde bir adı yoktur.
Servisler Ayrıştığında
İkinci ölçü sonuca bakar. Her kuralın bir gözlenebilir çıktısı vardır: esas ağırlık, uygulanan sözleşme oranı, uygulanacak indirim oranı, asgari ücretin devreye girip girmediği ve sigorta primi. Her gönderi için o kuralı yazan servislerin ürettiği değerler karşılaştırılır ve birden çok değer çıkan gönderiler sayılır.
// ayrisma-olc.mjs — ayni kurali yazan servisler 36 gonderide kac kez ayri sonuc veriyor import { TARIFE, KAYITLAR } from "./veri.mjs"; import { gonderi } from "./anemik/gonderi.mjs"; import { faturaSatiri as aFatura } from "./anemik/fatura.mjs"; import { mutabakatSatiri as aMutabakat } from "./anemik/mutabakat.mjs"; import { listeFiyati as aListe } from "./anemik/toplu-fiyatlama.mjs"; import { gonderi as alanGonderi } from "./alan/gonderi.mjs"; import { faturaSatiri as sFatura } from "./alan/fatura.mjs"; import { mutabakatSatiri as sMutabakat } from "./alan/mutabakat.mjs"; import { listeFiyati as sListe } from "./alan/toplu-fiyatlama.mjs"; const KURAL = [["ucrete esas agirlik", "esasGram"], ["sozlesme indirimi", "sozlesme"], ["indirim tavani", "indirim"], ["asgari ucret", "asgari"], ["sigorta primi", "prim"]]; const N = KAYITLAR.length; const olc = (ad, servisler, kur) => { const sonuc = KAYITLAR.map((k) => servisler.map((f) => f(kur(k), TARIFE))); const ayrisanGonderi = new Set(); console.log(ad); for (const [kuralAd, alan] of KURAL) { const yazan = sonuc[0].filter((r) => alan in r).length; let ayrisan = 0; KAYITLAR.forEach((k, i) => { const d = new Set(sonuc[i].filter((r) => alan in r).map((r) => String(r[alan]))); if (d.size > 1) { ayrisan += 1; ayrisanGonderi.add(k.gonderiNo); } }); console.log(` ${kuralAd.padEnd(20)} yazan servis ${yazan} ayrisan gonderi ${ayrisan}/${N}`); } console.log(` en az bir kuralda ayrisan gonderi = ${ayrisanGonderi.size}/${N}`); return sonuc; }; const A = olc("anemik", [aFatura, aMutabakat, aListe], (k) => gonderi(k)); olc("alan", [sFatura, sMutabakat, sListe], (k) => alanGonderi(k)); const ayri = KAYITLAR.map((_, i) => i).filter((i) => A[i][0].indirim !== A[i][1].indirim); console.log(`fatura ile mutabakatin indirim orani ayrilan gonderi = ${ayri.length}/${N}, ilk ucu:`); for (const i of ayri.slice(0, 3)) { console.log(` ${KAYITLAR[i].gonderiNo} fatura ${A[i][0].indirim.toFixed(2)} / ${A[i][0].tutar}` + ` mutabakat ${A[i][1].indirim.toFixed(2)} / ${A[i][1].tutar}`); } const gizli = ayri.filter((i) => A[i][0].tutar === A[i][1].tutar).length; console.log(`indirim orani ayrildigi halde tutari ayni cikan gonderi = ${gizli}/${ayri.length}`); let fark = 0; for (const k of KAYITLAR) { if (aFatura(gonderi(k), TARIFE).tutar !== sFatura(alanGonderi(k), TARIFE).tutar) fark += 1; } console.log(`fatura tutari iki modelde ayrilan gonderi = ${fark}/${N}`);
node ayrisma-olc.mjs
anemik ucrete esas agirlik yazan servis 3 ayrisan gonderi 0/36 sozlesme indirimi yazan servis 3 ayrisan gonderi 12/36 indirim tavani yazan servis 3 ayrisan gonderi 24/36 asgari ucret yazan servis 3 ayrisan gonderi 0/36 sigorta primi yazan servis 2 ayrisan gonderi 0/36 en az bir kuralda ayrisan gonderi = 24/36 alan ucrete esas agirlik yazan servis 3 ayrisan gonderi 0/36 sozlesme indirimi yazan servis 3 ayrisan gonderi 0/36 indirim tavani yazan servis 3 ayrisan gonderi 0/36 asgari ucret yazan servis 3 ayrisan gonderi 0/36 sigorta primi yazan servis 2 ayrisan gonderi 0/36 en az bir kuralda ayrisan gonderi = 0/36 fatura ile mutabakatin indirim orani ayrilan gonderi = 12/36, ilk ucu: G-6106 fatura 0.40 / 3990 mutabakat 0.42 / 3990 G-6108 fatura 0.40 / 4110 mutabakat 0.42 / 4110 G-6109 fatura 0.40 / 3990 mutabakat 0.50 / 3990 indirim orani ayrildigi halde tutari ayni cikan gonderi = 5/12 fatura tutari iki modelde ayrilan gonderi = 0/36
Otuz altı gönderinin yirmi dördünde üç servis en az bir kuralda ayrı sonuç veriyor. İki kaynak var. Sözleşme indirimi satırındaki on iki, taramada eksik görünen yerin bedelidir: sözleşme numarası boş olan on iki gönderide toplu fiyatlama yirmi iki puanlık sözleşme indirimini uygularken öteki iki servis onu sıfır sayıyor. İndirim tavanı satırındaki yirmi dört bu on ikiyi de içeriyor — sözleşme oranı ayrıldığında ona bağlı olan tavan da ayrılıyor — kalan on iki gönderi mutabakatın tavanı yalnız sözleşme indirimine uygulamasından geliyor.
Örnek satırlar ayrışmanın niçin fark edilmediğini gösteriyor. G-6106 gönderisinde fatura
yüzde kırk indirim uyguluyor, mutabakat yüzde kırk iki, iki tutar da 3990 çıkıyor. Ayrılan on
iki gönderinin beşinde tutar aynı, çünkü asgari ücret kuralı iki hesabı aynı tabana oturtuyor.
Ayrışma kalkmadı, görünmez oldu: fark yalnız yedi gönderide dışa vuruyor.
Alan modelinde beş kuralın beşinde ayrışan gönderi sayısı sıfır; üç servis aynı yönteme soruyor ve ayrışacak ikinci bir yazım yoktur. Son satır ölçünün ne olmadığını söylüyor: doğru yazılmış anemik servis ile alan modeli aynı fatura tutarını veriyor. Kazanç hesabın değişmesi değildir.
Kuralı Ortak Bir Modüle Çıkarmak
Anemik düzenin bilinen düzeltmesi kuralları ortak bir yardımcı modüle çıkarmaktır. Bu, birinci ölçüyü alan modelininkine eşitler: her kural bir dosyada yazılı olur. İkinci ölçüde ise yardımcı modül tek başına bir şey vaat etmez. Yardımcı işlev alanları parametre olarak alır, yani hangi değerin verileceğine çağıran servis karar verir; toplu fiyatlamanın ayrışması tam bu karardan doğdu — kuralın metnini yanlış yazmadı, kurala yanlış oranı verdi. Ölçünün ayırt ettiği şey buradan çıkar: kuralın kaç dosyada yazıldığı değil, girdisine kimin karar verdiği. Alan nesnesine sorulduğunda girdi nesnenin kendi alanıdır ve servisin verecek bir şeyi yoktur.
Anemik Sınıfın Yerinde Olduğu Yer
Alan taşıyan davranışsız kaydın yerinde olduğu durum Nesne Sorumluluğu dersinde ölçülmüştü: bir sınırı geçen kayıtların işi biçimi korumaktır, kural yorumlamak değil. Bu ders o sınırın üzerine alan modeline özgü iki ayrım ekler.
Birincisi kuralın kime ait olduğudur. Beş kuralın beşi de tek bir gönderinin alanlarıyla yanıtlanabiliyordu; bu yüzden gönderiye taşındılar. İki toplamayı birlikte gerektiren bir kural — bir sözleşmenin dönem hacminin indirimi etkilemesi gibi — gönderiye taşınmaz, Alan Servisleri dersinde kurulan alan servisine gider. Anemik modelin karşıtı “bütün kurallar nesnenin içinde” değildir.
İkincisi bedeldir: gönderi beş yöntem kazandı ve tarifeyi parametre olarak almak zorunda kaldı. Ölçüt kural sayısındadır — yorumlanmayan alanı taşıyan kayıt davranışsız kalabilir, yorumlanan alanın kuralı onunla birlikte durmalıdır.
Özet
- Anemik model, alan taşıyıp kural taşımayan düzendir; bu ders onu alan modeli bağlamında ölçtü, nesne sorumluluğu tartışmasına girmedi.
- Beş kural anemik düzende 13 dosya düzenlemesiyle yazılıydı, alan modelinde 5’le; bir kuralın değişmesi anemik düzende 2–3 dosyaya, alan modelinde 1 dosyaya dokunuyor.
- Sözleşme indirimi satırındaki 2 kazanç değil eksiklikti: gereken 14’tü, taramada eksik yer düşük bir sayı olarak göründü.
- Aynı kuralı yazan 3 servis 36 gönderinin 24’ünde en az bir kuralda ayrı sonuç verdi; alan modelinde beş kuralın beşinde ayrışan gönderi 0 çıktı.
- Fatura ile mutabakatın indirim oranı 12 gönderide ayrıldı, bunların 5’inde asgari ücret kuralı tutarları eşitlediği için ayrışma dışa vurmadı.
- Uzmanın 5 fiilinden 0’ı anemik gönderiye, 5’i alan gönderisine tek çağrıyla sorulabiliyordu; iki modelin fatura tutarı 36 gönderide 0 kez ayrıldı.
Sonraki Adım
Bu konu boyunca tek bir modelin iç düzeni kuruldu: kimlik hangi nesnede, hangi değişmez hangi
sınırın içinde, davranış nereye ait, nesne hangi yoldan yaratılır, olayın adı nereden gelir.
Her karar modelin alanla hizasıyla ölçüldü. Sorulmayan tek soru şudur: bu, hangi alanın
modeli? Gönderi ücretlendirme ve yönlendirme kitaplığında gonderi sözcüğü iki takımın
ağzında iki farklı nesneyi adlandırıyor — ücretlendirme için ağırlık, hacim, bölge ve beyan
değeri taşıyan fiyatlanabilir bir birim, teslimat operasyonu için rotası ve durum zaman
damgaları olan fiziksel bir paket. Sözcük aynı olduğu için tek bir sınıf yazılır ve o sınıf
iki işi birden yapmaya zorlanır. Sonraki konu bu zorlamanın bedelini iki sayıyla ölçer:
sınıfın taşıdığı alan sayısı ve hiçbir kullanımda birlikte geçmeyen alan çiftlerinin sayısı.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.