İçeriğe geç
academia.sh

Ders 14 / 14

Paradigma Karışımı

Aynı sistemde iki modelin birlikte kullanımı: taşıyıcı seçeneklerinin nesne ağırlıklı ve fonksiyon ağırlıklı iki modeli, yeni bir işlem ile yeni bir türün her iki modelde kaç dosyaya dokunduğunun sayılması ve seçimin ölçüte bağlanması.

İçindekiler

Önceki konu bir birim tanıdı: durumu ve onu koruyan davranışı bir arada tutan nesne. Bu konu başka bir birim kurdu: girdiden çıktıya eşleme yapan, durumu dışarıda bırakan fonksiyon. İki model aynı alanı — gönderi ücretlendirmeyi — farklı yerlerden böler.

Bu ders ikisini yan yana koyar. Aynı problem iki kez modellenir, sonra iki ayrı değişiklik istenir ve her birinin iki modelde kaç dosyaya dokunduğu sayılır. Sonuç bir üstünlük ilanı değildir; hangi ölçüte göre hangisinin seçildiğidir.

Aynı Problemin İki Modeli

Problem şudur: üç taşıyıcı seçeneği vardır — hızlı, ekonomik ve teslim noktası — ve her biri üzerinde iki işlem tanımlıdır: ücreti hesaplamak ve teslim süresini tahmin etmek.

Nesne ağırlıklı model, taşıyıcıyı birim alır. Her tür kendi dosyasında durur ve her ikisini de kendisi bilir.

mkdir -p nesne
cat > gonderiler.mjs <<'EOF'
export const GONDERILER = [
  { kod: "TR-4471", temelUcret: 4990, mesafeKm: 450, tasiyici: "hizli" },
  { kod: "TR-4472", temelUcret: 6567, mesafeKm: 1200, tasiyici: "ekonomik" },
  { kod: "TR-4473", temelUcret: 14715, mesafeKm: 320, tasiyici: "nokta" },
];
EOF
cat > nesne/hizli.mjs <<'EOF'
export class Hizli {
  ucret(g) { return g.temelUcret + 2500; }
  sure(g) { return Math.ceil(g.mesafeKm / 600) + 1; }
}
EOF
cat > nesne/ekonomik.mjs <<'EOF'
export class Ekonomik {
  ucret(g) { return g.temelUcret; }
  sure(g) { return Math.ceil(g.mesafeKm / 350) + 2; }
}
EOF
cat > nesne/nokta.mjs <<'EOF'
export class Nokta {
  ucret(g) { return g.temelUcret - 750; }
  sure(g) { return Math.ceil(g.mesafeKm / 350) + 3; }
}
EOF
cat > nesne/kayit.mjs <<'EOF'
import { Hizli } from "./hizli.mjs";
import { Ekonomik } from "./ekonomik.mjs";
import { Nokta } from "./nokta.mjs";

export const TASIYICILAR = {
  hizli: new Hizli(), ekonomik: new Ekonomik(), nokta: new Nokta(),
};
EOF
cat > nesne/kullan.mjs <<'EOF'
import { GONDERILER } from "../gonderiler.mjs";
import { TASIYICILAR } from "./kayit.mjs";

for (const g of GONDERILER) {
  const t = TASIYICILAR[g.tasiyici];
  console.log(`${g.kod} ${t.ucret(g)} kurus ${t.sure(g)} gun`);
}
EOF
node nesne/kullan.mjs
TR-4471 7490 kurus 2 gun
TR-4472 6567 kurus 6 gun
TR-4473 13965 kurus 4 gun

Fonksiyon ağırlıklı model işlemi birim alır. Taşıyıcı yalnızca bir addır; her işlem kendi dosyasında durur ve bütün türleri kendisi bilir.

mkdir -p fonk
cat > fonk/veri.mjs <<'EOF'
export const TASIYICILAR = ["hizli", "ekonomik", "nokta"];
EOF
cat > fonk/ucret.mjs <<'EOF'
export function ucret(g) {
  switch (g.tasiyici) {
    case "hizli": return g.temelUcret + 2500;
    case "ekonomik": return g.temelUcret;
    case "nokta": return g.temelUcret - 750;
    default: throw new Error(`bilinmeyen tasiyici: ${g.tasiyici}`);
  }
}
EOF
cat > fonk/sure.mjs <<'EOF'
export function sure(g) {
  switch (g.tasiyici) {
    case "hizli": return Math.ceil(g.mesafeKm / 600) + 1;
    case "ekonomik": return Math.ceil(g.mesafeKm / 350) + 2;
    case "nokta": return Math.ceil(g.mesafeKm / 350) + 3;
    default: throw new Error(`bilinmeyen tasiyici: ${g.tasiyici}`);
  }
}
EOF
cat > fonk/kullan.mjs <<'EOF'
import { GONDERILER } from "../gonderiler.mjs";
import { TASIYICILAR } from "./veri.mjs";
import { ucret } from "./ucret.mjs";
import { sure } from "./sure.mjs";

for (const g of GONDERILER) {
  if (TASIYICILAR.includes(g.tasiyici) === false) throw new Error(g.tasiyici);
  console.log(`${g.kod} ${ucret(g)} kurus ${sure(g)} gun`);
}
EOF
node fonk/kullan.mjs
TR-4471 7490 kurus 2 gun
TR-4472 6567 kurus 6 gun
TR-4473 13965 kurus 4 gun

İki model aynı sayıları üretir. Fark davranışta değil, kodun hangi eksende bölündüğündedir. Nesne modeli türe göre böler: bir dosya bir taşıyıcının her şeyini bilir. Fonksiyon modeli işleme göre böler: bir dosya bir işlemin her tür için nasıl yapıldığını bilir. Aynı bilgi kümesi, iki farklı yönde dilimlenmiştir.

Yeni Bir İşlem Eklendiğinde

Birinci değişiklik yeni bir işlemdir: her gönderi için tahmini karbon salımı, gram cinsinden, taşıyıcıya bağlı bir katsayıyla hesaplanacak. Ölçüm çağrı yerlerini dışarıda bırakır; yeni bir işlemin çağrılması her iki modelde de yeni bir çağrı yeri gerektirir.

cp -r nesne nesne-b && cp -r fonk fonk-b
# Nesne modeli: yeni islem her tur dosyasina ayri ayri eklenir.
cat > nesne-b/hizli.mjs <<'EOF'
export class Hizli {
  ucret(g) { return g.temelUcret + 2500; }
  sure(g) { return Math.ceil(g.mesafeKm / 600) + 1; }
  karbon(g) { return g.mesafeKm * 210; }
}
EOF
cat > nesne-b/ekonomik.mjs <<'EOF'
export class Ekonomik {
  ucret(g) { return g.temelUcret; }
  sure(g) { return Math.ceil(g.mesafeKm / 350) + 2; }
  karbon(g) { return g.mesafeKm * 95; }
}
EOF
cat > nesne-b/nokta.mjs <<'EOF'
export class Nokta {
  ucret(g) { return g.temelUcret - 750; }
  sure(g) { return Math.ceil(g.mesafeKm / 350) + 3; }
  karbon(g) { return g.mesafeKm * 60; }
}
EOF
# Fonksiyon modeli: yeni islem tek bir yeni dosyadir.
cat > fonk-b/karbon.mjs <<'EOF'
export function karbon(g) {
  switch (g.tasiyici) {
    case "hizli": return g.mesafeKm * 210;
    case "ekonomik": return g.mesafeKm * 95;
    case "nokta": return g.mesafeKm * 60;
    default: throw new Error(`bilinmeyen tasiyici: ${g.tasiyici}`);
  }
}
EOF
cat > karbon-kullan.mjs <<'EOF'
import { GONDERILER } from "./gonderiler.mjs";
import { TASIYICILAR } from "./nesne-b/kayit.mjs";
import { karbon } from "./fonk-b/karbon.mjs";

for (const g of GONDERILER) {
  console.log(`${g.kod} nesne ${TASIYICILAR[g.tasiyici].karbon(g)} fonk ${karbon(g)}`);
}
EOF
node karbon-kullan.mjs
for m in nesne fonk; do
  rapor=$(diff -rq -x 'kullan*' "$m" "$m-b")
  printf '%-6s yeni dosya: %d  degisen dosya: %d\n' "$m" \
    "$(printf '%s\n' "$rapor" | grep -c '^Only in')" \
    "$(printf '%s\n' "$rapor" | grep -c '^Files')"
done
TR-4471 nesne 94500 fonk 94500
TR-4472 nesne 114000 fonk 114000
TR-4473 nesne 19200 fonk 19200
nesne  yeni dosya: 0  degisen dosya: 3
fonk   yeni dosya: 1  degisen dosya: 0

İki model de doğru sonucu verir, ama maliyetleri eşit değildir. Fonksiyon modelinde yeni işlem tek bir yeni dosyadır; var olan hiçbir dosya açılmamıştır. Nesne modelinde üç tür dosyasının üçü de değişmiştir — yeni bir davranış, o davranışı taşıyacak her sınıfa ayrı ayrı eklenmek zorundadır.

