İçeriğe geç
academia.sh

Ders 02 / 30

Fabrika Yöntemi ve Soyut Fabrika

Nesne yaratımının bir yönteme ve bir aile üretecine çekilmesi: üretecin hangi tarifeyi kuracağına alt tipin karar verdiği fabrika yöntemi, birbirine uyması gereken tarife, rota ve etiket üçlüsünü birlikte üreten soyut fabrika, çağrı yerinde eşleştirme yapılan sürümde sessizce kabul edilen yanlış birleşimlerin sayılması ve yeni bir taşıyıcı ailesi eklendiğinde düzenlenen dosya sayısının karşılaştırılması.

İçindekiler

Kayıt çözümü taşıyıcı adlarını tek bir yere topladı, ama kayda yazılan şeyin nasıl kurulduğu sorusuna hiç dokunmadı. Bir taşıyıcının tek bir ücret işlevi olduğu sürece kurulum tek bir nesne sabitidir. Taşıyıcının bir ücret tarifesi, bir rota planı ve bir etiket biçimi olduğunda kurulum üç parçanın birlikte hazırlanmasına dönüşür ve bu üç parça birbirine uymak zorundadır.

Bu ders iki kalıbı tanımlar. Fabrika yöntemi (factory method) tek bir nesnenin yaratımını bir yönteme çeker ve o yöntemi alt tipin karşılamasını bekler. Soyut fabrika (abstract factory) birbirine uyması gereken bir nesne ailesini tek çağrıda birlikte üretir. İkisinin problemi de yaratım kararının nereye yazıldığıdır; ölçüleri farklıdır.

Fabrika Yöntemi

Üreteç, teklif satırını hazırlayan tiptir. Hangi tarifenin kurulacağına kendisi karar vermez: kurulumu tarifeYap yöntemine bırakır ve o yöntemi alt tipler kendi gerçekleştirimiyle karşılar.

// yontem/uretec.mjs — ureteci; hangi tarifenin kurulacagini alt tip belirler
export class TeklifUreteci {
  tarifeYap() {
    throw new TypeError(`${this.constructor.name} tarifeYap yontemini karsilamadi`);
  }

  teklif(gonderi) {
    const t = this.tarifeYap();
    return `${t.ad}: ${t.ucret(gonderi)} kurus`;
  }
}
// yontem/kur.mjs — iki alt tip ayni yontemi kendi tarifesiyle karsilar
import { TeklifUreteci } from "./uretec.mjs";

class YurticiUreteci extends TeklifUreteci {
  tarifeYap() {
    return { ad: "yurtici", ucret: (g) => 3900 + Math.ceil(g.agirlik) * 900 };
  }
}

class YurtdisiUreteci extends TeklifUreteci {
  tarifeYap() {
    return { ad: "yurtdisi", ucret: (g) => 18400 + Math.ceil(g.agirlik) * 2600 };
  }
}

const G = { agirlik: 2.4, adres: "34100" };
for (const u of [new YurticiUreteci(), new YurtdisiUreteci()]) console.log(u.teklif(G));

class EksikUreteci extends TeklifUreteci {}
try {
  new EksikUreteci().teklif(G);
} catch (e) {
  console.log(`${e.constructor.name}: ${e.message}`);
}
node yontem/kur.mjs
yurtici: 6600 kurus
yurtdisi: 26200 kurus
TypeError: EksikUreteci tarifeYap yontemini karsilamadi

Üst tipin gövdesi hiçbir taşıyıcı adı taşımaz; taşıyıcı adları yalnız alt tiplerin bulunduğu dosyada geçer. İki dosyada adı içeren satırlar sayıldığında ayrım görünür.

for d in yontem/uretec.mjs yontem/kur.mjs; do
  echo "$d: $(grep -c 'yurtici\|yurtdisi' $d)"
done
yontem/uretec.mjs: 0
yontem/kur.mjs: 2

Bu ölçü yeni değil: bir önceki derste kayıt sürümü için sayılan “tür adının geçtiği yer” ölçüsünün aynısıdır ve aynı yönde iyileşir. Fabrika yöntemini kayıttan ayıran şey seçimin nasıl yapıldığıdır. Kayıtta seçim bir dizgiyle, fabrika yönteminde hangi alt tipin örneğinin elde tutulduğuyla yapılır. Üçüncü satır kalıbın kendi başına getirdiği tek yeni güvenceyi gösteriyor: yöntemi karşılamayan bir alt tip ilk çağrıda hata verir, sessiz kalmaz.

Parçaların Çağrı Yerinde Eşleştirilmesi

Aile problemi fabrika yönteminin çözemediği yerde başlar. Aşağıdaki sürümde üç parça role göre gruplanmıştır: bütün tarifeler bir dosyada, bütün rotalar bir dosyada, bütün etiketler bir dosyada. Eşleştirmeyi çağıran yapar.

// rol/tarife.mjs — role gore gruplanmis parcalar: iki ailenin tarifesi bir arada
export const yurticiTarife = { ad: "yurtici", ucret: (g) => 3900 + Math.ceil(g.agirlik) * 900 };
export const yurtdisiTarife = { ad: "yurtdisi", ucret: (g) => 18400 + Math.ceil(g.agirlik) * 2600 };
// rol/rota.mjs — iki ailenin rotasi bir arada; gumruk noktasi yalniz yurtdisinda
export const yurticiRota = { duraklar: () => ["34", "06"], gumrukKodu: () => null };
export const yurtdisiRota = { duraklar: () => ["34", "GUM", "DE"], gumrukKodu: () => "TR34DE" };
// rol/etiket.mjs — iki ailenin etiketi bir arada; her biri rotadan farkli sey ister
export const yurticiEtiket = (gonderi, rota) => {
  if (rota.gumrukKodu() !== null) throw new TypeError("yurtici etiketi gumruk noktasi tasimaz");
  return `TR ${rota.duraklar().join(">")} ${gonderi.agirlik}kg`;
};

export const yurtdisiEtiket = (gonderi, rota) => {
  const kod = rota.gumrukKodu();
  if (kod === null) throw new TypeError("yurtdisi etiketi gumruk kodu ister");
  return `INT ${kod} ${rota.duraklar().join(">")} ${gonderi.agirlik}kg`;
};
// rol/teklif.mjs — uc parcayi cagiran verir; uyumu denetleyen kimse yok
export const teklif = (tarife, rota, etiket, gonderi) =>
  `${tarife.ucret(gonderi)} kurus  ${etiket(gonderi, rota)}`;
// rol/kur.mjs — cagiran uc parcayi ada gore kendisi eslestirir
import { yurticiTarife, yurtdisiTarife } from "./tarife.mjs";
import { yurticiRota, yurtdisiRota } from "./rota.mjs";
import { yurticiEtiket, yurtdisiEtiket } from "./etiket.mjs";
import { teklif } from "./teklif.mjs";

export const GONDERI = { agirlik: 2.4, adres: "34100" };
console.log(teklif(yurticiTarife, yurticiRota, yurticiEtiket, GONDERI));
console.log(teklif(yurtdisiTarife, yurtdisiRota, yurtdisiEtiket, GONDERI));
node rol/kur.mjs
6600 kurus  TR 34>06 2.4kg
26200 kurus  INT TR34DE 34>GUM>DE 2.4kg

İki satır doğru. Sorun, teklif imzasının doğru olmayan çağrıları da kabul etmesidir: üç parçanın her biri iki değerden birini alabildiği için sekiz farklı çağrı yazılabilir ve bunların yalnız ikisi kastedilen çifttir.

Soyut Fabrika

İkinci sürümde gruplama role göre değil aileye göredir. Her aile bir işlevdir ve çağrıldığında üç parçayı birlikte döndürür.

// aile/yurtici.mjs — yurtici ailesi: birbirine uyan uc parca birlikte uretilir
export const yurticiAilesi = () => ({
  tarife: { ad: "yurtici", ucret: (g) => 3900 + Math.ceil(g.agirlik) * 900 },
  rota: { duraklar: () => ["34", "06"], gumrukKodu: () => null },
  etiket: (gonderi, rota) => {
    if (rota.gumrukKodu() !== null) throw new TypeError("yurtici etiketi gumruk noktasi tasimaz");
    return `TR ${rota.duraklar().join(">")} ${gonderi.agirlik}kg`;
  },
});
// aile/yurtdisi.mjs — yurtdisi ailesi: gumruklu rota ve gumruk kodu isteyen etiket
export const yurtdisiAilesi = () => ({
  tarife: { ad: "yurtdisi", ucret: (g) => 18400 + Math.ceil(g.agirlik) * 2600 },
  rota: { duraklar: () => ["34", "GUM", "DE"], gumrukKodu: () => "TR34DE" },
  etiket: (gonderi, rota) => {
    const kod = rota.gumrukKodu();
    if (kod === null) throw new TypeError("yurtdisi etiketi gumruk kodu ister");
    return `INT ${kod} ${rota.duraklar().join(">")} ${gonderi.agirlik}kg`;
  },
});
// aile/teklif.mjs — teklif uc parcayi tek bir aileden alir; eslestirme yapmaz
export const teklif = (aile, gonderi) => {
  const { tarife, rota, etiket } = aile();
  return `${tarife.ucret(gonderi)} kurus  ${etiket(gonderi, rota)}`;
};
// aile/kur.mjs — bilesim koku: yalniz aile listesini bilir
import { yurticiAilesi } from "./yurtici.mjs";
import { yurtdisiAilesi } from "./yurtdisi.mjs";
import { teklif } from "./teklif.mjs";

export const AILELER = [yurticiAilesi, yurtdisiAilesi];
export const GONDERI = { agirlik: 2.4, adres: "34100" };
for (const a of AILELER) console.log(teklif(a, GONDERI));
node aile/kur.mjs
6600 kurus  TR 34>06 2.4kg
26200 kurus  INT TR34DE 34>GUM>DE 2.4kg

Aynı iki satır. Fark teklif imzasındadır: dört parametre yerine iki parametre alıyor ve parçalar arası eşleştirme çağıranın elinden çıkmıştır.

Eşleştirmelerin Sayılması

Aşağıdaki ölçer iki tasarımda yazılabilen bütün çağrıları üretir, her birini çalıştırır ve sonucu üçe ayırır: kastedilen satırı verenler, hata atanlar ve hata atmadığı hâlde kastedilen satırı vermeyenler. Üçüncü sınıf, sessizce kabul edilen yanlış çağrılardır.

// birlesim.mjs — iki tasarimda kurulabilen butun eslestirmeleri dener ve siniflar
import { yurticiTarife, yurtdisiTarife } from "./rol/tarife.mjs";
import { yurticiRota, yurtdisiRota } from "./rol/rota.mjs";
import { yurticiEtiket, yurtdisiEtiket } from "./rol/etiket.mjs";
import { teklif as rolTeklif } from "./rol/teklif.mjs";
import { yurticiAilesi } from "./aile/yurtici.mjs";
import { yurtdisiAilesi } from "./aile/yurtdisi.mjs";
import { teklif as aileTeklif } from "./aile/teklif.mjs";

const G = { agirlik: 2.4, adres: "34100" };
const dene = (f) => { try { return f(); } catch { return null; } };
const DOGRU = new Set([aileTeklif(yurticiAilesi, G), aileTeklif(yurtdisiAilesi, G)]);

function say(etiket, satirlar) {
  const hata = satirlar.filter((s) => s === null).length;
  const dogru = satirlar.filter((s) => s !== null && DOGRU.has(s)).length;
  const sessiz = satirlar.length - hata - dogru;
  console.log(`${etiket} birlesim=${satirlar.length} dogru=${dogru} ` +
    `sessiz-yanlis=${sessiz} hata=${hata}`);
}

say("rol ", [yurticiTarife, yurtdisiTarife].flatMap((t) =>
  [yurticiRota, yurtdisiRota].flatMap((r) =>
    [yurticiEtiket, yurtdisiEtiket].map((e) => dene(() => rolTeklif(t, r, e, G))))));
