İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat