İçeriğe geç
academia.sh

Ders 05 / 19

Bağımlılığın Ters Çevrilmesi

Soyutlamanın hangi tarafta tanımlandığının sayıya çevrilmesi: kural ve taşıyıcı paketleri arasındaki ithal kenarlarının üç sürümde yönüyle sayılması, sözleşmenin sahibi ile giren kenar sayısının ölçülmesi ve taşıyıcı paketi olmadan kural modülünün yüklenip yüklenemediğinin koşumla sınanması.

İçindekiler

Bölünmüş sözleşmeler istemcilerin yanında durdu, ama bu yerleşim ayrı bir karardı. Arayüzün kaç parçaya bölündüğü ile o parçaların hangi pakette tanımlandığı birbirinden bağımsızdır: küçük bir sözleşme de gerçekleştirimin yanında yaşayabilir. O durumda istemci küçük bir arayüze bağlanır ama yine gerçekleştirimin bulunduğu paketi ithal eder.

Bağımlılığın tersine çevrilmesi (dependency inversion) SOLID’in beşinci maddesidir ve tam bu soruyu ele alır: üst düzey kural, alt düzey gerçekleştirime değil, ikisinin de bağlandığı bir soyutlamaya bağlanmalıdır; soyutlama da gerçekleştirimin değil kuralın tanımladığı bir şey olmalıdır. Veri Erişim Katmanı ve İş Mantığı kursunda bu kural katmanlar arasındaki ithal grafiği üzerinden denetlenmiş, ihlal sayısı ve ithal kapanışının boyutu ölçülmüştü. Buradaki soru daha dar ve ölçüsü de farklı: iki paket arasındaki kenarlar hangi yöne bakıyor ve sözleşme dosyası hangi pakette duruyor?

Üç Yerleşim

Kod tabanı iki pakete ayrılmıştır. kural/ ücret kuralını, tasiyici/ taşıyıcıya özgü taban ücret hesaplarını tutar. Aynı davranış üç ayrı yerleşimle yazılır.

mkdir -p dogrudan/kural dogrudan/tasiyici arada/kural arada/tasiyici
mkdir -p tersine/kural tersine/tasiyici

Birinci yerleşimde soyutlama hiç yoktur; kural iki somut modülü doğrudan ithal eder ve aralarında kendisi seçim yapar.

// dogrudan/tasiyici/yurtici.mjs — yurtici tasiyicinin taban ucreti
export const yurticiTaban = (g) => (g.agirlik <= 1 ? 3900 : g.agirlik <= 5 ? 6400 : 11800);
// dogrudan/tasiyici/ekspres.mjs — ekspres tasiyicinin taban ucreti
export const ekspresTaban = (g) => 5200 + Math.ceil(g.agirlik) * 900;
// dogrudan/kural/ucret.mjs — ust duzey kural iki somut tasiyiciyi kendisi ithal eder
import { yurticiTaban } from "../tasiyici/yurtici.mjs";
import { ekspresTaban } from "../tasiyici/ekspres.mjs";

const BOLGE = { "34": 100, "06": 115, "65": 140 };
const katsayi = (adres) => BOLGE[adres.slice(0, 2)] ?? 160;

export function ucret(gonderi) {
  const taban = gonderi.tasiyici === "ekspres" ? ekspresTaban(gonderi) : yurticiTaban(gonderi);
  return Math.round((taban * katsayi(gonderi.adres)) / 100);
}

İkinci yerleşimde bir sözleşme vardır, ama taşıyıcı paketinde tanımlıdır.

// arada/tasiyici/sozlesme.mjs — sozlesme var, ama tasiyici paketinde tanimli
export function tasiyiciyiDogrula(t) {
  if (typeof t?.taban !== "function") throw new TypeError("tasiyici sozlesmesi eksik: taban");
  return t;
}
// arada/tasiyici/yurtici.mjs — sozlesmeyi karsilayan yurtici tasiyici
import { tasiyiciyiDogrula } from "./sozlesme.mjs";

export const yurtici = tasiyiciyiDogrula({
  ad: "yurtici",
  taban: (g) => (g.agirlik <= 1 ? 3900 : g.agirlik <= 5 ? 6400 : 11800),
});
// arada/tasiyici/ekspres.mjs — sozlesmeyi karsilayan ekspres tasiyici
import { tasiyiciyiDogrula } from "./sozlesme.mjs";

export const ekspres = tasiyiciyiDogrula({
  ad: "ekspres",
  taban: (g) => 5200 + Math.ceil(g.agirlik) * 900,
});
// arada/kural/ucret.mjs — kural sozlesmeye baglanir, ama sozlesme tasiyici paketinde
import { tasiyiciyiDogrula } from "../tasiyici/sozlesme.mjs";

const BOLGE = { "34": 100, "06": 115, "65": 140 };
const katsayi = (adres) => BOLGE[adres.slice(0, 2)] ?? 160;

export function ucret(tasiyici, gonderi) {
  tasiyiciyiDogrula(tasiyici);
  return Math.round((tasiyici.taban(gonderi) * katsayi(gonderi.adres)) / 100);
}

Üçüncü yerleşimde sözleşme dosyası olduğu gibi kural paketine taşınır. Dosyanın içeriği değişmez, yalnız yeri değişir.

// tersine/kural/sozlesme.mjs — sozlesme kural paketinde tanimli
export function tasiyiciyiDogrula(t) {
  if (typeof t?.taban !== "function") throw new TypeError("tasiyici sozlesmesi eksik: taban");
  return t;
}
// tersine/kural/ucret.mjs — kural yalniz kendi sozlesmesini ithal eder
import { tasiyiciyiDogrula } from "./sozlesme.mjs";

const BOLGE = { "34": 100, "06": 115, "65": 140 };
const katsayi = (adres) => BOLGE[adres.slice(0, 2)] ?? 160;

export function ucret(tasiyici, gonderi) {
  tasiyiciyiDogrula(tasiyici);
  return Math.round((tasiyici.taban(gonderi) * katsayi(gonderi.adres)) / 100);
}
// tersine/tasiyici/yurtici.mjs — gerceklestirim, kuralin sozlesmesini ithal eder
import { tasiyiciyiDogrula } from "../kural/sozlesme.mjs";

export const yurtici = tasiyiciyiDogrula({
  ad: "yurtici",
  taban: (g) => (g.agirlik <= 1 ? 3900 : g.agirlik <= 5 ? 6400 : 11800),
});
// tersine/tasiyici/ekspres.mjs — gerceklestirim, kuralin sozlesmesini ithal eder
import { tasiyiciyiDogrula } from "../kural/sozlesme.mjs";

export const ekspres = tasiyiciyiDogrula({
  ad: "ekspres",
  taban: (g) => 5200 + Math.ceil(g.agirlik) * 900,
});

Üç sürüm de aynı ücreti üretir.

// ayni-sonuc.mjs — uc surum ayni ucreti uretir; fark davranista degil
const GONDERI = { tasiyici: "ekspres", agirlik: 3.0, adres: "06500" };

const { ucret: dogrudanUcret } = await import("./dogrudan/kural/ucret.mjs");
const { ucret: aradaUcret } = await import("./arada/kural/ucret.mjs");
const { ekspres: aradaEkspres } = await import("./arada/tasiyici/ekspres.mjs");
const { ucret: tersineUcret } = await import("./tersine/kural/ucret.mjs");
const { ekspres: tersineEkspres } = await import("./tersine/tasiyici/ekspres.mjs");

console.log("dogrudan =", dogrudanUcret(GONDERI), "kurus");
console.log("arada    =", aradaUcret(aradaEkspres, GONDERI), "kurus");
console.log("tersine  =", tersineUcret(tersineEkspres, GONDERI), "kurus");
node ayni-sonuc.mjs
dogrudan = 9085 kurus
arada    = 9085 kurus
tersine  = 9085 kurus

Kenarların Yönle Sayılması

Fark yalnız ithal grafiğinde görünür. Aşağıdaki betik iki paketteki bütün modülleri okur, göreli ithal yollarını çözer ve paket sınırını geçen kenarları yönüne göre sayar. İkinci ölçüsü soyutlamanın kendisiyle ilgilidir: sözleşme dosyası hangi pakette duruyor ve kaç modül ona bağlanıyor?

// yon-olc.mjs — iki paket arasindaki ithal kenarlarini ve soyutlamanin sahibini sayar
import { readdirSync, readFileSync } from "node:fs";
import { join, relative, resolve, dirname } from "node:path";

const ITHAL = /^\s*(?:import|export)[^;'"]*from\s+["'](\.[^"']+)["']/gm;
const paket = (yol) => yol.split("/")[0];
const kok = process.argv[2];
const kenarlar = [];

for (const p of ["kural", "tasiyici"]) {
  for (const ad of readdirSync(join(kok, p)).filter((a) => a.endsWith(".mjs")).sort()) {
    const kaynak = `${p}/${ad}`;
    for (const m of readFileSync(join(kok, kaynak), "utf8").matchAll(ITHAL)) {
      kenarlar.push([kaynak, relative(kok, resolve(kok, dirname(kaynak), m[1]))]);
    }
  }
}

const disa = kenarlar.filter(([a, b]) => paket(a) !== paket(b));
for (const [a, b] of disa) console.log(`  ${a} -> ${b}`);
const say = (p) => disa.filter(([a]) => paket(a) === p).length;
console.log(`${kok.padEnd(9)} kural -> tasiyici = ${say("kural")}` +
  `   tasiyici -> kural = ${say("tasiyici")}`);

const soyutlamalar = [...new Set(kenarlar.map(([, b]) => b))].filter((b) => b.endsWith("sozlesme.mjs"));
if (soyutlamalar.length === 0) {
  console.log(`${kok.padEnd(9)} soyutlama tanimli degil`);
}
for (const s of soyutlamalar) {
  const giren = kenarlar.filter(([, b]) => b === s).length;
  console.log(`${kok.padEnd(9)} soyutlamanin sahibi = ${paket(s)}, giren kenar = ${giren}`);
}
node yon-olc.mjs dogrudan
node yon-olc.mjs arada
node yon-olc.mjs tersine
  kural/ucret.mjs -> tasiyici/yurtici.mjs
  kural/ucret.mjs -> tasiyici/ekspres.mjs
dogrudan  kural -> tasiyici = 2   tasiyici -> kural = 0
dogrudan  soyutlama tanimli degil
  kural/ucret.mjs -> tasiyici/sozlesme.mjs
arada     kural -> tasiyici = 1   tasiyici -> kural = 0
arada     soyutlamanin sahibi = tasiyici, giren kenar = 3
  tasiyici/ekspres.mjs -> kural/sozlesme.mjs
  tasiyici/yurtici.mjs -> kural/sozlesme.mjs
tersine   kural -> tasiyici = 0   tasiyici -> kural = 2
tersine   soyutlamanin sahibi = kural, giren kenar = 3

Üç satır üç ayrı durumu ayırıyor. Birinci sürümde kuraldan gerçekleştirime iki kenar var ve soyutlama yok. İkinci sürümde soyutlama var, giren kenar sayısı üç — yani sözleşme gerçekten işini görüyor — ama sınırı geçen kenar sayısı ikiden bire indi, sıfıra inmedi: kural hâlâ taşıyıcı paketini ithal ediyor. Üçüncü sürümde aynı sözleşme, aynı üç giren kenarla, kural paketinde duruyor ve sınırı geçen kenarların tamamı ters yöne bakıyor.

İkinci sürüm ilkenin en sık yapılan yarım uygulamasıdır. Soyutlamanın var olması yetmez; kimin tanımladığı ölçünün asıl belirleyicisidir.

Gerçekleştirim Olmadan Kural

Kenar yönü soyut bir ölçü gibi görünür; karşılığı somuttur. Her sürümden yalnız kural paketini içeren bir kopya çıkarılır ve kural modülü tek başına yüklenmeye çalışılır.

// kurali-dene.mjs — tasiyici paketi olmadan kural modulu yuklenebiliyor mu
const kok = process.argv[2];
try {
  const m = await import(`./${kok}/kural/ucret.mjs`);
  console.log(`${kok.padEnd(16)} -> yuklendi, disa verilen: ${Object.keys(m).join(", ")}`);
} catch (e) {
  console.log(`${kok.padEnd(16)} -> yuklenemedi, ${e.code}`);
}
for k in dogrudan arada tersine; do
  mkdir -p "$k-yalniz"
  cp -r "$k/kural" "$k-yalniz/kural"
  node kurali-dene.mjs "$k-yalniz"
done
dogrudan-yalniz  -> yuklenemedi, ERR_MODULE_NOT_FOUND
arada-yalniz     -> yuklenemedi, ERR_MODULE_NOT_FOUND
tersine-yalniz   -> yuklendi, disa verilen: ucret

Sözleşmesi olan ikinci sürüm de yüklenemedi. Kuralın gerçekleştirimden bağımsız çözümlenebilmesi, sözleşmenin varlığına değil, sözleşmenin kural tarafında tanımlanmış olmasına bağlı. Ölçünün sayı karşılığı budur: kuraldan gerçekleştirime giden kenar sayısı sıfır olmadıkça kural tek başına ayakta duramaz.

İlkenin Sınırı ve Bedeli

Üçüncü sürümde ücret modülü hangi taşıyıcıyla çalışacağını bilmiyor; taşıyıcı ona dışarıdan veriliyor. Bu, Frontend Mimarisi ve Birim Testi kurslarında kurulan bağımlılık enjeksiyonudur ve burada ilkenin bir sonucu olarak beliriyor: bağımlılığını kendisi kurmayan bir modülün onu bir yerden alması gerekir. O yer bileşim köküdür.

Ters çevirmenin bedeli de sayılabilir: üçüncü sürümde iki paket arasında bir sözleşme dosyası fazladan duruyor ve taşıyıcı seçimi artık ücret modülünün içinde değil, dışında. Gerçekleştirimin tek olduğu ve değişmeyeceği bilinen bir yerde bu fazlalık karşılıksızdır. İlke her bağımlılığın tersine çevrilmesini istemez; değişme hızı yüksek olan tarafın, değişme hızı düşük olan tarafa bağlanmasını ister. Taşıyıcı listesi ücret kuralından daha sık değişiyorsa yön tersine çevrilir; tersi doğruysa ölçüm de tersini söyler.

Özet

  • Bağımlılığın tersine çevrilmesi, üst düzey kuralın gerçekleştirime değil soyutlamaya bağlanmasını ve soyutlamanın kural tarafından tanımlanmasını ister.
  • Ölçü, paket sınırını geçen ithal kenarlarının yönüdür: kuraldan taşıyıcıya 2, 1 ve 0 kenar sayıldı; taşıyıcıdan kurala 0, 0 ve 2 kenar sayıldı.
  • Soyutlamanın varlığı yetmez: sözleşmesi taşıyıcı paketinde duran sürümde giren kenar sayısı 3 olmasına rağmen kural hâlâ taşıyıcı paketini ithal ediyordu.
  • Kenar yönünün karşılığı koşumla görüldü: taşıyıcı paketi olmadan yalnız üçüncü sürümün kural modülü yüklendi, ilk ikisi ERR_MODULE_NOT_FOUND verdi.
  • İlke her bağımlılığı değil, değişme hızı yüksek tarafa bakan bağımlılıkları çevirir; sözleşme dosyası ve dışarıdan verme düzeneği bunun bedelidir.

Sonraki Adım

Beş ilke birimin kime bağlanacağını söyledi: kaç aktöre, hangi genişleme eksenine, hangi sözleşmeye, hangi büyüklükteki arayüze, hangi taraftaki soyutlamaya. Hiçbiri bağın kendisini bir ölçüye bağlamadı. İki modül arasındaki bağın gücü nasıl sayılır — paylaşılan ad sayısı mı, geçilen parametre sayısı mı, ortak değiştirilen durum mu? Bir modülün içindeki parçaların birbirine gerçekten ait olup olmadığı nasıl anlaşılır? Sonraki konu bu iki ölçüyü kurar ve ilkelerin buraya kadar niteliksel bıraktığı yargıları sayıya çevirir.

İ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