say("aile", [yurticiAilesi, yurtdisiAilesi].map((a) => dene(() => aileTeklif(a, G))));
node birlesim.mjs
rol  birlesim=8 dogru=2 sessiz-yanlis=2 hata=4
aile birlesim=2 dogru=2 sessiz-yanlis=0 hata=0

Birinci sürümde yazılabilen sekiz çağrının dördü hata atıyor, çünkü etiket rotadan gümrük kodu istiyor ya da gümrük noktası bulunmasını yasaklıyor. Kalan dördün ikisi kastedilen çifttir; diğer ikisi hata atmaz ve yanlış ücretle geçerli görünen bir etiket üretir. Yurtiçi tarifesiyle yurtdışı rotası eşleştirildiğinde çıkan satır 6600 kuruş ile bir gümrük etiketi taşır ve hiçbir denetim bunu yakalamaz.

İkinci sürümde yazılabilen çağrı sayısı sekizden ikiye düşüyor. Kazanç hata yakalamada değil, yanlış çağrının yazılamaz olmasındadır: teklif artık parça almadığı için eşleştirme diye bir işlem kalmamıştır. Soyut fabrikanın ölçülebilir katkısı budur ve sayısı 2’den 0’a inen sessiz yanlışlardır.

Yeni Ailenin Maliyeti

İkinci ölçü Tasarım İlkeleri kursundan gelir: yeni bir tür eklendiğinde düzenlenen var olan dosya ve eklenen satır sayısı. Yeni aile bir hava aktarmalı ekspres taşıyıcıdır; iki ağaç kopyalanır ve aynı gereksinim ikisine de uygulanır.

cp -r rol rol-yeni
cp -r aile aile-yeni

cat >> rol-yeni/tarife.mjs <<'SON'
export const ekspresTarife = { ad: "ekspres", ucret: (g) => 24600 + Math.ceil(g.agirlik) * 4100 };
SON
cat >> rol-yeni/rota.mjs <<'SON'
export const ekspresRota = { duraklar: () => ["34", "GUM", "FRA"], gumrukKodu: () => "TR34FR" };
SON
cat >> rol-yeni/etiket.mjs <<'SON'
export const ekspresEtiket = (gonderi, rota) => {
  const kod = rota.gumrukKodu();
  if (kod === null) throw new TypeError("ekspres etiketi gumruk kodu ister");
  return `EXP ${kod} ${rota.duraklar().join(">")} ${gonderi.agirlik}kg`;
};
SON
cat >> rol-yeni/kur.mjs <<'SON'
import { ekspresTarife } from "./tarife.mjs";
import { ekspresRota } from "./rota.mjs";
import { ekspresEtiket } from "./etiket.mjs";
console.log(teklif(ekspresTarife, ekspresRota, ekspresEtiket, GONDERI));
SON

cat > aile-yeni/ekspres.mjs <<'SON'
// aile/ekspres.mjs — ucuncu aile: uc parcasi da bu dosyada
export const ekspresAilesi = () => ({
  tarife: { ad: "ekspres", ucret: (g) => 24600 + Math.ceil(g.agirlik) * 4100 },
  rota: { duraklar: () => ["34", "GUM", "FRA"], gumrukKodu: () => "TR34FR" },
  etiket: (gonderi, rota) => {
    const kod = rota.gumrukKodu();
    if (kod === null) throw new TypeError("ekspres etiketi gumruk kodu ister");
    return `EXP ${kod} ${rota.duraklar().join(">")} ${gonderi.agirlik}kg`;
  },
});
SON
cat >> aile-yeni/kur.mjs <<'SON'
import { ekspresAilesi } from "./ekspres.mjs";
console.log(teklif(ekspresAilesi, GONDERI));
SON

node rol-yeni/kur.mjs
node aile-yeni/kur.mjs

