Kısım II · İnşaat · Bölüm 27
Modüler Kod
Modül, bağımlılık ve derin modül fikri (Modules and code organization)
Neden burada: Kerem'in prototipi zaten ayrı dosyalara bölünmüş (HTML, CSS, JavaScript). Ama bir uygulama büyüdükçe tek bir JavaScript dosyası da yetmez: yüzlerce satır, onlarca fonksiyon, her şeyin birbirine değdiği bir yığın olur. Bu bölüm iki soruyu cevaplıyor: kodu neden parçalara ayırırız ve ayrılan parçalar birbirine nasıl bağlanır? Kısım II'nin geri kalanı (Git, npm, TypeScript, React) bu cevabın üstüne kurulur; çünkü hepsi "dosya" ve "parça" fikrine dayanır.
Neye dayanıyor: Bölüm 25'ten fonksiyonlar, kapsam (scope) ve nesneler; Bölüm 26'dan puan, seri ve soru ilerleyişini tutan durum (state) kodu; Bölüm 1.4'ten soyutlama: bir şeyin içini bilmeden onu "arayüzüyle" kullanmak. Bu bölüm soyutlamanın kod düzenindeki halidir.
- Kat 10Bizim uygulamamızBizim uygulamamız: puan, seri, sorular gibi parçalara ayrılacak olan şey bu kat.
- Kat 9Web ve tarayıcı: HTML, CSS, JavaScriptTarayıcı denen programın anladığı diller: iskelet, görünüm, davranış. Bölüm 20, 23–26.
- Kat 8Ağ ve internetBilgisayarların birbirine bit göndermesi; IP, TCP, DNS, HTTP. Bölüm 19–20.
- Kat 7İşletim sistemi (macOS, Windows, Linux)Dosya, süreç, bellek dağıtır; terminal buradan konuşur. Bölüm 13–18.
- Kat 6Gömülü yazılım (firmware) ve açılışDüğmeye basınca ilk çalışan program. Bölüm 12.
- Kat 5Programlama dilleri ve derleyiciJavaScript'in kendisi de bu katta bir dil: modül sözdizimi dilin parçası. Bölüm 21, 25.
- Kat 4Makine dili ve assemblyİşlemcinin doğrudan anladığı komutlar. Bölüm 7.
- Kat 3İşlemci ve bellek (CPU, RAM)Komutları çalıştıran makine ve verinin durduğu yer. Bölüm 6, 8.
- Kat 2Devreler: hesap ve hafızaToplayıcılar ve hatırlayan devreler. Bölüm 4–5.
- Kat 1Transistör ve mantık kapılarıElektrikle açılıp kapanan anahtarlar. Bölüm 3.
- Kat 0Elektrik ve bit (zemin)Var ya da yok, 1 ya da 0. Bölüm 2.
Bu bölümün sonunda şunları bileceksin: bir modülün ne olduğunu ve import ile export'un tam olarak ne yaptığını; kodu parçalara ayırırken neye göre ayıracağını; iki parçanın birbirine ne kadar sıkı bağlı olabileceğini ve bunun neden önemli olduğunu; bir klasörün ve bir adın neden "okunabilirlik" meselesi olduğunu; ve yazılım tasarımının belki en kalıcı fikrini: küçük bir arayüzün arkasında büyük bir iş saklamak. Bunları tek bir somut küçük örnek üzerinden yürüteceğiz: uygulamamızın puan hesabı.
27.1 Modül, import / export, ES modülleri
Sorun: tek büyük dosya ve ortak oda
Bölüm 25'te bir değişkenin kapsamı olduğunu gördün: bir fonksiyonun içinde tanımlanan değişken dışarıdan görünmez. Peki fonksiyonların dışında tanımlanan değişkenler? Eski usul bir web sayfasında <script src="a.js"> ve <script src="b.js"> yazdığında, bu iki dosyanın en üst düzeyindeki her şey aynı odaya düşer: global kapsam (global scope). MDN bunu şöyle anlatır: tüm klasik script'ler aynı kapsamda çalışır; bu hem kullanışlı hem tehlikelidir.2 Dosyalarından biri puan adında bir değişken tanımlarsa, ötekinin aynı adda bir şey tanımlaması ya onu ezer ya da hata verir. MDN'in sıraladığı üç sorun şunlar: kapsam kirlenmesi ve isim çakışmaları; bütün bağımlılıkları yazarın elle yönetmesi (A kodu B'ye dayanıyorsa B'nin script'ini A'dan önce sen eklemelisin); ve her şeyin sayfa açılır açılmaz yüklenmesi, istek üzerine yüklemenin zor olması.2
Bunu bir apartmana benzetebilirsin: tüm daireler tek bir büyük salonu paylaşıyor, herkesin eşyası ortada. Kimin neyi nereye koyduğunu bilmeden bir şeye dokunamazsın. Modül, her daireye kendi kapısını verir; eşyanı salona çıkarmak için bilerek taşırsın.
Modül nedir?
javascript.info'nun tanımı çok sade: modül (module) yalnızca bir dosyadır. Bir script, bir modüldür.1 Modüller birbirini yükleyebilir ve iki özel anahtar kelimeyle işlev paylaşabilir:1
export(dışa aktarmak): "bu değişken ya da fonksiyon dışarıdan erişilebilir olsun" diye işaretler.import(içe aktarmak): başka bir modülden bu işaretlenmiş şeyleri alır.
Elimizdeki küçük örneği kuralım. Uygulamamızda bir cevap doğru olunca puan verilsin; seri (streak) 7 günü bulmuşsa bonus eklensin. Kerem'in prototipinde bu hesabın muhtemelen başka bir yerde olduğunu varsayarak, burada kendi örneğimizi yazıyoruz (prototipinde muhtemelen buna benzer bir şey var, ama içini görmedik):
// puan.js
const TEMEL_PUAN = 10;
export function puanHesapla(dogruMu, seri) {
if (!dogruMu) return 0;
const bonus = seri >= 7 ? 5 : 0;
return TEMEL_PUAN + bonus;
}
Satır satır: TEMEL_PUAN bu dosyanın içinde tanımlı bir sabit; export yazılmadığı için dışarıdan görünmez. puanHesapla ise export ile işaretlendi: dışarıdaki herkes onu kullanabilir. İçeride: cevap yanlışsa 0 puan; doğruysa temel puan 10, seri 7 ya da daha fazlaysa 5 bonus. Şimdi başka bir dosya bunu kullanıyor:
// main.js
import { puanHesapla } from "./puan.js";
let toplam = 0;
toplam += puanHesapla(true, 3); // 10
toplam += puanHesapla(true, 7); // 15
toplam += puanHesapla(false, 7); // 0
console.log(toplam); // 25
Bu kodu gerçekten çalıştırdım: çıktı 25 (10 + 15 + 0). import { puanHesapla } from "./puan.js" satırı "./puan.js dosyasından, yani bu dosyanın yanındaki puan.js'ten puanHesapla adlı dışa aktarılmış şeyi al" demektir. Yol (path) Bölüm 16'daki göreli yol fikriyle aynıdır: ./ "şu anki klasör" demek. javascript.info'ya göre import, modülü bu yolla yükler ve dışa aktarılan fonksiyonu karşılık gelen değişkene atar.1
Modülün gerçekten farklı yaptığı şeyler
Bir dosyayı "modül" yapmak sadece sözdizimi değildir; çalışma biçimi de değişir. javascript.info'nun sıraladığı çekirdek özellikler:1
- Kendi kapsamı vardır. Modülün üst düzeyindeki değişken ve fonksiyonlar başka script'lerde görünmez. Yukarıdaki
TEMEL_PUAN'amain.js'ten ulaşmaya kalkarsan hata alırsın (denedim:typeof TEMEL_PUAN"undefined"verdi). Paylaşmak istediğiniexportedersin, ihtiyacınıimportedersin; global değişkene güvenmezsin. - Hep katı kipte (strict mode) çalışır. Tanımlanmamış bir değişkene değer atamak gibi sessizce kabul edilen hatalar, modülde hata verir.
- Yalnızca bir kez çalıştırılır. Aynı modülü beş farklı dosya
importetse bile, modülün kodu ilkimport'ta bir kez çalışır; sonra herkes aynı çıktıları paylaşır.1 Bunu da denedim:sorular.js'e birconsole.logkoyup iki yerdenimportettiğimde mesaj tek kez yazıldı. Bunun önemli bir sonucu: iki dosya aynı modülden bir nesne alırsa, ikisi de tam olarak aynı nesneyi görür; birinin yaptığı değişiklik ötekine yansır.1
Tarayıcıda bir de şunu bilmek gerekir: bir script'in modül olarak ele alınması için HTML'de <script type="module" src="main.js"></script> yazılır.1 Modül script'leri ayrıca otomatik olarak ertelenir (deferred): HTML'in yüklenmesini engellemezler, belge hazır olunca çalışırlar.1 Bir başka tuzak: modüller yalnızca HTTP(S) üzerinden çalışır; dosyayı çift tıklayıp file:// ile açarsan import çalışmaz. Çözüm yerel bir sunucudur (javascript.info "Live Server" benzeri bir araç öneriyor).1 Neden böyle olduğunu Bölüm 20'den biliyorsun: tarayıcı import satırını görünce ilgili dosyayı bir HTTP isteğiyle sunucudan ister; file://'ta sunucu yok. Yani modüller, dosya sistemi (Kat 7) ile ağın (Kat 8) üzerinde çalışan bir dil özelliğidir. (Sunucu ve geliştirme sunucusu için Bölüm 29'a bak.)
Adlandırılmış ve varsayılan dışa aktarım
Yukarıda { puanHesapla } gibi süslü parantezle alınan şeye adlandırılmış dışa aktarım (named export) denir: adı vardır ve almak isteyen o adı yazmak zorundadır. Bir de modül başına en fazla bir tane olabilen varsayılan dışa aktarım (default export) vardır. MDN'e göre bu, tek bir ana değer (bir sınıf, bir fonksiyon) etrafında kurulan modüller içindir; alan kişi ona kendi istediği adı verir.2
// puan.js (devamı)
export default function ozet(puan) {
return `Toplam: ${puan} puan`;
}
// main.js
import ozet from "./puan.js"; // süslü parantez yok, ad serbest
import { puanHesapla } from "./puan.js"; // süslü parantez var, ad sabit
console.log(ozet(25)); // Toplam: 25 puan
İki biçimin karşılaştırması şöyle: adlandırılmış dışa aktarımda ad iki tarafta da aynı olmak zorunda olduğu için kod aranabilir ve hata yapmak zordur; varsayılanda herkes aynı şeye farklı ad verebilir, bu da büyük projelerde karışıklık doğurabilir. Bu yüzden birçok ekip adlandırılmış olanı tercih eder; React gibi araçların belgelerinde ikisini de göreceksin, o yüzden ikisini de tanıman gerekir.
Ad çakışması olursa MDN'in gösterdiği iki çare var: import { draw as drawSquare } from ... ile as kullanarak yeniden adlandırmak ya da import * as Square from "./square.js" ile bir modül ad alanı nesnesi (module namespace object) almak; böylece her şeye Square.draw() diye ulaşırsın.2 Birkaç modülün çıktısını tek bir "vitrin" modülde toplamak (MDN buna aggregating der) da mümkündür.2
Statik ve dinamik import
Şimdiye kadar gördüğümüz import satırları statiktir: dosyanın en üstünde, sabit bir metin yoluyla yazılır; if bloğunun içine konamaz, yol bir fonksiyon sonucu olamaz.3 Bunun bir nedeni var ve çok güzel bir nedeni: javascript.info'ya göre bu katılık, kodun yapısının araçlarca önceden analiz edilebilmesini sağlar; böylece modüller tek dosyada toplanabilir ve kullanılmayan çıktılar atılabilir ("tree-shaking", ağacı silkeleyip ölü yaprakları düşürmek).3 Bu araçları Bölüm 29'da göreceğiz.
İstek üzerine yüklemek için import() ifadesi vardır: modülü yükler ve çıktılarını içeren bir Promise döndürür (Promise'i Bölüm 25.6'da gördün); herhangi bir yerden çağrılabilir.3 Örneğin ağır bir "profil sayfası" kodunu, yalnızca kullanıcı o sayfaya girince yüklemek için: const { profilGoster } = await import("./profil.js");.
Modül sistemlerinin kısa tarihi
JavaScript'te dil düzeyinde modül sözdizimi 2015 standardıyla geldi; o zamandan beri tüm büyük tarayıcılarda ve Node.js'te desteklenir.1 Öncesinde topluluk kendi çözümlerini üretmişti: AMD (RequireJS), Node.js için üretilen CommonJS ve ikisiyle uyumlu olmaya çalışan UMD.1 Eski kodlarda ve eski Node belgelerinde require(...) ve module.exports gördüğünde bu CommonJS'tir; "ES modülleri" (ES modülleri (ES modules, ESM)) ise import/export olan, bugünün standart sistemidir. Node.js'te bir dosyanın ES modülü sayılması için .mjs uzantısı ya da package.json'a "type": "module" yazmak gerekir.2 (package.json'u Bölüm 29.1'de açacağız.)
A modülü B'yi, B de A'yı import ederse ne olur? Denedim: a.js başlıyor, b.js'i yüklemek için duruyor; b.js çalışırken a.js'in henüz bitmemiş çıktısına ulaşmaya kalkınca ReferenceError: Cannot access 'a' before initialization hatası geldi. Yani iki dosya birbirini döngü halinde beklediğinde biri henüz hazır olmayan şeyi kullanmaya çalışır. Ders: bağımlılıklar tek yönde akmalı (yukarıdan aşağıya, bir ağaç ya da çizge gibi, ama halka değil). MDN'in dediği gibi modüller bir bağımlılık çizgesi (dependency graph) oluşturur: her modül hangi modüllere dayandığını açıkça yazar.2
Prototipinde HTML, CSS ve JavaScript ayrı dosyalarda; muhtemelen JavaScript tarafı da (soru verisi, puan, seri, ekran güncelleme) bir ya da birkaç dosya. Eğer HTML'de birden çok <script src="…"> satırı varsa ve dosyalar birbirinin değişkenlerine dokunuyorsa, bu yukarıdaki "ortak oda" düzenindesin demektir. Dönüşümün yolu belli: her dosyaya export/import eklemek ve HTML'de tek bir <script type="module" src="main.js"> bırakmak. Expo (Bölüm 32) ve TanStack (Bölüm 33) ile yazacağımız her dosya zaten bir ES modülü olacak; yani bu bölümdeki import satırlarını yarın her dosyanın başında göreceksin.
27.2 Sorumlulukların ayrılması ve tek sorumluluk
Dosyaları ayırmanın mekanik yolunu öğrendik. Ama neye göre ayıracağız? Yirmi fonksiyonu rastgele dört dosyaya dağıtmak, bir odadaki eşyayı rastgele dört odaya dağıtmak gibidir: oda sayısı arttı, düzen artmadı. Cevap iki eski fikirde.
Sorumlulukların ayrılması
Sorumlulukların ayrılması (separation of concerns) terimi, bilinen kayda göre Edsger Dijkstra'nın 1974 tarihli "On the role of scientific thought" yazısına gider.6 Fikir şu: bir sistemi, her biri tek bir "derdi" ele alan parçalara böl, ve bir derdi düşünürken ötekileri kafandan çıkarabilmeyi hedefle. Bu, bu kitabın başından beri yürüttüğümüz soyutlama fikrinin ta kendisi: aynı anda her şeyi düşünemeyiz, o yüzden bir kerede bir şey düşünürüz.
Bunu zaten gördün: Bölüm 23–25'te web sayfasını üç dosyaya ayırdık: HTML yapıyı, CSS görünümü, JavaScript davranışı taşıyor. Bu, sorumlulukların ayrılmasının "dil düzeyindeki" hali. Şimdi aynı fikri JavaScript'in içine taşıyoruz.
Uygulamamızdan bir örnek: neleri ayırırız?
Kullanıcı bir soruyu cevaplayınca prototipte neler olur? (Bölüm 26'daki akışı hatırla.) En az dört ayrı "dert" var:
| Dert | Soru | Örnek parça |
|---|---|---|
| Veri | Sorular ve cevaplar neler? | sorular.js |
| Kural | Doğru cevaba kaç puan? | puan.js |
| Saklama | Puan ve seri kapanınca nerede durur? | depo.js (ör. localStorage, Bölüm 26.4) |
| Ekran | Sonucu nasıl göstereriz? | ekran.js (DOM, Bölüm 26.1) |
Hepsi tek fonksiyonda olsaydı, "doğru cevaba 12 puan olsun" diye bir kural değişikliği yapmak için ekranı çizen satırların ortasında gezinirdin. Ayrı olsaydı yalnızca puan.js'e dokunurdun. Bu, kural değişince ekran kodunun bozulma riskini de ortadan kaldırır.
Tek sorumluluk ilkesi
Tek sorumluluk ilkesi (single responsibility principle, SRP) aynı fikrin daha keskin söylenişidir. Robert C. Martin, onu "bir sınıfın değişmek için yalnızca bir nedeni olmalı" diye ifade etmiş, sonra "neden" kelimesindeki karışıklık yüzünden ilkeyi sorumlulukların ayrılması ile ilişkilendirerek açıklamıştır.7 Ölçüt şudur: bir modülü hangi sebeplerle değiştirmen gerekir? Eğer cevap "puan kuralı değişirse" ve "ekran tasarımı değişirse" ve "veri biçimi değişirse" ise, o modül üç ayrı derdin bekçiliğini yapıyor demektir.
Hukukta da bunun karşılığı var: yasama, yürütme ve yargı ayrı organlardadır; çünkü aynı elde birleşirse biri değişince ötekileri etkiler ve hesap sorulabilirliği azalır. (Analoji burada biter; yazılımda amaç iktidar paylaşımı değil, değişikliğin zararını sınırlamaktır.)
"Tek sorumluluk" her fonksiyonu ayrı dosyaya koy demek değildir. Aşırı bölünmüş kodda bir şeyi anlamak için on dosyaya gitmen gerekir. 27.5'te Ousterhout'un bu konuda ne dediğini göreceğiz: ayırmak her zaman iyi değildir; bölünen parçaların arayüzleri de bir maliyettir.
27.3 Bağımlılık; bağlaşım ve uyum
Bağımlılık
Modül A modül B'den bir şey import ediyorsa, A, B'ye bağımlıdır (dependency). John Ousterhout'un A Philosophy of Software Design kitabında bağımlılık şöyle tanımlanır: verilen bir kod parçasının tek başına anlaşılıp değiştirilemediği durumdur. Ousterhout bağımlılıkların yazılımın temel bir parçası olduğunu, tamamen ortadan kaldırılamayacağını, ama tasarım yoluyla azaltılıp sadeleştirilebileceğini söyler; ona göre karmaşıklığın iki ana nedeni bağımlılıklar ve muğlaklıktır (obscurity).45
Bağımlılık kötü bir şey değildir; main.js kaçınılmaz olarak puan.js'e bağımlıdır. Soru: bu bağımlılık ne kadar sıkı?
Bağlaşım
Bağlaşım (coupling), iki modül arasındaki karşılıklı bağımlılığın derecesidir: iki parçanın birbirine ne kadar sıkı bağlı olduğunun ölçüsü.8 Aşağıdaki iki yazım aynı işi yapar; biri sıkı, biri gevşek bağlaşımlıdır.
// SIKI bağlaşım: puan hesabı, ekranın iç yapısını biliyor
export function puanHesapla(dogruMu) {
const el = document.querySelector("#puan-kutusu"); // ekrana uzanıyor
const eski = Number(el.textContent);
el.textContent = eski + (dogruMu ? 10 : 0);
}
// GEVŞEK bağlaşım: yalnızca sayı alıp sayı döndürüyor
export function puanHesapla(dogruMu, seri) {
if (!dogruMu) return 0;
return 10 + (seri >= 7 ? 5 : 0);
}
İlkinde puanHesapla, sayfada #puan-kutusu adlı bir öğenin var olduğunu bilmek zorunda; HTML'de o öğenin kimliğini değiştirirsen puan hesabı bozulur. Test etmek için bile bir web sayfası gerekir. İkincisi hiçbir şey bilmiyor: bir sayı alıp bir sayı veriyor. Ekranı güncellemek, onu çağıran ekran.js'in işi. Ousterhout'un diliyle: ilkinde bilgi sızıntısı (information leakage) var; ekran düzeni hakkındaki bilgi puan modülüne de sızmış.5 Bir tasarım kararı birden çok modülde yansıyorsa, o karara dokunmak çok yere dokunmak demektir.5 Ousterhout'un bunu karmaşıklığın belirtileri olarak adlandırdığı şeyler: değişiklik çoğalması (change amplification): basit bir değişiklik birçok yerde kod değişikliği gerektirir; bilişsel yük (cognitive load): bir işi yapmak için kafanda ne kadar şey taşıman gerektiği.5
Uyum
Uyum (cohesion) ise bir modülün içindeki öğelerin ne kadar birbirine ait olduğunun ölçüsüdür.8 Bağlaşım parçalar arası, uyum parça içi bir ölçüdür. İyi bir tasarımın hedefi: modüller arası bağlaşım gevşek, modül içi uyum yüksek. puan.js'te yalnızca puan kuralları varsa uyumludur; oraya birden "kullanıcının adını büyük harfe çeviren" bir fonksiyon eklenirse uyum düşer, çünkü o fonksiyon bu modülün konusu değil.
İkisinin ilişkisini şöyle düşün: ayırma işini iyi yaptıysan, bir değişiklik kararın tek bir modülde kalır (yüksek uyum) ve dışarıya taşmaz (gevşek bağlaşım).
Gelecekte agent'lara iş verdiğinde (Bölüm 46) bu iki ölçü senin en iyi dostun olacak. "Seri kuralını değiştir" dediğinde, agent'ın hangi dosyaları değiştirdiğine bak: yalnızca seri kuralının bulunduğu modül mü, yoksa ekran, depolama ve soru dosyaları da mı? İkinci durum, yüksek bağlaşımın işaretidir; agent'ın hatası olmayabilir, ama kod tabanındaki bir tasarım sorununu gösterir. Bunu fark edebilmek, "anlamadığım bir şeyin beni sürüklemesi" yerine yönü senin belirlemen demektir.
27.4 Klasör yapısı, isimlendirme, okunabilirlik
Klasör yapısı
Dosyalar çoğalınca onları gruplamak gerekir; klasör (Bölüm 16'daki dosya sistemi ağacı) burada kullanılan araçtır. Küçük bir proje için iki yaygın düzen vardır:
- Türe göre:
veri/,kurallar/,ekranlar/. Aynı türden dosyalar bir arada. Küçük projede rahattır. - Özelliğe göre:
sorular/,puan/,seri/,profil/. Her klasörde o özelliğin kuralı, verisi ve ekranı birlikte. Büyüdükçe daha iyi ölçeklenir; çünkü "seriyi değiştir" demek için tek klasöre bakarsın.
uygulama/
├── src/
│ ├── sorular/
│ │ ├── sorular.js (veri)
│ │ └── soruEkrani.js (ekran)
│ ├── puan/
│ │ └── puan.js (kural)
│ ├── seri/
│ │ └── seri.js
│ └── main.js (giriş noktası)
└── index.html
Hangisinin doğru olduğu yoktur; tutarlı olmak yeter. Ousterhout'un "tutarlılık" bölümünün fikri: benzer şeyler benzer biçimde yapılırsa, bir yerde öğrendiğin şeyi başka yerleri anlamak için de kullanabilirsin.5 Bir de giriş noktası (entry point) kavramı var: uygulamanın başladığı dosya (burada main.js); modül çizgesi bu dosyadan dallanır. Tarayıcıda HTML'in type="module" ile yüklediği dosya budur.
İsimlendirme
İsimler, kodun en çok okunan kısmıdır. Ousterhout isimlerin kesin ve tutarlı olmasını önerir: isim, okuyanın zihninde doğru bir resim oluşturmalı; aynı kavram her yerde aynı adla geçmeli.5 Örnekler:
| Belirsiz | Kesin | Neden |
|---|---|---|
veri | soruListesi | "Veri" her şey olabilir; liste olduğu adından belli olsun. |
hesapla() | puanHesapla() | Neyi hesapladığı belli. |
x, tmp | kalanCan | Bir değerin neyi temsil ettiği isimde yazsın. |
puan bir yerde, skor başka yerde, score üçüncüde | Hep puan | Aynı kavram, tek ad: sözlükteki kural kodda da geçerli. |
JavaScript'te alışılmış yazım biçimi, ilk sözcük küçük, sonrakilerin baş harfi büyük olan puanHesapla gibi deve yazımı (camelCase); sınıf ve (React'te) bileşen adları büyük harfle başlar (SoruKarti). Dosya adlarında ise ekipler farklı tercih eder; önemli olan tutarlılık. Bir de pratik alışkanlık: kod ve dosya adlarında genellikle Türkçe karakterlerden (ş, ı, ğ) kaçınılır; Bölüm 2.5'teki kodlama sorunlarını dosya adlarına taşımamak için soruEkrani.js yazarız. (Bu bir kural değil, yaygın bir ihtiyat.)
Okunabilirlik
Okunabilirlik (readability), kodu bir başkasının (ya da altı ay sonra senin) kolayca anlayabilmesidir. Ousterhout'un yaklaşımı şu: karmaşıklık, bir yazılımın yapısında anlamayı ve değiştirmeyi zorlaştıran her şeydir; ve karmaşıklık, yazana değil okuyana daha belirgin görünür: kendi kodunu karmaşık bulmayabilirsin, başkası bulur.5 Bu bir uyarı: "Bende çalışıyor" kodun iyi olduğunu göstermez; başkasının (veya agent'ın) bu kodu okuyup güvenle değiştirebilmesi iyi olduğunu gösterir. Ousterhout'un bu konudaki cümlesi: yazılım, yazma kolaylığı için değil okuma kolaylığı için tasarlanmalıdır; bariz (obvious) kod, fazla düşünmeden hızla okunabilen koddur.5
Pratik bir kural: bir fonksiyonun adını okuyunca ne yaptığını tahmin edebiliyorsan ve gövdesini okuyunca tahminin doğru çıkıyorsa, kod okunaklıdır. Tahmin yanlış çıkıyorsa, ad ya da fonksiyon yanlıştır.
27.5 Derin modül fikri: sade arayüz, zengin içerik
Şimdi bölümün asıl fikrine geldik. John Ousterhout (Stanford Üniversitesi bilgisayar bilimi profesörü) A Philosophy of Software Design kitabını (ikinci baskı Temmuz 2021'de çıktı)4 tek bir savın etrafında kurmuştur: yazılım tasarımının amacı karmaşıklığı azaltmaktır. Kitabın önsözünde, bilgisayar biliminin en temel problemlerinden birinin problemi parçalara ayırmak olduğunu söyler: karmaşık bir problemi bağımsız çözülebilecek parçalara bölmek.5 Biz bunu 27.2'de gördük. Peki iyi bir parça hangisidir?
Arayüz ve gerçekleme
Ousterhout her modülün iki parçası olduğunu söyler. Arayüz (interface), başka bir modülde çalışan bir geliştiricinin bu modülü kullanmak için bilmesi gereken her şeydir. Gerçekleme (implementation) ise arayüzün verdiği sözü yerine getiren koddur.5 (Hatırla: bu kitapta implementation için "gerçekleme" deriz; "uygulama" yalnızca application demektir.) Arayüz yalnızca fonksiyonların adları ve parametreleri değildir; Ousterhout'a göre koda yazılan resmî kısımlar yanında, kodda yazılamayıp yalnızca yorumlarla anlatılabilen gayriresmî kısımlar da vardır (ör. "önce baslat() çağrılmalı" gibi gizli kurallar).5
Bizim puan.js'te arayüz: tek bir fonksiyon, puanHesapla(dogruMu, seri); ikisini ver, bir sayı al. Gerçekleme: TEMEL_PUAN sabiti, bonus kuralı, "yanlışsa sıfır" kuralı. main.js'in gerçeklemeden haberi olması gerekmez. Yarın bonus kuralını "seri 7'den 10'a çıkınca" diye değiştirirsek, main.js'e dokunmaya gerek kalmaz: Ousterhout'un söylediği, bir modülü arayüzünü değiştirmeden değiştirebiliyorsan başka hiçbir modülün etkilenmemesidir.5 Bu export/import'un tasarım anlamı: export yazdıkların arayüz, yazmadıkların gerçekleme.
Derin modül
Ousterhout'un kilit cümlesi: en iyi modüller derindir; basit bir arayüzün arkasında çok işlev bulunur.5 Derinliği "maliyet–fayda" olarak düşünmeni önerir: maliyet arayüzdür (modülü kullanan herkesin öğrenmesi gereken şey), fayda ise modülün sağladığı işlevdir.5 Bir modülün şeklini dikdörtgen olarak çizersen: alan işlevi, üst kenarın genişliği arayüzü gösterir. Derin modül dar ama uzun, sığ modül geniş ama kısadır.
DERİN modül SIĞ modül
┌──┐ ← dar arayüz ┌────────────────┐ ← geniş arayüz
│ │ │ │
│ │ └────────────────┘
│ │ ← çok işlev az işlev
│ │
└──┘
Günlük bir örnek: bir otomobilin direksiyonu çok derin bir modüldür. Arayüz: sağa-sola çevir. Arkasında: dişli sistemi, hidrolik/elektrikli destek, geometri, hepsi senin bilmeden kullandığın şeyler. Sığ bir modül örneği: "sol tekerleğin açısını 0,3 derece değiştir" ve "sağ tekerleğin açısını 0,3 derece değiştir" diye iki ayrı düğmeyi şoföre verirsek, arayüz karmaşık, işlev az olur.
Yazılımda en tanıdık örnek, Bölüm 16'daki dosya sistemi. İşletim sistemi bir dosyayı kullanman için sana yalnızca birkaç basit işlem verir (aç, oku, yaz, kapat); disk blokları, önbellek ve izinler gibi ayrıntılar bu arayüzün arkasında saklıdır. Bu, Ousterhout'un kitabından değil, bizim Bölüm 16'dan bir gözlem; ama "derin modül" ölçüsüne tam oturur.
Bilgi gizleme
Derinliği mümkün kılan teknik bilgi gizleme (information hiding)'dir: her modül, bazı bilgiyi (veri yapısı, algoritma, kural) kendi içinde saklar ve arayüzde göstermez.5 İki faydası vardır: arayüz sadeleşir (bilişsel yük azalır) ve sistemi değiştirmek kolaylaşır, çünkü gizlenen bilgiye bağlı tasarım değişikliği yalnızca o modülü etkiler.5 Ousterhout'un uyarısı: private yazmak tek başına bilgi gizlemek değildir; asıl soru, hangi tasarım kararının modülde saklandığıdır.5 Ters olgu, bilgi sızıntısı: aynı bilginin birden fazla modülde yaşaması.5 Biz bunu 27.3'te #puan-kutusu ile gördük.
Sığlığın tuzağı: "classitis"
Yaygın bir öğüt "sınıflar küçük olmalı" der. Ousterhout buna karşı çıkar: bunu aşırıya götürünce her biri tek başına basit, ama biriken arayüzleri toplamda muazzam bir karmaşıklık yaratan çok sayıda küçük sınıf ortaya çıkar; bu hastalığa classitis der.5 Ve bir arayüzde çok sayıda özellik varsa çoğu geliştirici yalnızca birkaçını bilir.5 27.2'deki dengeli ol uyarısının nedeni budur: puanHesapla, bonusHesapla, temelPuanBul, yanlisMiBak diye dört küçük dosya yazarsan arayüz toplamı büyür ama işlev aynı kalır; sığ olur. Tek bir puan.js, tek bir fonksiyonla, derindir.
Yani "küçük dosya" hedef değil. Hedef: sade arayüz. Bazen bir dosyanın 300 satır olması, arayüzü sadeyse, 10 küçük dosyadan daha iyidir.
Gelecekteki parçalarımızı derinlik ölçüsüyle düşün: seri.js (seri takibi) dışarıya belki yalnızca seriyiGuncelle(bugun) ve seriyiOku() sunsun; içinde "dün girdi mi, tatil günü sayılır mı, saat dilimi" gibi inceliklerin hepsi saklı kalsın. Sonra Go arka ucu (Bölüm 34) ve veritabanı (Bölüm 38) bu fikri büyük ölçekte tekrarlayacak: bir API de bir arayüzdür ve arkasında gizlenen bir gerçekleme vardır. Mimarilerde (Bölüm 44) Ousterhout'a yeniden döneceğiz.
Bölüm 1.4 (soyutlama): Bir şeyin içini bilmeden arayüzüyle kullanmak. Modülün arayüzü soyutlamadır; export'lar onun görünür yüzü. Derin modül, "iyi soyutlama"nın sayısal ölçüsüne en yakın şeydir: küçük arayüz, büyük gizli işlev. Bölüm 25 (JavaScript): fonksiyon ve kapsam; modül, kapsam fikrinin dosya düzeyine genişlemiş halidir (kapanış/closure kavramıyla aynı mantık: içeriyi gizle, ihtiyacı dışarı ver). Bölüm 26 (DOM ve olaylar): ekran güncelleyen ve durum tutan kodları ayırma sorusu, tam da 27.2'deki "ekran" ile "kural" ayrımıdır.
Bu bölümde neler kuruldu
Bir modül bir dosyadır; kendi kapsamı vardır, hiçbir şey kendiliğinden dışarı çıkmaz. export bir şeyi dışarı açar, import başka modülden alır; adlandırılmış ve varsayılan dışa aktarım iki biçimdir; dinamik import() istek üzerine yükler; ES modülleri 2015'ten beri dilin standardıdır. Bir kodu sorumlulukların ayrılması ve tek sorumluluk ilkesiyle böleriz: bir modülün değişmek için tek nedeni olmalı. Parçalar arası ilişki bağımlılık; bunun sıkılığı bağlaşım, modül içi birliktelik uyum: hedef gevşek bağlaşım ve yüksek uyum. Klasör düzeni, kesin ve tutarlı adlar ve okunabilirlik, bu yapının insanlara görünen yüzüdür. Son olarak derin modül fikri: arayüz maliyet, işlev fayda; en iyi modül dar arayüzün arkasında çok iş saklayandır, bilgi gizleme bunun aracıdır. Terimler sözlükte, Bölüm 27 başlığı altında.
javascript.info, Modules, introduction: bu bölümün 27.1 kısmının tamamı, örneklerle. Derinleşmek için MDN, JavaScript modules (özellikle Default exports versus named exports, Aggregating modules) ve Ousterhout'un kitabı A Philosophy of Software Design, 4. bölüm (Modules Should Be Deep) ve 5. bölüm (Information Hiding (and Leakage)); kitap basılıdır, Stanford'daki kitap sayfasında ikinci baskı ve bir bölüm özeti (extract) bulunur.
Takıldığın her yeri yapay zekâ öğretmenine sor. Bir öneri: prototipindeki JavaScript dosyalarını ona göster ve "bu dosyaların bağlaşımı ve uyumu nasıl, hangi parçayı hangi modüle ayırırdın?" diye sor; sonra onun cevabını bu bölümdeki 27.2 ve 27.3 ölçütleriyle kendin değerlendir.
Bölüm sonu soruları
Kitaba bakmadan cevapla. Kâğıt kalem serbest.
export yazılmamış bir fonksiyonu başka bir modül almaya çalışırsa ne olur?export ile işaretlenenler dışarı açılır. Bkz. 27.1.import ederse modülün kodu kaç kez çalışır?import'ta çalıştırılır, çıktıları sonraki herkesle paylaşılır. Bu yüzden iki dosya aynı nesneyi görür. Bkz. 27.1.file:// ile açtığın sayfada import neden çalışmaz?import, tarayıcının ilgili dosyayı bir HTTP isteğiyle istemesidir; file://'ta sunucu yoktur. Çözüm yerel bir geliştirme sunucusudur. Bkz. 27.1.puanHesapla içinde document.querySelector("#puan-kutusu") kullanmak neyi artırır?puan, başka yerde skor yazmak hangi ilkeye aykırıdır?Örnek cevabı göster
Arayüz, başkalarının modülü kullanmak için bilmesi gereken şeydir; gerçekleme gizlidir. puanHesapla(dogruMu, seri) arayüzü aynı kaldığı sürece bonus kuralını (7 günlük seriden 10 güne) değiştirmek yalnızca puan.js'i etkiler; main.js, ekran ve depolama kodu aynen çalışır. Hukuk benzetmesi: bir kanunun uygulama yönetmeliği değişse bile, vatandaşın o kanuna başvuru usulü (arayüz) aynı kalabilir.
utils.js adlı bir dosyada hangi sorunları görürsün ve nasıl düzeltirsin?Örnek cevabı göster
Düşük uyum: fonksiyonlar birbirine ait değil (kural, biçim, ekran ayrı dertler) ve dosyanın değişmek için üç ayrı nedeni var (tek sorumluluk ihlali). "utils" adı da belirsizdir. Düzeltme: puan kurallarını puan.js'e, tarih biçimlemeyi tarih.js'e, ekran çizmeyi ekran.js'e ayırmak; ama her birinin arayüzü küçük kalsın ki aşırı bölünme (sığ modüller) oluşmasın.
Kaynaklar
- javascript.info, Modules, introduction: modül bir dosyadır,
export/import, kendi kapsamı, bir kez çalışma,type="module",file://sınırı, AMD/CommonJS/UMD tarihçesi, 2015 standardı. - MDN, JavaScript modules: klasik script ile modül farkı, global kapsam sorunları, bağımlılık çizgesi, varsayılan ve adlandırılmış export, yeniden adlandırma, ad alanı nesnesi, toplama,
.mjsve"type": "module". - javascript.info, Dynamic imports: statik import kısıtları,
import()ifadesi, tree-shaking gerekçesi. - John Ousterhout, A Philosophy of Software Design (2. baskı, 2021, basılı); yazarın kitap sayfası (Stanford): ikinci baskının yenilikleri ve bölüm özeti.
- Alexander Sivji, A Philosophy of Software Design: okuma notları (ikincil özet): karmaşıklık, bağımlılık, muğlaklık, arayüz/gerçekleme, derin modüller, classitis, bilgi gizleme ve sızıntısı. Not: ikincil kaynaktır; ilgili bölümler kitabın 2, 4 ve 5. bölümleridir.
- Wikipedia, Separation of concerns: terimin Dijkstra'nın 1974 tarihli yazısına atfı.
- Wikipedia, Single-responsibility principle: Robert C. Martin'in "değişmek için tek neden" ifadesi.
- Wikipedia, Coupling ve Cohesion: modüller arası bağımlılık derecesi ve modül içi birliktelik.