Ders 02 / 12
Kalıtım ve Soyut Sınıflar
Bir üst sınıfın içi değiştiğinde alt sınıfın sonucunun örtük biçimde kayması, soyut yöntem eklemenin alt sınıfları durdurması ve sayacı bileşimle kurmanın aynı değişikliği yerinde bırakması.
Yayın Güncelleme
Bu derste
- Soyut bir sınıfın örneklenemediğini ve final yöntemin yeniden tanımlanamadığını gösterir
- Üst sınıfın iç değişikliğinin alt sınıfın sayacını örtük biçimde değiştirdiğini ölçer
- Aynı sayacı bileşimle kurarak değişikliğin yerinde kaldığını doğrular
İçindekiler
Sınıflar ve Nesneler dersinde bir sınıfın içindeki ad değişti ve kullanan etkilenmedi, çünkü araya görünürlük girmişti. Kalıtımda araya hiçbir şey girmez: alt sınıf, üst sınıfın yöntemlerini ve korunan özelliklerini doğrudan çağırır. Bu yüzden soru değişir. Üst sınıfın içi değiştiğinde alt sınıf ne yaşıyor?
Alt Sınıf Kurmak
Kalıtım (inheritance), bir sınıfın başka bir sınıfın özelliklerini ve yöntemlerini devralmasıdır. Devralan sınıf alt sınıf (subclass), devralınan üst sınıf (parent class) olur. PHP’de extends sözcüğü bunu bildirir. protected görünürlüğü, bir üyeyi sınıfın kendisine ve alt sınıflara açar, dışarıya kapalı tutar. Alt sınıf bir yöntemi aynı adla yazarsa yöntemi yeniden tanımlar (overriding); üst sınıfın sürümüne yine de parent:: ile ulaşılır.
<?php // Liste.php — protected bir ozellik ve iki yontem tasiyan ust sinif class Liste { protected array $ogeler = []; public function ekle(string $oge): void { $this->ogeler[] = $oge; } public function hepsiniEkle(array $ogeler): void { foreach ($ogeler as $oge) { $this->ekle($oge); } } public function say(): int { return count($this->ogeler); } }
<?php // SayanListe.php — ekle() yontemini yeniden tanimlayan alt sinif class SayanListe extends Liste { public int $sayi = 0; public function ekle(string $oge): void { $this->sayi++; parent::ekle($oge); } }
<?php // liste_kullan.php — alt sinif ust sinifin yontemini kullanir; protected ozellik disaridan kapali require 'Liste.php'; require 'SayanListe.php'; $liste = new SayanListe(); $liste->hepsiniEkle(['a', 'b', 'c']); echo $liste->sayi, ' ', $liste->say(), "\n"; try { echo count($liste->ogeler); } catch (Error $hata) { echo get_class($hata), "\n"; }
3 3 Error
NM5. hepsiniEkle() alt sınıfta yazılmadı, devralındı; her öğe için ekle() çağrıldı ve SayanListe::ekle() her çağrıda sayacı artırdı: 3 3. protected özellik alt sınıfa açıktı ama dışarıya kapalı kaldı. Burada dikkat çeken şey, alt sınıfın sayacının üst sınıfın bir iç ayrıntısına dayanmasıdır: hepsiniEkle()’nin tek tek ekle() çağırması.
Soyut Sınıf ve final
Soyut sınıf (abstract class), örneği kurulamayan ve gövdesiz yöntem taşıyabilen sınıftır; alt sınıflar o yöntemleri yazmak zorundadır. Aşağıdaki sınıf bir yöntemi gövdesiz bildirir ve bir yöntemi gövdesiyle verir.
<?php // Rapor.php — govdesiz bir yontem ve ona dayanan govdeli bir yontem abstract class Rapor { abstract public function baslik(): string; public function yaz(): string { return strtoupper($this->baslik()); } }
<?php // Gunluk.php — soyut yontemi yazan somut alt sinif class Gunluk extends Rapor { public function baslik(): string { return 'gunluk'; } }
<?php // rapor_kullan.php — somut alt sinif kurulur, soyut sinif kurulamaz require 'Rapor.php'; require 'Gunluk.php'; echo (new Gunluk())->yaz(), "\n"; try { new Rapor(); } catch (Error $hata) { echo get_class($hata), "\n"; }
GUNLUK Error
Bir yöntem final ile işaretlenirse alt sınıflar onu yeniden tanımlayamaz. Dosya yüklenir yüklenmez durur. Aşağıdaki betik bir final yöntemi yeniden tanımlayan iki sınıfı bir dosyaya yazar ve çıkış kodunu basar.
# final.sh — final yontemi yeniden tanimlayan sinif yuklenemez cat > Sabit.php <<'PHP' <?php class Taban { final public function kimlik(): string { return 'taban'; } } class Alt extends Taban { public function kimlik(): string { return 'alt'; } } PHP php -d display_errors=0 Sabit.php; echo "cikis=$?"
cikis=255
NM6. new Gunluk() çalıştı ve Rapor::yaz() alt sınıfın baslik() yöntemini kullandı. new Rapor() ve final yöntemi yeniden tanımlayan dosya ise sinyal verdi: ilki Error fırlattı, ikincisi çıkış 255 ile durdu. Soyut sınıf ve final, kalıtımın iki ucudur: biri alt sınıfa yazma zorunluluğu getirir, öteki yazma yetkisini alır.
Üst Sınıfın Yapıcısı
Alt sınıf kendi yapıcısını yazarsa üst sınıfın yapıcısı kendiliğinden çalışmaz. Üst sınıfın kurduğu durumun hazır olması için parent::__construct() açıkça çağrılır. Çağrı unutulursa üst sınıfın özellikleri kurulmadan kalır.
<?php // Kisi.php — ust sinif yapicisi, cagrilan ve unutulan alt sinif class Kisi { public function __construct(protected string $ad) {} public function selam(): string { return 'merhaba ' . $this->ad; } } class Calisan extends Kisi { public function __construct(string $ad, private int $no) { parent::__construct($ad); } public function kart(): string { return $this->selam() . ' #' . $this->no; } } class Unutan extends Kisi { public function __construct(private int $no) {} public function oku(): string { return $this->ad; } } echo (new Calisan('ali', 7))->kart(), "\n"; try { echo (new Unutan(1))->oku(); } catch (Error $e) { echo get_class($e), "\n"; }
merhaba ali #7 Error
Calisan üst sınıfın yapıcısını çağırdığı için ad kuruldu. Unutan çağırmadığı için ad hiç yazılmadı ve okunduğunda Error doğdu. Kalıtımda yapıcı zinciri yalnız yazılırsa işler.
Yeniden Tanımlamada İmza ve final Sınıf
Alt sınıf bir yöntemi yeniden tanımlarken üst sınıfın imzasıyla uyumlu kalmak zorundadır; üst sınıfı tipli bir değişkende tutan kod alt sınıfla da aynı çağrıyı yapabilmelidir. Zorunlu bir parametre eklemek bu uyumu bozar. Bir sınıfın hiç genişletilmemesi isteniyorsa sınıf final ile işaretlenir; genişleten dosya yüklenir yüklenmez durur.
# imza.sh — zorunlu parametre eklemek ve final sinifi genisletmek bildirilemez yapar cat > Imza.php <<'PHP' <?php class Taban { public function ekle(string $oge): void {} } class Alt extends Taban { public function ekle(string $oge, int $adet): void {} } PHP cat > KapaliSinif.php <<'PHP' <?php final class Kapali {} class Acan extends Kapali {} PHP php -d display_errors=0 Imza.php; echo "imza cikis=$?" php -d display_errors=0 KapaliSinif.php; echo "final cikis=$?"
imza cikis=255 final cikis=255
İki durumda da hata yürütme sırasında değil, sınıf bildirimi sırasında çıktı. final yöntem tek bir davranışı, final sınıf bütün davranışı alt sınıfların elinden alır.
Kalıtım Ne Zaman Yerindedir
Kalıtım iki sınıf arasında “bir tür” ilişkisi varsa yerindedir: Calisan bir Kisi’dir, Gunluk bir Rapor’dur. Yalnız kodu paylaşmak için kurulan kalıtım, alt sınıfı üst sınıfın bütün yöntemlerine ve gelecekteki bütün değişikliklerine bağlar. Bu ders boyunca ölçülen şey de budur: üst sınıfın içi değişince bağlı alt sınıflar bundan etkilenir. Paylaşılan kod bir ilişkiyi değil bir yeteneği anlatıyorsa, arayüz ya da trait daha az bağ kurar; bunlar sıradaki iki derstir.
Ölçüm Çekirdeği
Bu dersin ölçümü yayilim.php dosyasını kullanır; dosya Sınıflar ve Nesneler dersinde satır satır açıklanan çekirdektir ve her dersin çalışma dizininde yeniden yazılır.
<?php // yayilim.php — bir degisikligin nereye ulastigini olcen yardimcilar function yaz(string $dizin, array $dosyalar): void { foreach ($dosyalar as $ad => $icerik) { if ($icerik === null) { continue; // null: dosya silindi } $yol = "$dizin/$ad"; if (!is_dir(dirname($yol))) { mkdir(dirname($yol), 0777, true); } file_put_contents($yol, $icerik); } } function oku(string $dizin): array { $dosyalar = []; $gezgin = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dizin, FilesystemIterator::SKIP_DOTS) ); foreach ($gezgin as $dosya) { $ad = substr($dosya->getPathname(), strlen($dizin) + 1); $dosyalar[$ad] = file_get_contents($dosya->getPathname()); } return $dosyalar; } function kostur(string $dizin): array { $komut = 'cd ' . escapeshellarg($dizin) . ' && php -d display_errors=0 main.php 2>/dev/null'; exec($komut, $satirlar, $kod); return [$kod, implode("\n", $satirlar)]; } function olc(string $ad, array $taban, array $degisiklik, array $baglanti = []): string { if (array_key_exists('main.php', $degisiklik)) { throw new LogicException("$ad: kullanan duzenlenmez"); } yaz("olcum/$ad/once", $taban); yaz("olcum/$ad/sonra", array_merge($taban, $degisiklik)); [$kodOnce, $ciktiOnce] = kostur("olcum/$ad/once"); [$kodSonra, $ciktiSonra] = kostur("olcum/$ad/sonra"); if ($kodOnce !== 0) { throw new LogicException("$ad: taban calismadi"); } $sinif = match (true) { $kodSonra !== 0 => 'kiran', $ciktiOnce !== $ciktiSonra => 'ortuk', array_intersect(array_keys($degisiklik), $baglanti) !== [] => 'baglantida', default => 'yerinde', }; printf( "%-18s %d dosya cikis=%-3d cikti %-6s -> %s\n", $ad, count($degisiklik), $kodSonra, $ciktiOnce === $ciktiSonra ? 'ayni' : 'farkli', $sinif ); return $sinif; }
Üst Sınıfın İçi Değişir
İlk gözlem yukarıdaki Liste ve SayanListe ikilisini kullanır. Üst sınıfın hepsiniEkle() yöntemi artık tek tek ekle() çağırmaz, diziyi array_merge() ile bir seferde birleştirir. Alt sınıf ve kullanan main.php aynı kalır. Üst sınıfın dışarıya verdiği sözleşme de değişmez: iki yöntem aynı adı ve imzayı taşır.
<?php // kalitim.php — ust sinif toplu eklemede tekli ekleme yontemini artik cagirmaz require_once 'yayilim.php'; olc('ust-sinif', [ 'liste.php' => <<<'PHP' <?php class Liste { protected array $ogeler = []; public function ekle(string $oge): void { $this->ogeler[] = $oge; } public function hepsiniEkle(array $ogeler): void { foreach ($ogeler as $oge) { $this->ekle($oge); } } } PHP, 'main.php' => <<<'PHP' <?php require 'liste.php'; class SayanListe extends Liste { public int $sayi = 0; public function ekle(string $oge): void { $this->sayi++; parent::ekle($oge); } } $liste = new SayanListe(); $liste->hepsiniEkle(['a', 'b', 'c']); echo $liste->sayi, "\n"; PHP, ], [ 'liste.php' => <<<'PHP' <?php class Liste { protected array $ogeler = []; public function ekle(string $oge): void { $this->ogeler[] = $oge; } public function hepsiniEkle(array $ogeler): void { $this->ogeler = array_merge($this->ogeler, $ogeler); } } PHP, ]); foreach (['once', 'sonra'] as $asama) { [$kod, $cikti] = kostur("olcum/ust-sinif/$asama"); echo $asama, ': ', $cikti, "\n"; }
ust-sinif 1 dosya cikis=0 cikti farkli -> ortuk once: 3 sonra: 0
NM7. Çıkış kodu 0, ama çıktı değişti: sayaç 3 iken 0 oldu. Alt sınıf yalnız ekle() yöntemini yeniden tanımlamıştı; üst sınıf artık ekle() çağırmadığı için alt sınıfın yöntemi hiç çalışmadı. Kullanan kodda hata yok, uyarı yok, yalnızca yanlış bir sayı var. Bu, kırılgan üst sınıf sorunudur (fragile base class problem): alt sınıf, üst sınıfın sözleşmesine değil iç işleyişine güvenmişti ve iç işleyiş değişti. Bu dersin ölçü sınıfı örtüktür. M08/K02 çağrılan yöntemi hangi tipin seçtiğini ölçmüştü; burada ölçülen, üst sınıftaki değişikliğin alt sınıfa ulaşıp ulaşmadığıdır ve ulaştı.
Soyut Yöntem Eklemek
Karşıt ölçüm değişikliği üst sınıfın içinden sözleşmesine taşır. Soyut bir sınıfa yeni bir abstract yöntem eklenir. Daha önce yazılmış Gunluk’un benzeri bir alt sınıf bu yöntemi bilmez.
<?php // soyut.php — soyut ust sinifa yeni bir abstract yontem eklenir require_once 'yayilim.php'; olc('soyut-yontem', [ 'rapor.php' => <<<'PHP' <?php abstract class Rapor { abstract public function baslik(): string; public function yaz(): string { return strtoupper($this->baslik()); } } PHP, 'main.php' => <<<'PHP' <?php require 'rapor.php'; class Gunluk extends Rapor { public function baslik(): string { return 'gunluk'; } } echo (new Gunluk())->yaz(), "\n"; PHP, ], [ 'rapor.php' => <<<'PHP' <?php abstract class Rapor { abstract public function baslik(): string; abstract public function alt(): string; public function yaz(): string { return strtoupper($this->baslik()); } } PHP, ]);
soyut-yontem 1 dosya cikis=255 cikti farkli -> kiran
NM8. Aynı kullanan, aynı alt sınıf, aynı tek dosyalık değişiklik; bu kez kullanan durdu. Soyut yöntem bir alt sınıfa “yazmak zorundasın” der ve alt sınıf yazmadığında sınıf bildirilemez. Çıkış kodu 0 değil, sınıf kıran. Sözleşme gövdesiz olduğu için dil değişikliği yakalar; üst sınıfın iç değişikliğini yakalayacak bir sözleşme yoktu.
Sınırlayıcı Ölçüm
Örtük sonucun nedeni kalıtımın kendisi mi, yoksa sayacın üst sınıfın içine bağlanması mı? Aynı sayaç bileşimle (composition) kurulduğunda ölçüm bunu ayırır. Bileşimde sınıf başka bir sınıfı genişletmez, onun bir örneğini taşır ve kendi iki yönteminde sayar. Üst sınıfta yapılan değişiklik ise öncekiyle aynıdır: hepsiniEkle() artık ekle() çağırmaz.
<?php // bilesim.php — sayac kalitimla degil bilesimle kurulur; ayni ust sinif degisikligi require_once 'yayilim.php'; olc('bilesim', [ 'liste.php' => <<<'PHP' <?php class Liste { protected array $ogeler = []; public function ekle(string $oge): void { $this->ogeler[] = $oge; } public function hepsiniEkle(array $ogeler): void { foreach ($ogeler as $oge) { $this->ekle($oge); } } } PHP, 'main.php' => <<<'PHP' <?php require 'liste.php'; class SayanListe { public int $sayi = 0; private Liste $ic; public function __construct() { $this->ic = new Liste(); } public function ekle(string $oge): void { $this->sayi++; $this->ic->ekle($oge); } public function hepsiniEkle(array $ogeler): void { $this->sayi += count($ogeler); $this->ic->hepsiniEkle($ogeler); } } $liste = new SayanListe(); $liste->hepsiniEkle(['a', 'b', 'c']); echo $liste->sayi, "\n"; PHP, ], [ 'liste.php' => <<<'PHP' <?php class Liste { protected array $ogeler = []; public function ekle(string $oge): void { $this->ogeler[] = $oge; } public function hepsiniEkle(array $ogeler): void { $this->ogeler = array_merge($this->ogeler, $ogeler); } } PHP, ]); foreach (['once', 'sonra'] as $asama) { [$kod, $cikti] = kostur("olcum/bilesim/$asama"); echo $asama, ': ', $cikti, "\n"; }
bilesim 1 dosya cikis=0 cikti ayni -> yerinde once: 3 sonra: 3
NM9. Üst sınıfın değişikliği aynı, kullanan aynı; sonuç 3 kaldı. Bileşimde sayaç, üst sınıfın hangi yöntemi hangisine çağırdığına bakmaz: kendi iki yönteminde sayar ve üst sınıfa yalnız sözleşmeyi, yani iki yöntemin adını ve imzasını kullanarak ulaşır. Aynı iç değişiklik kalıtımda örtük, bileşimde yerinde kaldı. Ölçülen ayrım dilin değil tasarımın: kalıtım alt sınıfı üst sınıfın içine bağlar, bileşim bağlamaz. Bedel de buradan gelir: SayanListe artık bir Liste değildir.
Özet
extendsbir alt sınıf kurar,protectedüyeyi alt sınıfa açıp dışarıya kapalı tutar,parent::üst sınıfın yöntemine ulaşır.- Soyut sınıf örneklenemez ve alt sınıfa gövdesiz yöntemleri yazma zorunluluğu getirir;
finalyöntem yeniden tanımlanamaz. - Üst sınıf toplu eklemede
ekle()çağırmayı bırakınca alt sınıfın sayacı3’ten0’a düştü; bu dersin ölçü sınıfı örtüktür. - Soyut sınıfa
abstractyöntem eklemek aynı kullananı durdurdu, kıran sınıf; sözleşme gövdesiz olduğu için dil yakalar. - Sayaç bileşimle kurulunca aynı üst sınıf değişikliği yerinde kaldı:
3.
Sonraki Adım
Soyut yöntem gövdesiz bir sözleşme getirdi ve sözleşmeye yöntem eklemek alt sınıfı durdurdu. Arayüzler dersi bu sözleşmeyi kalıtımdan ayırıyor: bir arayüze yöntem eklenince onu gerçekleştiren sınıflar ne yaşıyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.