Yeni Bir Taşıyıcı Türü Eklendiğinde

İkinci değişiklik ters yöndedir: dördüncü bir taşıyıcı, gece kuryesi, eklenecek. Ücreti sabit bir ek taşır, teslim süresi taşıdığı mesafeden bağımsız olarak bir gündür.

cp -r nesne nesne-a && cp -r fonk fonk-a
cat > gonderiler.mjs <<'EOF'
export const GONDERILER = [
  { kod: "TR-4471", temelUcret: 4990, mesafeKm: 450, tasiyici: "hizli" },
  { kod: "TR-4472", temelUcret: 6567, mesafeKm: 1200, tasiyici: "ekonomik" },
  { kod: "TR-4473", temelUcret: 14715, mesafeKm: 320, tasiyici: "nokta" },
  { kod: "TR-4474", temelUcret: 5739, mesafeKm: 280, tasiyici: "gece" },
];
EOF
# Nesne modeli: yeni tur bir yeni dosya, kayit dosyasi guncellenir.
cat > nesne-a/gece.mjs <<'EOF'
export class Gece {
  ucret(g) { return g.temelUcret + 4200; }
  sure(g) { return 1; }
}
EOF
cat > nesne-a/kayit.mjs <<'EOF'
import { Hizli } from "./hizli.mjs";
import { Ekonomik } from "./ekonomik.mjs";
import { Nokta } from "./nokta.mjs";
import { Gece } from "./gece.mjs";

export const TASIYICILAR = {
  hizli: new Hizli(), ekonomik: new Ekonomik(), nokta: new Nokta(), gece: new Gece(),
};
EOF
# Fonksiyon modeli: her islem dosyasina bir dal, veri dosyasina bir ad eklenir.
cat > fonk-a/veri.mjs <<'EOF'
export const TASIYICILAR = ["hizli", "ekonomik", "nokta", "gece"];
EOF
cat > fonk-a/ucret.mjs <<'EOF'
export function ucret(g) {
  switch (g.tasiyici) {
    case "hizli": return g.temelUcret + 2500;
    case "ekonomik": return g.temelUcret;
    case "nokta": return g.temelUcret - 750;
    case "gece": return g.temelUcret + 4200;
    default: throw new Error(`bilinmeyen tasiyici: ${g.tasiyici}`);
  }
}
EOF
cat > fonk-a/sure.mjs <<'EOF'
export function sure(g) {
  switch (g.tasiyici) {
    case "hizli": return Math.ceil(g.mesafeKm / 600) + 1;
    case "ekonomik": return Math.ceil(g.mesafeKm / 350) + 2;
    case "nokta": return Math.ceil(g.mesafeKm / 350) + 3;
    case "gece": return 1;
    default: throw new Error(`bilinmeyen tasiyici: ${g.tasiyici}`);
  }
}
EOF
node nesne-a/kullan.mjs | tail -n 1
node fonk-a/kullan.mjs | tail -n 1
for m in nesne fonk; do
  rapor=$(diff -rq -x 'kullan*' "$m" "$m-a")
  printf '%-6s yeni dosya: %d  degisen dosya: %d\n' "$m" \
    "$(printf '%s\n' "$rapor" | grep -c '^Only in')" \
    "$(printf '%s\n' "$rapor" | grep -c '^Files')"
done
TR-4474 9939 kurus 1 gun
TR-4474 9939 kurus 1 gun
nesne  yeni dosya: 1  degisen dosya: 1
fonk   yeni dosya: 0  degisen dosya: 3

Sayılar tersine dönmüştür. Nesne modelinde yeni tür bir yeni dosyadır; var olan üç tür dosyasının hiçbiri açılmamış, yalnızca kayıt dosyası güncellenmiştir. Fonksiyon modelinde üç dosyanın üçü de değişmiştir — her işlem, yeni türü tanımak için kendi dallanmasına bir dal eklemek zorundadır.

İki ölçüm birlikte okunduğunda tablo tamamlanır. Nesne modeli yeni türe kapalıdır ama yeni işleme açık değildir; fonksiyon modeli yeni işleme kapalıdır ama yeni türe açık değildir. Bir modelin kapalı olduğu eksen, diğerinin açık olduğu eksendir. Bu simetriye ifade problemi (expression problem) denir ve iki paradigmanın ödünleşimini tek cümlede toplar.

Seçimin Ölçütü ve Karışım

Ölçüt bu yüzden bir üstünlük yargısı değil, bir tahmindir: sistemin hangi ekseninde büyüme beklenmektedir. Taşıyıcı sayısı sabit, üzerinde yapılan işlem sayısı artan bir alanda — ücret, süre, karbon, sigorta, iade — fonksiyon modeli her yeni işlemi tek dosyayla karşılar. Taşıyıcı sayısı sürekli artan, işlem kümesi oturmuş bir alanda nesne modeli her yeni türü tek dosyayla karşılar.

İki eksenin de büyüdüğü durumda seçim ikisinden birini almak değil, sınırı çizmektir. Bu kursun ücretlendirme kitaplığı bu karışımın örneğidir. Ücret hesabının kendisi — kademe seçimi, bölge çarpanı, indirimler, taban — saf fonksiyonlardan kurulmuş bir boru hattıdır ve bir sonuç döndürür; girdi/çıktı ile zaman kabukta durur. Taşıyıcı seçenekleri ise nesnedir, çünkü orada büyüyen eksen tür eksenidir. İki model aynı sistemde, birbirinin yerine değil, farklı eksenlerde çalışır.

Karışımın tek kuralı sınırın açık olmasıdır. Nesnelerin yaşadığı yerde durum değişebilir; fonksiyonların yaşadığı yerde veri değiştirilemezdir. Bir değerin bu sınırı hangi yönde geçtiği belirsizse, iki modelin de güvencesi kaybolur.

Özet

  • Aynı problem iki eksende bölünebilir: nesne modeli türe göre, fonksiyon modeli işleme göre. İki model de aynı sayıları üretir.
  • Yeni bir işlem eklendiğinde fonksiyon modelinde 1 yeni dosya açılır ve hiçbir dosya değişmez; nesne modelinde üç tür dosyasının üçü de değişir.
  • Yeni bir tür eklendiğinde nesne modelinde 1 yeni dosya ile 1 kayıt dosyası değişir; fonksiyon modelinde üç işlem dosyasının üçü de değişir.
  • Bir modelin kapalı olduğu eksen diğerinin açık olduğu eksendir; bu simetri ifade problemi olarak adlandırılır.
  • Seçim, sistemin hangi ekseninde büyüme beklendiğine bağlıdır; iki eksen de büyüyorsa doğru karar birini seçmek değil, iki modelin arasındaki sınırı çizmektir.

Kurs Kapanışı

Bu kurs bir programın hangi birimlere bölüneceği sorusunu iki ayrı yanıtla ele aldı.

İlk konu yapısal ve nesneye dayalı düzenlemeyi kurdu: denetim akışının disipline edilmesi, durumun kapsüllenerek değişmezlerin korunması, gerekli olanı gösterip gerisini gizleyen soyutlama, alt tip ilişkisi olarak kalıtım, üç ayrı çok biçimlilik biçimi, arayüz ile soyut sınıf arasındaki seçim, kırılgan üst sınıf karşısında bileşimin tercih edilmesi ve veri ile davranışın birlikte tutulması. İkinci konu fonksiyonel modeli kurdu: saf fonksiyonlar ve referans saydamlığı, değiştirilemez veriyle paylaşılan durumun ortadan kaldırılması, davranışın fonksiyon parametresiyle dışarı alınması, küçük adımlardan kurulan boru hatları, yan etkilerin saf çekirdek ile kirli kabuk arasında bölünmesi ve son olarak iki modelin aynı sistemde ölçüte bağlı seçimi.

İkisinin ortak bıraktığı bir varsayım vardır. Her iki paradigma da tek tek birimlerin nasıl kurulacağını anlattı — birim nesne mi olacak, fonksiyon mu; içinde ne duracak, dışında ne kalacak. Kurulan ölçütlerin hepsi birimin içine bakar: kaç karar noktası var, kaç satır dışarıya dokunuyor, kaç sahte bağımlılık gerekiyor. Birimler arasındaki ilişkinin ne zaman sağlıklı olduğu ise ölçülmedi. Oysa bir tasarımın iyi olup olmadığı çoğu zaman birimin içine değil, birimler arasındaki bağa bakılarak anlaşılır: bir birim kaç başkasını tanıyor, bir değişiklik kaç birime yayılıyor, bir birimin içindeki parçalar gerçekten aynı işe mi ait.

Sonraki kurs, Tasarım İlkeleri, bu soruyu ele alır. SOLID ilkeleri birimlerin sorumluluğunu ve genişletilme biçimini adlandırır; bağlaşım ve uyum ölçütleri birimler arasındaki bağı ve birim içindeki bütünlüğü sayılabilir hâle getirir; bağımlılık yönünün soyutlamalara çevrilmesi, hangi birimin hangisini tanıyacağını bir tercih olmaktan çıkarıp bir kurala bağlar. Bu kursta kurulan birimler, orada birbirine bağlanacak.

İ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