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_FOUNDverdi. - İ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.