olc() {
  echo "$1 -> $2"
  echo "  duzenlenen var olan dosya = $(diff -rq $1 $2 | grep -c '^Files ')"
  echo "  yeni dosya                = $(diff -rq $1 $2 | grep -c '^Only in ')"
  echo "  eklenen satir             = $(diff -rN $1 $2 | grep '^>' | grep -cvE '^> *(//|$)')"
}
olc rol rol-yeni
olc aile aile-yeni
6600 kurus  TR 34>06 2.4kg
26200 kurus  INT TR34DE 34>GUM>DE 2.4kg
36900 kurus  EXP TR34FR 34>GUM>FRA 2.4kg
6600 kurus  TR 34>06 2.4kg
26200 kurus  INT TR34DE 34>GUM>DE 2.4kg
36900 kurus  EXP TR34FR 34>GUM>FRA 2.4kg
rol -> rol-yeni
  duzenlenen var olan dosya = 4
  yeni dosya                = 0
  eklenen satir             = 11
aile -> aile-yeni
  duzenlenen var olan dosya = 1
  yeni dosya                = 1
  eklenen satir             = 11

Eklenen satır sayısı ikisinde de 11; yazılan kod miktarı aynı. Düzenlenen var olan dosya sayısı dörtten bire indi ve kalan tek dosya bileşim köküdür. Bu, Bileşen Uyum İlkeleri dersindeki ayrımın aynısıdır: role göre gruplama bir ailenin parçalarını üç dosyaya dağıtır, aileye göre gruplama birlikte değişenleri bir arada tutar.

Bedel de sayılabilir. Aile sürümü ekspresi eklerken bir dosya kazandı ve dosya sayısı dörtten beşe çıktı; ayrıca etiket işlevinin gövdesi üç aile dosyasında üç kez ayrı ayrı yazıldı. Rol sürümünde etiket biçimlerinin üçü aynı dosyada yan yana durur ve etiket sözleşmesi değişirse tek dosya düzenlenir. Aile sürümünde aynı değişiklik aile sayısı kadar dosyaya dokunur. Kalıp bir eksene kapanır, öbürüne açılır.

Bu yüzden soyut fabrika, parçalar arasında gerçek bir uyum kısıtı yoksa uygulanmaz. Kısıt yoksa sessiz yanlış sayısı zaten sıfırdır ve kalıp yalnız dosya sayısını artırır.

Özet

  • Fabrika yöntemi tek bir nesnenin yaratımını bir yönteme çeker; üst tipin gövdesinde tür adı geçmez ve yöntemi karşılamayan alt tip ilk çağrıda hata verir.
  • Soyut fabrika birbirine uyması gereken parçaları tek çağrıda birlikte üretir; ölçüsü yazılabilen çağrı sayısıdır.
  • Çağrı yerinde eşleştirme yapılan sürümde sekiz çağrı yazılabiliyordu: dördü hata attı, ikisi doğruydu, ikisi hata atmadan yanlış ücret üretti; aile sürümünde çağrı sayısı 2’ye, sessiz yanlış sayısı 0’a indi.
  • Yeni bir aile eklendiğinde iki sürüm de 11 satır ekledi, ama düzenlenen var olan dosya sayısı 4’ten 1’e indi ve o tek dosya bileşim köküdür.
  • Kalıbın bedeli etiket sözleşmesi değiştiğinde ortaya çıkar: aile sürümünde bu değişiklik aile sayısı kadar dosyaya dokunur, rol sürümünde tek dosyaya.

Sonraki Adım

Aile işlevi çağrıldığında üç parçayı birlikte döndürüyor, ama parçaların kendisi hâlâ tek ifadeyle kurulabilen sabitler. Kurulacak nesne çok alanlı olduğunda — gönderinin ağırlığı, üç boyutu, iki adresi, beyan edilen değeri ve sözleşme numarası varsa — yaratım tek bir çağrıya sıkıştırılır ve sıralı parametreler birbirinin yerine geçebilir hâle gelir. Sonraki ders bu çağrıyı adımlara böler: argüman çiftlerinin yer değiştirdiği bütün çağrıları üreterek sessizce kabul edilen yanlış kurulum sayısını sayar, kurulumu adlandırılmış adımlara taşır ve zorunlu alan denetimini tek bir noktada toplar.

İ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