İçeriğe geç
academia.sh

Ders 06 / 21

Nesne Kabı ve Cepheler

Nesne kabında kayıt, tekil kayıt ve otomatik çözümleme, aynı ada ikinci kaydın ilkini sessizce ezmesi ve cephenin her çağrıda nesneyi kaptan çözmesi.

Yayın Güncelleme

Bu derste

  • Nesne kabının kayıt, tekil kayıt ve otomatik çözümleme olarak üç yoldan nesne kurduğunu okur
  • Aynı ada ikinci kaydın ilkini hata vermeden ezdiğini ve ezilen gerçekleştirimin bağlanmadı sınıfına düştüğünü ölçer
  • Tek kayıtlı kapta aynı gerçekleştirimin koştu sınıfına düştüğünü karşılaştırır
  • Cephenin her çağrıda nesneyi kaptan çözdüğünü ve tekil kaydın yeniden bağlamayı gizlediğini ölçer
İçindekiler

Önceki ders, eylemin önündeki katmanlardan birinin yanıt vermesiyle zincirin geri kalanının atlandığını gösterdi. Katmanlar, denetleyiciler ve onların bağımlılıkları ise zincirin dışında bir yerde kurulur. Çerçevede bu yer nesne kabıdır (object container): ad verilerek istenen nesneyi kuran ve ona bir bağımlılık gerekiyorsa onu da kurup veren bir kayıt defteri. Bir ad iki kez kaydedilirse kabın hangi kaydı seçeceği, bu dersin ölçeceği şeydir.

Nesne Kabı Üç Yoldan Nesne Kurar

Bu kursta nesne kabı daha önce iki kez göründü; burada üç yolunu da açıyoruz. İlk yol kayıt’tır (binding): bagla() bir ada bir tarif, yani nesneyi kuran bir fonksiyon ilişkilendirir. İkinci yol tekil kayıt’tır (singleton): tarif ilk çözümde bir kez çalışır ve ortaya çıkan nesne saklanır, sonraki bütün al() çağrıları aynı nesneyi döndürür. Üçüncü yol otomatik çözümleme’dir (autowiring): ad kayıtlı değilse kap, sınıfın yapıcı parametrelerinin tiplerine ReflectionClass ile bakar ve her birini yine kaptan alır.

Bağımlılığın neden dışarıdan verildiği M12/K02’deki Bağımlılık Enjeksiyonu dersinde anlatıldı: sınıf bağımlılığını kendisi kurmaz, dışarıdan alır. Orada bağımlılığı elle veren bizdik. Burada onu veren kaptır ve bunun bedeli, kimin neyi kurduğunun bir kayıt defterine taşınmasıdır. Çerçeve kursunun öteki yüzü olan M08/K06’da, bu devredilen denetim denetimin ters çevrilmesi olarak adlandırılır ve orada bir davranışın hangi kaynaktan geldiği aranır. Burada aranan başka bir şeydir: kayıtlı bir parçanın bu istekte gerçekten koşup koşmadığı.

kap.php kabı kurar. GunlukYazici arayüzünün iki gerçekleştirimi vardır: DosyaYazici ve BellekYazici. İkisi de yaz() çağrıldığında kendi adını koştu olarak günlüğe bildirir. bagla() her kaydı $tarifler dizisine ada göre yazar; dizi anahtarı ad olduğu için aynı ad varsa önceki kayıt sessizce ezilir. kayitlariYola() kaptaki kayıtlı parçaları yola koyar: yola, kabın bu istekte elindeki kayıtlardır. Cephe ve Yazici ise derste sonra kullanılacak: cephe, kaptaki bir nesneye statik bir adla ulaşmanın yoludur.

Üç yolun sırası da al() içinde görünür ve sıra önemlidir. Önce saklanan tekil nesneye bakılır, sonra kayıtlı tarife, en son otomatik çözümlemeye. Yani bir ad için tekil nesne varsa kayıt hiç sorulmaz, kayıt varsa sınıfın yapıcısı hiç okunmaz. Otomatik çözümleme bu yüzden varsayılan davranıştır: kaydedilmemiş her sınıf, yapıcı parametrelerinin tipleri çözülebildiği sürece çalışır. Parametre bir arayüzse ya da skaler bir değerse çözümleme durur, çünkü kap o adın hangi gerçekleştirime gideceğini bilemez. Arayüzü bir gerçekleştirime bağlamak kaydın gerçek işidir; GunlukYazici arayüzü bu yüzden kayıtsız çözülemez.

<?php
// kap.php — kayıt, tekil kayıt, otomatik çözümleme ve cephe
declare(strict_types=1);
require_once 'kostu.php';

interface GunlukYazici { public function yaz(string $mesaj): void; }

final class DosyaYazici implements GunlukYazici {
    public function yaz(string $mesaj): void { Gunluk::kostu('DosyaYazici'); }
}

final class BellekYazici implements GunlukYazici {
    public function yaz(string $mesaj): void { Gunluk::kostu('BellekYazici'); }
}

final class NesneKabi {
    private array $tarifler = [];
    private array $tekiller = [];
    public function bagla(string $ad, Closure $tarif, bool $tekil = false, string $parca = ''): void {
        $this->tarifler[$ad] = [$tarif, $tekil, $parca];      // aynı ad varsa sessizce ezilir
    }
    public function kayitlariYola(): void {
        foreach ($this->tarifler as [, , $parca]) Gunluk::yola($parca);
    }
    public function al(string $ad): object {
        if (isset($this->tekiller[$ad])) return $this->tekiller[$ad];
        if (isset($this->tarifler[$ad])) {
            [$tarif, $tekil] = $this->tarifler[$ad];
            $nesne = $tarif($this);
            if ($tekil) $this->tekiller[$ad] = $nesne;
            return $nesne;
        }
        $bagimliliklar = [];
        foreach ((new ReflectionClass($ad))->getConstructor()?->getParameters() ?? [] as $p) {
            $bagimliliklar[] = $this->al($p->getType()->getName());
        }
        return new $ad(...$bagimliliklar);
    }
}

abstract class Cephe {
    private static NesneKabi $kap;
    public static function kapla(NesneKabi $kap): void { self::$kap = $kap; }
    abstract protected static function ad(): string;
    public static function __callStatic(string $yontem, array $arguman): mixed {
        return self::$kap->al(static::ad())->$yontem(...$arguman);      // her çağrıda kaptan çözülür
    }
}

final class Yazici extends Cephe {
    protected static function ad(): string { return GunlukYazici::class; }
}

Çekirdek kostu.php her derste aynı biçimde yeniden verilir.

<?php
// kostu.php — beklenen bir parça bu istekte koştu mu?
declare(strict_types=1);

final class Gunluk {
    public static array $yol = [];         // isteğin yoluna konan parçalar
    public static array $kostu = [];       // istek içinde çağrılanlar
    public static array $ertelenen = [];   // isteğin dışına bırakılanlar
    public static function yeniIstek(): void { self::$yol = self::$kostu = self::$ertelenen = []; }
    public static function yola(string $parca): void { self::$yol[] = $parca; }
    public static function kostu(string $parca): void { self::$kostu[] = $parca; }
    public static function ertele(string $parca): void { self::$ertelenen[] = $parca; }
}

function sinifla(string $parca): string {
    return match (true) {
        in_array($parca, Gunluk::$kostu, true) => 'koştu',
        in_array($parca, Gunluk::$ertelenen, true) => 'ertelendi',
        in_array($parca, Gunluk::$yol, true) => 'atlandı',
        default => 'bağlanmadı',
    };
}

ÇT18. Kapta GunlukYazici adıyla tek bir kayıt varsa, al() o kaydın tarifini çalıştırır ve dönen nesnenin yaz() çağrısı koştu olarak görünür. Aynı ada ikinci bir kayıt eklenirse $tarifler dizisindeki ilk kayıt silinir; çözümleme ikinci kaydı çalıştırır. İlk kaydın gerçekleştirimi yola hiç girmez, çünkü yola yalnız kapta kalan kayıtlar konur.

İkinci Kayıt İlkini Ezer

İki kap kurulur. İlkinde GunlukYazici adına önce DosyaYazici, sonra BellekYazici kaydedilir. İkincisinde yalnız DosyaYazici kaydedilir. Her kapta aynı istek yapılır: kayıtlar yola konur, GunlukYazici kaptan alınır ve yaz() çağrılır.

<?php
// kayit.php — aynı ada ikinci kayıt ilkini sessizce ezer
declare(strict_types=1);
require_once 'kap.php';

function olc(NesneKabi $kap): void {
    Gunluk::yeniIstek();
    $kap->kayitlariYola();
    $kap->al(GunlukYazici::class)->yaz('kaydedildi');
}

$iki = new NesneKabi();
$iki->bagla(GunlukYazici::class, fn() => new DosyaYazici(), parca: 'DosyaYazici');
$iki->bagla(GunlukYazici::class, fn() => new BellekYazici(), parca: 'BellekYazici');

$tek = new NesneKabi();
$tek->bagla(GunlukYazici::class, fn() => new DosyaYazici(), parca: 'DosyaYazici');

foreach (['iki-kayit' => $iki, 'tek-kayit' => $tek] as $ad => $kap) {
    olc($kap);
    foreach (['DosyaYazici', 'BellekYazici'] as $parca) {
        printf("%-10s %-13s %s\n", $ad, $parca, sinifla($parca));
    }
}
iki-kayit  DosyaYazici   bağlanmadı
iki-kayit  BellekYazici  koştu
tek-kayit  DosyaYazici   koştu
tek-kayit  BellekYazici  bağlanmadı

ÇT19. İki kayıtlı kapta BellekYazici koştu, ama DosyaYazici bağlanmadı: onu kaydeden satır dosyada duruyor ve hiçbir hata ya da uyarı çıkmadı. Tek kayıtlı kapta ise aynı DosyaYazici koştu. Fark, o kaydın dosyadaki varlığında değil, kapta kalıp kalmadığındadır. Kaydı yazan geliştirici, ilk satırın yürürlükte olduğunu varsayabilir; ama kapta yürürlükte olan, son yazılan kayıttır.

Bu sınıfın neden zor yakalandığı, kaydın doğasından gelir. Kayıtlar genellikle çerçevenin ve uygulamanın başlangıç dosyalarında, birbirinden uzak yerlerde durur: biri çerçevenin kendi kayıtları, biri uygulamanın, biri bir paketin. Aynı ada yazan ikisi arasında hangisinin son olacağı, dosyaların yüklenme sırasına bağlıdır. Her kayıt kendi başına doğru görünür; çakışma yalnız ikisi birlikte yüklendiğinde ortaya çıkar. Ölçümde bu çakışmayı iki ardışık satırla kurduk; gerçek bir uygulamada aralarında yüzlerce satır ve birkaç dosya olabilir.

Ölçümün buradan çıkardığı iş şudur: beklenen gerçekleştirimi ad vererek sormak. Kaydı okumak yetmez, çünkü okunan satır yürürlükte olan satır olmayabilir. Yola konan parçaların listesi, kapta kalan kayıtların listesidir; beklenen parça bu listede yoksa kayıt ezilmiştir ya da hiç yapılmamıştır ve ikisinin dışarıdan görünüşü aynıdır.

Bu dersin ölçü sınıfı bağlanmadı‘dır. Atlandı’dan farkı şudur: atlanan parça yoldaydı ve önündeki bir katman yanıt verdiği için çağrılmadı; bağlanmayan parça ise yola hiç girmedi, çünkü kaydı kapta yok. Günlükte bir izi olmayan şey, ne bir hata mesajıyla ne bir durum koduyla kendini belli eder. Bu yüzden bağlanmadı, karar sırasının en tehlikeli sınıfıdır ve onu görmek için beklenen parçayı geliştiricinin adlandırmış olması gerekir.

Sınırlayıcı Ölçüm

Aynı ada ikinci kaydın ilkini ezmesi her koşulda geçerli midir? Bu ölçüm iki şeyi birleştirir: tekil kayıt ve cephe. Yazici cephesi, Yazici::yaz() yazıldığında PHP’nin __callStatic sihirli yöntemini çalıştırır; bu yöntem kaptan GunlukYazici adını çözer ve çağrıyı elde edilen nesneye iletir. Cephe nesneyi saklamaz, her çağrıda kaptan yeniden alır.

Kap önce DosyaYazici ile tekil olarak kaydedilir. İlk çağrıdan sonra aynı ada BellekYazici kaydedilir, ardından cephe yeniden çağrılır.

<?php
// cephe.php — tekil kayıt, yeniden bağlamayı gizler
declare(strict_types=1);
require_once 'kap.php';

$kap = new NesneKabi();
$kap->bagla(GunlukYazici::class, fn() => new DosyaYazici(), tekil: true, parca: 'DosyaYazici');
Cephe::kapla($kap);

foreach (['ilk çağrı', 'yeniden bağladıktan sonra'] as $adim => $ad) {
    if ($adim === 1) {
        $kap->bagla(GunlukYazici::class, fn() => new BellekYazici(), parca: 'BellekYazici');
    }
    Gunluk::yeniIstek();
    $kap->kayitlariYola();
    Yazici::yaz('kaydedildi');
    printf("%s: Dosya %s, Bellek %s\n", $ad, sinifla('DosyaYazici'), sinifla('BellekYazici'));
}
ilk çağrı: Dosya koştu, Bellek bağlanmadı
yeniden bağladıktan sonra: Dosya koştu, Bellek atlandı

ÇT20. İlk çağrıda DosyaYazici koştu ve tekil kayıt gereği nesne saklandı. İkinci kayıt $tarifler içindeki tarifi gerçekten ezdi: BellekYazici yola girdi. Ama al(), tarife bakmadan önce saklanan tekil nesneye bakar ve onu bulduğu için eski nesneyi döndürdü. Sonuçta ikinci çağrıda DosyaYazici yine koştu ve yeniden bağlanan BellekYazici yolda olduğu hâlde atlandı. Önceki bölümdeki sonuç, yani ikinci kaydın kazanması, tekil kayıt henüz çözülmediyse geçerlidir; bir kez çözülmüşse saklanan nesne kazanır.

Bu sınırın bir sonucu daha var: tekil kayıtla bir test ya da bir isteğin ortasındaki yeniden bağlama, beklenen etkiyi göstermeyebilir. Yeniden bağlamadan sonra saklanan nesneyi de unutturmak gerekir; aksi hâlde yeni kayıt yürürlüğe girmiş görünür ama çözümleme hâlâ eskisini verir. Bu derste kullanılan kap bunu yapmaz ve bu bilerek böyle bırakılmıştır, çünkü sınırın görünmesi için kapta bir saklama noktası olması gerekir.

Bu ölçüm cephenin kendi başına bir sorun yaratmadığını da gösterir. Cephe her çağrıda kaptan sorar; yanlış nesneye giden şey cephe değil, kabın verdiği yanıttır. Cephe, bu karmaşayı statik bir ad arkasına gizlediği için sorunu görmeyi güçleştirir: Yazici::yaz() satırı hangi gerçekleştirimin koşacağını söylemez.

Özet

  • Nesne kabı kayıt, tekil kayıt ve otomatik çözümleme olarak üç yoldan nesne kurar; cephe kaptaki bir nesneye statik bir adla ulaşmanın yoludur.
  • Aynı ada ikinci kayıt ilkini hata ya da uyarı olmadan ezer; iki kayıtlı kapta DosyaYazici bağlanmadı, BellekYazici koştu.
  • Tek kayıtlı kapta aynı DosyaYazici koştu; fark kaydın dosyada olması değil, kapta kalmasıdır.
  • Tekil kayıt çözüldükten sonra yeniden bağlama saklanan nesneyi değiştirmez: DosyaYazici koştu, BellekYazici yoldaydı ama atlandı.
  • Bu dersin ölçü sınıfı bağlanmadı’dır: kapta kalmayan kayıt yola girmez ve günlükte hiçbir iz bırakmaz.

Sonraki Adım

Altı derstir çerçevenin parçalarını tek tek, elle kurduk: yaşam döngüsü, rota, denetleyici, ara katman, kap. Sıradaki konu, Görünüm ve Veri, bunların ürettiği yanıtın gövdesine geçer ve ilk dersi, Şablon Motoru, şu soruyu soruyor: şablon her seferinde hangi adımı yeniden koşturuyor ve o adımın sonucu saklanıyorsa bir sonraki istek ne gö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