Ders 05 / 30
Tekil
Tek örneğin küresel durum olarak tutulmasının sınanabilirlik cinsinden ölçülmesi: aynı dört sınamanın modül düzeyinde tek örnek tutan kayıt ile örnek üreten kayda uygulanması, başarısız sınama sayısının sayılması, sınama sırası değiştirildiğinde sonucun değişmesi ve sıfırlama düzeneğinin dışa açılan yüzeye eklediği adın ölçülmesi.
İçindekiler
Dört yaratımsal düzenek nesnenin nereden geldiğini düzenledi; hiçbiri nesnenin kaç tane olduğunu sormadı. Kitaplıkta tek örneğinin bulunması istenen nesneler var: tarife kaydı bunların başında geliyor, çünkü iki ayrı kayıt aynı ada iki farklı tarife bağlayabilir ve aynı gönderi iki farklı ücret alabilir.
Tekil (singleton) bu isteği nesnenin kendisine yazar: tip kendi örneğini kurar, tek tuttuğunu güvenceye alır ve o örneğe küresel erişim verir. Klasik biçiminde kurucu dışarıya kapatılır ve erişim tek bir sınıf yöntemiyle verilir. Modül sistemi olan bir dilde bu iş zaten modülün kendisiyle yapılır: bir modülün gövdesi süreç başına bir kez çalışır ve modül düzeyindeki durum bütün ithal edenler için aynıdır. Bu ders kalıbın bedelini tek bir ölçüyle sayar: sınanabilirlik.
Modül Düzeyinde Tek Örnek
Birinci sürümde kayıt bir modüldür. Durum modül düzeyinde durur, ilk ithalde kurulur ve süreç boyunca tek kalır.
// tekil/tarifeler/standart.mjs — standart tarife tanimi export const STANDART = { ad: "standart", taban: () => 3900 };
// tekil/kayit.mjs — modul duzeyinde tek ornek; durum ilk ithalde kurulur import { STANDART } from "./tarifeler/standart.mjs"; const TARIFELER = new Map(); export function kaydet(tarifeNesnesi) { TARIFELER.set(tarifeNesnesi.ad, tarifeNesnesi); } export function tarife(ad) { const t = TARIFELER.get(ad); if (t === undefined) throw new RangeError(`bilinmeyen tarife: ${ad}`); return t; } export const adlar = () => [...TARIFELER.keys()]; export function sifirla() { TARIFELER.clear(); kaydet(STANDART); } sifirla();
Son satır kalıbın belirleyici yanıdır: durum modül yüklenirken kurulur, dolayısıyla ithal eden
hiçbir modül kurulum anını seçemez. TARIFELER dışa verilmemiştir, ama kaydet çağıran her
modül onu değiştirir.
Aynı Kaydın Örnek Olarak Kurulması
İkinci sürümde kayıt bir tiptir ve durum örneğin özel alanında durur. Kaç örnek kurulacağına modül karar vermez; bunu çağıran belirler.
// ornek/tarifeler/standart.mjs — standart tarife tanimi export const STANDART = { ad: "standart", taban: () => 3900 };
// ornek/kayit.mjs — her cagri bagimsiz bir kayit ornegi uretir import { STANDART } from "./tarifeler/standart.mjs"; export class TarifeKaydi { #tarifeler = new Map(); kaydet(tarifeNesnesi) { this.#tarifeler.set(tarifeNesnesi.ad, tarifeNesnesi); return this; } tarife(ad) { const t = this.#tarifeler.get(ad); if (t === undefined) throw new RangeError(`bilinmeyen tarife: ${ad}`); return t; } adlar() { return [...this.#tarifeler.keys()]; } } export const yeniKayit = () => new TarifeKaydi().kaydet(STANDART);
Uygulamanın tek kayıtla çalışması gereksinimi ortadan kalkmıyor; yer değiştiriyor. Bileşim kökü
yeniKayit çağrısını bir kez yapar ve elde ettiği örneği kullanacak modüllere verir. Yani
“tek örnek” kararı nesnenin kendisinden bileşim köküne taşınmıştır.
Aynı Sınamaların İki Sürüme Uygulanması
İki sürümün dışa verdiği yöntem adları aynıdır: kaydet, tarife, adlar. Bu yüzden aynı
sınama kümesi ikisine de uygulanabilir. Sınamalar kayıt sağlayıcısını parametre olarak alır;
tekil sürümde sağlayıcı her çağrıda aynı modülü, örnek sürümünde her çağrıda yeni bir örnek
döndürür.
// sinamalar.mjs — dort sinama; her biri bir kayit saglayicisi alir import assert from "node:assert/strict"; export const SINAMALAR = [ ["yuklenen tarife sayisi", (kayitAl) => assert.equal(kayitAl().adlar().length, 1)], ["yeni tarife kaydedilir", (kayitAl) => { const k = kayitAl(); k.kaydet({ ad: "indirimli", taban: () => 3200 }); assert.equal(k.tarife("indirimli").taban(), 3200); }], ["kayit yalniz yuklenenleri icerir", (kayitAl) => assert.equal(kayitAl().adlar().length, 1)], ["bilinmeyen tarife hata verir", (kayitAl) => assert.throws(() => kayitAl().tarife("indirimli"), RangeError)], ];
// duz.test.mjs — dort sinama tanimlandigi sirada iki surume uygulanir import test from "node:test"; import { SINAMALAR } from "./sinamalar.mjs"; import * as tekilKayit from "./tekil/kayit.mjs"; import { yeniKayit } from "./ornek/kayit.mjs"; const SURUM = [["tekil", () => tekilKayit], ["ornek", () => yeniKayit()]]; for (const [surum, kayitAl] of SURUM) for (const [ad, sinama] of SINAMALAR) test(`${surum}: ${ad}`, () => sinama(kayitAl));
node --test --test-reporter=tap duz.test.mjs 2>&1 | grep -E '^(not ok|# (pass|fail))'
not ok 3 - tekil: kayit yalniz yuklenenleri icerir not ok 4 - tekil: bilinmeyen tarife hata verir # pass 6 # fail 2
Sekiz sınamanın ikisi başarısız ve ikisi de tekil sürüme ait. Örnek sürümünde dördün dördü
geçti. Başarısız iki sınamanın kendi içinde bir kusuru yok: üçüncü sınama kayıtta yalnız
yüklenen tarifelerin bulunmasını, dördüncüsü bilinmeyen bir tarifenin hata vermesini bekliyor.
İkisini de bozan şey ikinci sınamanın çalışmış olmasıdır — ikinci sınama kayda indirimli
tarifesini yazdı ve o yazma süreç boyunca kaldı.
Bu, Bağlaşım Türleri dersindeki ortak bağlaşımın tanımının aynısıdır: bir birimin davranışı, onu hiç ithal etmemiş bir birim tarafından değiştirilebiliyor. Üçüncü sınama ikinci sınamayı ithal etmez, adını bilmez, sırasını seçmez; buna karşın sonucu ona bağlıdır.
Sıranın Değiştirilmesi
Bağın gerçekten sıraya bağlı olduğu, aynı dört sınama ters sırada koşturularak sınanır.
// ters.test.mjs — ayni dort sinama ters sirada kosar import test from "node:test"; import { SINAMALAR } from "./sinamalar.mjs"; import * as tekilKayit from "./tekil/kayit.mjs"; import { yeniKayit } from "./ornek/kayit.mjs"; const SURUM = [["tekil", () => tekilKayit], ["ornek", () => yeniKayit()]]; for (const [surum, kayitAl] of SURUM) for (const [ad, sinama] of [...SINAMALAR].reverse()) test(`${surum}: ${ad}`, () => sinama(kayitAl));
node --test --test-reporter=tap ters.test.mjs 2>&1 | grep -E '^(not ok|# (pass|fail))'
not ok 4 - tekil: yuklenen tarife sayisi # pass 7 # fail 1
Başarısız sınama sayısı 2’den 1’e indi ve başarısız olan sınama değişti: düz sırada üçüncü ve dördüncü sınamalar, ters sırada birinci sınama çöktü. Sınamaların metnine, kayıt modüllerine ve tarife tanımına dokunulmadı; değişen tek şey sıra oldu. Frontend Kalitesi kursunda tanımlanan kırılgan test tam olarak budur ve sayısı tekil sürümde 2, örnek sürümünde 0’dır.
Örnek sürümü iki koşumda da dört sınamanın dördünü geçti, çünkü her sınama kendi kaydını kurar ve bitince bırakır.
Sıfırlamanın Bedeli
Tekil sürüm sınanabilir hâle getirilebilir; bunun için durumu sıfırlayan bir yöntem ve her sınamadan önce onu çağıran bir adım gerekir.
// sifirlamali.test.mjs — tekil surum her sinamadan once sifirlanir import test, { beforeEach } from "node:test"; import { SINAMALAR } from "./sinamalar.mjs"; import * as tekilKayit from "./tekil/kayit.mjs"; beforeEach(() => tekilKayit.sifirla()); for (const [ad, sinama] of SINAMALAR) test(`tekil: ${ad}`, () => sinama(() => tekilKayit));
node --test --test-reporter=tap sifirlamali.test.mjs 2>&1 | grep -E '^(not ok|# (pass|fail))' for d in tekil/kayit.mjs ornek/kayit.mjs; do echo "$d: $(grep -c 'export ' $d) disa acilan ad" done
# pass 4 # fail 0 tekil/kayit.mjs: 4 disa acilan ad ornek/kayit.mjs: 2 disa acilan ad
Dördün dördü geçti; çözüm işliyor. Bedeli iki sayıda. Birincisi dışa açılan ad sayısıdır: tekil
sürüm dört ad veriyor, örnek sürümü iki. Fazladan çıkan sifirla adı üretimde hiçbir yerde
çağrılmaz; yalnız sınamalar için vardır. Temiz Kod kursundaki en dar görünürlük ilkesine göre bu
ad ölü yüzeydir ve üretim kodundan da çağrılabilir olması yeni bir hata biçimi açar.
İkinci bedel sayının kendisinde değil, sürdürülmesinde. Sıfırlama listesi elle tutulur: kayda
ikinci bir küresel alan eklendiğinde sifirla de düzenlenmelidir, yoksa sınamalar yeniden
sıraya bağlı hâle gelir ve bu bağ ancak sıra değiştiğinde görünür. Örnek sürümünde böyle bir
liste yoktur, çünkü sıfırlanacak durum yeni örnekle birlikte zaten yoktur.
Tekil Ne Zaman Uygulanır
Ölçüler kalıbı tümden yasaklamıyor, uygulanabildiği bağlamı daraltıyor. Tekil, tuttuğu durum süreç boyunca değişmiyorsa ölçüleri bozmaz: sabit bir tablo, bir kez okunan bir yapılandırma ya da değiştirilemez bir eşleme modül düzeyinde durabilir, çünkü sınamalar arasında taşınacak bir değişiklik yoktur. Ölçüleri bozan şey tekillik değil, tekil örneğin değişebilir olmasıdır.
Kayıt bu ölçüte uymuyor: kaydet yöntemi durumu değiştirir. Kitaplıkta tekil olarak
bırakılabilecek şey tarife nesnelerinin kendisidir — dondurulmuş, davranışı sabit nesneler —
kayıt değil.
Özet
- Tekil, tek örneği nesnenin kendi sorumluluğuna yazar ve ona küresel erişim verir; modül sistemi olan bir dilde modül düzeyindeki durum bu kalıbı zaten gerçekleştirir.
- Aynı dört sınama iki sürüme uygulandığında tekil sürümde 2 sınama başarısız oldu, örnek sürümünde 4 sınamanın dördü geçti.
- Başarısızlığın nedeni sınamaların kendisi değil sırasıdır: sınamalar ters sırada koşturulunca başarısız sayısı 2’den 1’e indi ve başarısız olan sınama değişti.
- Tekil sürüm sıfırlama yöntemiyle sınanabilir hâle geldi ve dört sınamanın dördü geçti; bedel dışa açılan ad sayısının 2’den 4’e çıkması ve elle sürdürülen bir sıfırlama listesidir.
- Tekil, tuttuğu durum değiştirilemez olduğunda ölçüleri bozmaz; bozan şey tekillik değil, tek örneğin değişebilir olmasıdır.
Sonraki Adım
Örnek sürümü sınamaları geçirdi, ama bir soruyu açıkta bıraktı: kaydı kullanan modül o örneği
nereden alacak. Modülün içinde yeniKayit çağrılırsa her modül kendi kaydını kurar ve tek örnek
gereksinimi çiğnenir; modül tekil sürümü ithal ederse ölçüler geri gelir. Sonraki ders bu
soruyu yaratım sorumluluğunun taşınması olarak ele alır: bir modülün kendi bağımlılığını
kurduğu sürümle bağımlılığı dışarıdan aldığı sürümde ücret modülünün doğrudan bağ sayısını ve
ithal kapanışının boyutunu karşılaştırır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.