İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat