Ders 01 / 19
Tek Sorumluluk İlkesi
Bir modülün değişim nedeni sayısının aktör tablosuyla sayılması: üç farklı aktörün kararını taşıyan tek modül ile kararları ayrılmış tasarımın karşılaştırılması ve muhasebe kaynaklı bir değişikliğin operasyon raporuna sızıp sızmadığının çıktı satırı sayarak ölçülmesi.
İçindekiler
Programlama Paradigmaları kursu, iki paradigmanın da tek tek birimlerin nasıl kurulacağını söylediğini, ama birimler arasındaki ilişkinin ne zaman sağlıklı olduğunu ölçmediğini adlandırarak kapandı. Bir tasarımın iyi olup olmadığı birimin içine değil, birimler arasındaki bağa bakılarak anlaşılır. Bu kurs o bağı ölçer.
İlk ölçü en küçük soruya ait: bir modül ne zaman fazla şey yapıyor sayılır? Yaygın yanıt “çok iş yapıyorsa” biçimindedir ve işe yaramaz, çünkü iş miktarı için bir eşik yoktur. Tek sorumluluk ilkesi (single responsibility principle) soruyu satır sayısından çıkarıp değişikliği isteyen tarafa taşır: bir modülün yalnız tek bir aktörün kararıyla değişmesi beklenir. Buradaki ölçü, modülün büyüklüğü değil, ona değiştirme isteği gönderebilen ayrı tarafların sayısıdır.
Üç Aktör, Tek Modül
Kurs boyunca sürecek örnek bir gönderi ücretlendirme ve yönlendirme kitaplığıdır. Ücret hesabı üç ayrı kararın çarpımıdır: ağırlık kademesinden gelen taban ücret, adresten türetilen bölge katsayısı ve sözleşmeli müşteriye uygulanan hacim indirimi. Bu kararların sahipleri farklı birimlerdir: bölge katsayılarını operasyon, indirim eşiğini sözleşme yönetimi, fatura tutarının yuvarlanmasını muhasebe belirler.
mkdir -p birlesik ayrilmis
// birlesik/ucret.mjs — uc aktorun karari tek modulde export const KADEME = [[1, 3900], [5, 6400], [20, 11800]]; export const BOLGE = { "34": 100, "06": 115, "65": 140 }; export const HACIM_ESIGI = 3; export const yuvarla = (kurus) => Math.round(kurus / 100) * 100; export const bolgeKatsayisi = (adres) => BOLGE[adres.slice(0, 2)] ?? 160; export const indirimOrani = (adet) => (adet >= HACIM_ESIGI ? 10 : 0); export function kademeUcreti(agirlik) { for (const [ust, ucret] of KADEME) if (agirlik <= ust) return ucret; return KADEME.at(-1)[1] + Math.ceil(agirlik - KADEME.at(-1)[0]) * 900; } export function ucret(gonderi, adet) { const ham = (kademeUcreti(gonderi.agirlik) * bolgeKatsayisi(gonderi.adres)) / 100; return yuvarla((ham * (100 - indirimOrani(adet))) / 100); } export function bolgeRaporu(gonderiler) { const toplam = new Map(); for (const g of gonderiler) { const b = g.adres.slice(0, 2); toplam.set(b, (toplam.get(b) ?? 0) + ucret(g, gonderiler.length)); } return [...toplam].sort().map(([b, k]) => `bolge ${b} toplam ${k} kurus`); }
// birlesik/rapor.mjs — operasyonun bolge raporu ile muhasebenin fatura tutari import { bolgeRaporu, ucret } from "./ucret.mjs"; const GONDERILER = [ { agirlik: 0.8, adres: "34100" }, { agirlik: 4.2, adres: "06500" }, { agirlik: 12.0, adres: "65200" }, { agirlik: 2.5, adres: "34700" }, ]; for (const satir of bolgeRaporu(GONDERILER)) console.log(satir); console.log("fatura toplami =", GONDERILER.reduce((t, g) => t + ucret(g, GONDERILER.length), 0), "kurus");
node birlesik/rapor.mjs
bolge 06 toplam 6600 kurus bolge 34 toplam 9300 kurus bolge 65 toplam 14900 kurus fatura toplami = 30800 kurus
Modül otuz satır ve doğru çalışıyor. Satır sayısına bakan bir ölçüt burada bir sorun göremez.
Değişim Nedeninin Sayılması
Ölçü, aktörlerin hangi simgeleri değiştireceğini bir tabloya yazıp bu simgelerin hangi dosyalarda tanımlandığını saymakla elde edilir. Aşağıdaki betik bir dizindeki modülleri tarar, her modül için kaç ayrı aktörün karar taşıdığını ve her aktörün kaç dosyaya dokunacağını bildirir.
// neden-say.mjs — her modulun kac aktorun karariyla degistigini sayar import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; // Her aktorun degistirmek isteyecegi simgeler. Gereksinimler bu tablodan okunur. const AKTOR = { operasyon: ["BOLGE", "bolgeKatsayisi", "bolgeRaporu"], muhasebe: ["yuvarla", "faturaTutari"], sozlesme: ["HACIM_ESIGI", "indirimOrani"], }; const tanim = (metin, simge) => new RegExp(`\\b(?:const|function|let|class)\\s+${simge}\\b`).test(metin); const kok = process.argv[2]; const dosyalar = readdirSync(kok).filter((a) => a.endsWith(".mjs")).sort(); const aktorunDosyalari = new Map(Object.keys(AKTOR).map((a) => [a, []])); let enYuksek = 0; for (const d of dosyalar) { const metin = readFileSync(join(kok, d), "utf8"); const bulunan = Object.entries(AKTOR) .filter(([, simgeler]) => simgeler.some((s) => tanim(metin, s))) .map(([a]) => a); for (const a of bulunan) aktorunDosyalari.get(a).push(d); enYuksek = Math.max(enYuksek, bulunan.length); console.log(`${d.padEnd(18)} degisim nedeni = ${bulunan.length} ${bulunan.join(" ")}`); } for (const [a, ds] of aktorunDosyalari) { console.log(`${a.padEnd(10)} -> ${ds.length} dosya: ${ds.join(" ")}`); } console.log(`en yuksek degisim nedeni = ${enYuksek}`);
node neden-say.mjs birlesik
rapor.mjs degisim nedeni = 0 ucret.mjs degisim nedeni = 3 operasyon muhasebe sozlesme operasyon -> 1 dosya: ucret.mjs muhasebe -> 1 dosya: ucret.mjs sozlesme -> 1 dosya: ucret.mjs en yuksek degisim nedeni = 3
Üç aktör de tek dosyaya dokunuyor ve o dosyanın değişim nedeni sayısı üç. İlkeyi ihlal eden durum budur: dosyanın büyüklüğü değil, üç ayrı tarafın aynı metni düzenleyecek olması.
Kararların Ayrılması
Ayrılmış tasarımda her aktörün kararı kendi modülünde durur. Ortada kalan tarife çekirdeği hiçbir aktörün doğrudan kararını taşımaz; kuruş cinsinden kesin değeri üretir ve yuvarlamayı yapmaz.
// ayrilmis/bolge.mjs — operasyonun karari: bolge katsayilari export const BOLGE = { "34": 100, "06": 115, "65": 140 }; export const bolgeKatsayisi = (adres) => BOLGE[adres.slice(0, 2)] ?? 160;
// ayrilmis/sozlesme.mjs — sozlesme yonetiminin karari: hacim indirimi export const HACIM_ESIGI = 3; export const indirimOrani = (adet) => (adet >= HACIM_ESIGI ? 10 : 0);
// ayrilmis/tarife.mjs — tarife cekirdegi: kurus cinsinden kesin ucret, yuvarlama yok import { bolgeKatsayisi } from "./bolge.mjs"; import { indirimOrani } from "./sozlesme.mjs"; export const KADEME = [[1, 3900], [5, 6400], [20, 11800]]; export function kademeUcreti(agirlik) { for (const [ust, ucret] of KADEME) if (agirlik <= ust) return ucret; return KADEME.at(-1)[1] + Math.ceil(agirlik - KADEME.at(-1)[0]) * 900; } export function ucret(gonderi, adet) { const ham = (kademeUcreti(gonderi.agirlik) * bolgeKatsayisi(gonderi.adres)) / 100; return (ham * (100 - indirimOrani(adet))) / 100; }
// ayrilmis/fatura.mjs — muhasebenin karari: fatura tutari tam liraya yuvarlanir import { ucret } from "./tarife.mjs"; export const yuvarla = (kurus) => Math.round(kurus / 100) * 100; export const faturaTutari = (gonderiler) => gonderiler.reduce((t, g) => t + yuvarla(ucret(g, gonderiler.length)), 0);
// ayrilmis/bolge-raporu.mjs — operasyonun karari: rapor kesin ucreti toplar import { ucret } from "./tarife.mjs"; export function bolgeRaporu(gonderiler) { const toplam = new Map(); for (const g of gonderiler) { const b = g.adres.slice(0, 2); toplam.set(b, (toplam.get(b) ?? 0) + ucret(g, gonderiler.length)); } return [...toplam].sort().map(([b, k]) => `bolge ${b} toplam ${k} kurus`); }
// ayrilmis/rapor.mjs — ayni iki cikti, ayrilmis tasarimla import { bolgeRaporu } from "./bolge-raporu.mjs"; import { faturaTutari } from "./fatura.mjs"; const GONDERILER = [ { agirlik: 0.8, adres: "34100" }, { agirlik: 4.2, adres: "06500" }, { agirlik: 12.0, adres: "65200" }, { agirlik: 2.5, adres: "34700" }, ]; for (const satir of bolgeRaporu(GONDERILER)) console.log(satir); console.log("fatura toplami =", faturaTutari(GONDERILER), "kurus");
node ayrilmis/rapor.mjs
node neden-say.mjs ayrilmis
bolge 06 toplam 6624 kurus bolge 34 toplam 9270 kurus bolge 65 toplam 14868 kurus fatura toplami = 30800 kurus bolge-raporu.mjs degisim nedeni = 1 operasyon bolge.mjs degisim nedeni = 1 operasyon fatura.mjs degisim nedeni = 1 muhasebe rapor.mjs degisim nedeni = 0 sozlesme.mjs degisim nedeni = 1 sozlesme tarife.mjs degisim nedeni = 0 operasyon -> 2 dosya: bolge-raporu.mjs bolge.mjs muhasebe -> 1 dosya: fatura.mjs sozlesme -> 1 dosya: sozlesme.mjs en yuksek degisim nedeni = 1
Fatura toplamı iki tasarımda da aynı. Bölge raporu değişti: birleşik sürüm yuvarlanmış tutarları topluyordu, ayrılmış sürüm kesin kuruşları topluyor. İkinci ölçü tabloyu tersten okur: aktör başına dosya sayısı operasyon için ikiye çıktı. Dosya sayısındaki bu artış ilkenin bedelidir; karşılığı bir sonraki ölçüde görünür.
Sızan Değişiklik
Muhasebe yeni bir kural getiriyor: fatura tutarı en yakın liraya değil, yukarı yuvarlanacak.
Değişiklik Math.round yerine Math.ceil yazmaktır. Aşağıdaki betik değişikliği iki
tasarımda da uygular ve çıktının kaç satırının değiştiğini sayar. Yerinde düzenleme
seçeneğinin uyumlu biçimi için yedek uzantısı verilmiştir; GNU ve BSD sed bu biçimde
aynı davranır.
node birlesik/rapor.mjs > birlesik-once.txt node ayrilmis/rapor.mjs > ayrilmis-once.txt echo "dokunulan dosya: $(grep -rl 'Math.round' birlesik ayrilmis | sort | tr '\n' ' ')" sed -i.yedek 's/Math.round/Math.ceil/' $(grep -rl 'Math.round' birlesik ayrilmis) node birlesik/rapor.mjs > birlesik-sonra.txt node ayrilmis/rapor.mjs > ayrilmis-sonra.txt echo "birlesik: degisen cikti satiri = $(diff birlesik-once.txt birlesik-sonra.txt | grep -c '^<')" echo "ayrilmis: degisen cikti satiri = $(diff ayrilmis-once.txt ayrilmis-sonra.txt | grep -c '^<')" diff birlesik-once.txt birlesik-sonra.txt || true
dokunulan dosya: ayrilmis/fatura.mjs birlesik/ucret.mjs birlesik: degisen cikti satiri = 3 ayrilmis: degisen cikti satiri = 1 1,2c1,2 < bolge 06 toplam 6600 kurus < bolge 34 toplam 9300 kurus --- > bolge 06 toplam 6700 kurus > bolge 34 toplam 9400 kurus 4c4 < fatura toplami = 30800 kurus --- > fatura toplami = 31000 kurus
İki tasarımda da tek dosyaya dokunuldu; fark dokunma sayısında değil, sonuçta. Muhasebenin istediği tek satır fatura toplamıydı: ayrılmış tasarımda yalnız o satır değişti. Birleşik tasarımda bölge raporunun iki satırı da kaydı, çünkü rapor yuvarlama kararının ürününü topluyordu. Operasyon bu değişikliği talep etmedi, haberi de olmadı.
İlkenin ölçülebilir vaadi budur: değişim nedeni sayısı bire indiğinde bir aktörün kararı, kararı paylaşmayan bir aktörün çıktısını bozamaz. Bedel de ölçüldü — operasyonun dosya sayısı ikiye çıktı. İlke, sürprizi dosya sayısıyla takas eder.
Sorumluluğun Sınırı Nerede
Tabloyu aktör yerine “işlev” üzerinden kurmak yaygın bir hatadır. Bölge katsayısı ile bölge raporu farklı işlerdir, ama ikisi de operasyonun kararıdır; ayrı dosyalarda durmaları zorunlu değildir. Buna karşılık yuvarlama ile bölge katsayısı aynı satırda hesaplansa bile farklı aktörlere aittir; birlikte durmaları ölçülebilir bir sürprize yol açar.
Sınırı bulmanın pratik yolu, geçmiş değişiklik isteklerinin kimden geldiğine bakmaktır. Aynı dosyaya iki farklı birimin talebiyle dokunulduysa değişim nedeni sayısı birden büyüktür. Hiçbir talep gelmemiş bir ayrımı önden kurmak ise ters yönde bir maliyettir: dosya sayısı artar, karşılığında ölçülen bir sürpriz azalmaz.
Özet
- Tek sorumluluk ilkesi modülün büyüklüğünü değil, ona değişiklik talebi gönderebilen ayrı aktörlerin sayısını sınırlar.
- Değişim nedeni sayısı ölçülebilir: aktör–simge tablosu taranarak birleşik modülde 3, ayrılmış tasarımda en fazla 1 bulundu.
- Ölçünün karşılığı koşumla görüldü: aynı muhasebe değişikliği birleşik tasarımda 3 çıktı satırını, ayrılmış tasarımda 1 çıktı satırını değiştirdi.
- Ayrımın bedeli de sayıldı: operasyonun dokunduğu dosya sayısı birden ikiye çıktı. İlke sürprizi dosya sayısıyla takas eder.
- Sınır işlev benzerliğine göre değil, değişiklik talebinin kimden geldiğine göre çizilir.
Sonraki Adım
Ayrılmış tasarımda her aktör kendi dosyasını değiştirdi, ama her aktör yine de var olan
bir dosyayı düzenledi. Bölge katsayısı tablosuna yeni bir bölge eklemek bolge.mjs
dosyasının içindeki nesneyi düzenlemek demektir. Kademe tablosuna değil de tarifenin
türüne dokunan bir istek geldiğinde — örneğin ağırlık yerine hacme göre ücretlendiren
ikinci bir tarife — düzenlenecek dosya sayısı yeniden artar. Sonraki ders, yeni bir tarife
türü eklendiğinde değişen satır sayısı ile eklenen satır sayısını iki tasarımda ölçer ve
genişlemenin değişiklik gerektirmediği bir yapının ne demek olduğunu gösterir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.