Ders 03 / 12
Arayüzler
Bir arayüze yöntem eklendiğinde onu gerçekleştiren sınıfın bildirilememesi, tip olarak arayüz ve instanceof, ve aynı eklemenin gövdeli bir soyut sınıfta yerinde kalması.
Yayın Güncelleme
Bu derste
- Bir arayüz tanımlar, birden çok arayüzü bir sınıfta gerçekleştirir ve arayüzü parametre tipi olarak kullanır
- instanceof ile bir nesnenin hangi arayüzü gerçekleştirdiğini sorgular
- Arayüze yöntem eklenmesinin gerçekleştiren sınıfı durdurduğunu çıkış koduyla ölçer
- Aynı ekleme gövdeli bir soyut sınıfta yapılınca değişikliğin yerinde kaldığını ve bunun bedelini gösterir
İçindekiler
Kalıtım dersinde soyut sınıfa yeni bir abstract yöntem eklemek alt sınıfı durdurmuştu ve bunun nedeni sözleşmenin gövdesiz olmasıydı. Arayüz bu sözleşmenin yalın hâlidir: yalnız imza taşır, hiçbir gövde taşımaz. Soru, bir sözleşmeye yöntem eklenince onu gerçekleştiren sınıfların ne yaşadığıdır.
Arayüz ve Gerçekleştirim
Arayüz (interface), bir sınıfın hangi yöntemleri sunması gerektiğini bildirir ve hiçbirini yazmaz. Arayüzü uygulayan sınıf implements ile bildirimde bulunur ve bütün yöntemleri yazmak zorundadır; buna gerçekleştirim (implementation) denir. Bir sınıf birden çok arayüzü gerçekleştirebilir, virgülle ayrılmış olarak.
<?php // Odenebilir.php — yalniz bir imza tasiyan arayuz interface Odenebilir { public function tutar(): int; }
<?php // Yazdirilabilir.php — ikinci bir arayuz interface Yazdirilabilir { public function metin(): string; }
<?php // Fatura.php — iki arayuzu birden gerceklestiren sinif class Fatura implements Odenebilir, Yazdirilabilir { public function tutar(): int { return 120; } public function metin(): string { return 'fatura'; } }
<?php // Maas.php — yalniz Odenebilir'i gerceklestiren sinif class Maas implements Odenebilir { public function tutar(): int { return 300; } }
Arayüz bir tip olarak da kullanılır. Aşağıdaki fonksiyon Odenebilir bekler ve bu arayüzü gerçekleştiren herhangi bir sınıfın nesnesini kabul eder. instanceof işleci bir nesnenin verilen arayüzü gerçekleştirip gerçekleştirmediğini sorar.
<?php // arayuz_kullan.php — arayuz parametre tipi olarak ve instanceof ile require 'Odenebilir.php'; require 'Yazdirilabilir.php'; require 'Fatura.php'; require 'Maas.php'; function toplamTutar(Odenebilir ...$kalemler): int { $toplam = 0; foreach ($kalemler as $kalem) { $toplam += $kalem->tutar(); } return $toplam; } echo toplamTutar(new Fatura(), new Maas()), "\n"; var_dump(new Fatura() instanceof Yazdirilabilir); var_dump(new Maas() instanceof Yazdirilabilir);
420 bool(true) bool(false)
NM10. toplamTutar() iki ayrı sınıfın nesnesini aldı, çünkü ikisi de Odenebilir’i gerçekleştiriyordu: 120 + 300. Fatura iki arayüze, Maas yalnız birine uyuyor; instanceof bu farkı true ve false ile söyledi. Arayüz imzayı denetler, anlamı denetlemez: tutar() negatif bir sayı da döndürebilirdi ve dil sesini çıkarmazdı. M08/K03 bir davranışın garantisini kimin verdiğini sormuştu; burada arayüz bir söz değil, bir sınırdır. İmzanın dışındaki anlamı ölçmek Ortak Arayüz Standartları dersine kalıyor.
Arayüz Sabiti ve Arayüzün Arayüzü Genişletmesi
Arayüz yalnız imza taşımaz, sabit de taşıyabilir. Arayüz sabiti (interface constant) gerçekleştiren her sınıfta aynı adla okunur. Bir arayüz başka bir arayüzü extends ile genişletebilir; bu durumda genişleten arayüzü gerçekleştiren sınıf iki arayüzün bütün imzalarını yazmak zorundadır. Sınıflarda tek kalıtım kuralı arayüzler için geçerli değildir: bir arayüz birden çok arayüzü genişletebilir.
<?php // Vergili.php — Odenebilir'i genisleten ve bir sabit tasiyan arayuz require_once 'Odenebilir.php'; interface Vergili extends Odenebilir { public const ORAN = 20; public function vergi(): int; }
<?php // Satis.php — Vergili'yi gerceklestiren sinif, iki arayuzun imzalarini da yazar require_once 'Vergili.php'; class Satis implements Vergili { public function tutar(): int { return 200; } public function vergi(): int { return intdiv($this->tutar() * self::ORAN, 100); } }
<?php // satis_kullan.php — genisleten arayuz, sabit ve ust arayuz tipi require 'Satis.php'; $satis = new Satis(); echo $satis->tutar(), ' ', $satis->vergi(), ' ', Vergili::ORAN, "\n"; var_dump($satis instanceof Odenebilir);
200 40 20 bool(true)
Satis yalnız Vergili’yi gerçekleştirdiğini söylüyor, ama instanceof Odenebilir de true verdi: genişleten arayüz, üst arayüzün tipi sayılır. Sabit Vergili::ORAN ile arayüzün adı üzerinden de, self::ORAN ile gerçekleştiren sınıfın içinden de okunur.
Tip Olarak Arayüz ve Örneklenememe
Arayüz bir sınıf gibi örneklenemez, çünkü gövdesi yoktur. Aynı sözleşmeyi taşımayan bir nesne de arayüz tipli bir parametreye giremez. İkisi de çalışma anında bir hata olarak görünür; ilki Error, ikincisi TypeError verir.
<?php // tip_hatasi.php — arayuz ornek olamaz, sozlesmesiz nesne parametreye giremez require 'Satis.php'; function ode(Odenebilir $kalem): int { return $kalem->tutar(); } echo ode(new Satis()), "\n"; try { ode(new stdClass()); } catch (TypeError $e) { echo get_class($e), "\n"; } try { $kalem = new Odenebilir(); } catch (Error $e) { echo get_class($e), "\n"; }
200 TypeError Error
ode() bir sınıfa değil bir davranışa bağlıdır: Satis, Fatura ya da yarın yazılacak başka bir sınıf Odenebilir’i gerçekleştirdiği sürece gider. Bu bağımsızlık arayüzün asıl yararıdır, çünkü çağıran kod gerçekleştirimi tanımaz. Öte yandan sözleşmeyi taşımayan stdClass tür denetimine takılır ve TypeError doğurur.
Gerçekleştirenin Yükümlülüğü
Arayüzü bildiren sınıf, arayüzün bütün yöntemlerini yazmak ve hepsini public yapmak zorundadır. Biri eksikse ya da görünürlüğü daraltılmışsa sınıf bildirilemez. Aşağıdaki betik bu iki durumu ayrı dosyalarda dener.
# yukumluluk.sh — eksik yontem ve daraltilmis gorunurluk sinifi bildirilemez yapar cat > Eksik.php <<'PHP' <?php interface Odenebilir { public function tutar(): int; } class Bos implements Odenebilir {} PHP cat > Dar.php <<'PHP' <?php interface Odenebilir { public function tutar(): int; } class Gizli implements Odenebilir { private function tutar(): int { return 1; } } PHP php -d display_errors=0 Eksik.php; echo "eksik cikis=$?" php -d display_errors=0 Dar.php; echo "dar cikis=$?"
eksik cikis=255 dar cikis=255
Kısıt sınıfın değil kullananın korunması içindir: arayüz tipli bir parametreye giren nesnenin tutar() yöntemi her zaman çağrılabilir olmalıdır. Bu garanti, aşağıdaki gibi bir yer değiştirmeyi güvenli yapar.
<?php // sahte.php — sozlesmeyi tasiyan sahte bir gerceklestirim, gercek olanin yerine gecer require 'Odenebilir.php'; require 'Yazdirilabilir.php'; require 'Fatura.php'; class SahteKalem implements Odenebilir { public function tutar(): int { return 1; } } $kalemler = [new Fatura(), new SahteKalem()]; echo array_sum(array_map(fn (Odenebilir $kalem): int => $kalem->tutar(), $kalemler)), "\n";
121
array_map() içindeki fonksiyon yalnız Odenebilir biliyor; gerçek bir Fatura ile sınama için yazılmış SahteKalem aynı kodun içinden geçti. Arayüzün sınamada sağladığı yarar budur: kodu gerçek ödeme ya da gerçek dosya gibi ağır bir bağımlılığa bağlamadan çalıştırmak mümkün olur. Kurs kapanışında ele alınan, bağımlılığın dışarıdan verilmesi bu yararın üzerine kuruludur ve arayüz olmadan aynı esnekliği vermez.
Ölçüm Çekirdeği
Bu dersin ölçümü de yayilim.php dosyasını kullanır; Sınıflar ve Nesneler dersinde açıklanan çekirdek bu çalışma dizininde yeniden yazılıyor.
<?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; }
Arayüze Yöntem Eklenir
Ölçülecek değişiklik, yukarıdaki arayüze ikinci bir imza eklemektir: paraBirimi(). Gerçekleştiren sınıf Fatura yalnız tutar() yöntemini taşır. Kullanan main.php sınıfı bildirir ve tutar() değerini yazdırır; dosya düzenlenmez.
<?php // arayuz.php — arayuze yeni bir yontem eklenir require_once 'yayilim.php'; olc('arayuze-yontem', [ 'odenebilir.php' => <<<'PHP' <?php interface Odenebilir { public function tutar(): int; } PHP, 'main.php' => <<<'PHP' <?php require 'odenebilir.php'; class Fatura implements Odenebilir { public function tutar(): int { return 120; } } echo (new Fatura())->tutar(), "\n"; PHP, ], [ 'odenebilir.php' => <<<'PHP' <?php interface Odenebilir { public function tutar(): int; public function paraBirimi(): string; } PHP, ]); foreach (['once', 'sonra'] as $asama) { [$kod, $cikti] = kostur("olcum/arayuze-yontem/$asama"); echo $asama, ' cikis=', $kod, ' cikti=', $cikti, "\n"; }
arayuze-yontem 1 dosya cikis=255 cikti farkli -> kiran once cikis=0 cikti=120 sonra cikis=255 cikti=
NM11. Tek dosya düzenlendi ve kullanan durdu: Fatura artık paraBirimi() yöntemini taşımadığı için bildirilemedi, çıkış 255. Önceki koşum 120 vermişti. Hata, kullanan kodun kendisinde değil gerçekleştiren sınıftadır; arayüzün imzası değişince onu gerçekleştiren her sınıf sözleşmeyi yeniden karşılamak zorunda kalır. Bu dersin ölçü sınıfı kırandır. olc() hata iletisini basmaz, yalnız çıkış kodunu görür.
Soyut Sınıf mı, Arayüz mü
İkisi de gövdesiz imza bildirebilir, ama bir sınıfa farklı şeyler söylerler. Arayüz “bu davranışı sunuyorsun” der ve ilişkisiz sınıflar arasında bir ortak ad kurar; durum ve gövde taşıyamaz; sabit ve PHP 8.4’ten beri özellik sözleşmesi bildirebilir. Soyut sınıf “sen bu ailedensin” der; ortak özellik ve gövdeli yöntem taşıyabilir ama sınıfın tek kalıtım hakkını harcar.
Seçimi üç soru yönlendirir. Gerçekleştirenler birbirine akraba mı, yoksa yalnızca aynı davranışı mı sunuyor? Ortak gövde ya da ortak durum paylaşılacak mı? Sözleşme zamanla büyüyecek mi? Birinci soruya “yalnız aynı davranış”, ikinciye “hayır” derseniz arayüz doğrudur. Ortak durum ya da gövde istiyorsanız soyut sınıf gerekir; bir sınıfın hem bir soyut sınıfı genişletmesi hem de birkaç arayüzü gerçekleştirmesi yaygın bir birleşimdir. Üçüncü soruya “evet” derseniz aşağıdaki ölçüm, bu seçimin bedelini gösteriyor: arayüz büyüdükçe gerçekleştirenlerin her biri durur, gövdeli soyut sınıf büyüdükçe durmaz. Bu nedenle sözleşmesi sık değişecek bir davranış için soyut sınıf, uzun süre sabit kalacak bir sınır için arayüz seçmek, yazan tarafın değişikliğin bedelini kimin ödeyeceğine verdiği karardır.
Sınırlayıcı Ölçüm
Kırılmanın kaynağı arayüz kavramı mı, yoksa eklenen yöntemin gövdesiz olması mı? Aynı yöntemi gövdesiyle bir soyut sınıfa eklemek bunu ayırır. Bu ölçümde sözleşme soyut bir sınıftır, Fatura onu genişletir ve eklenen paraBirimi() yöntemi bir gövde taşır.
<?php // govdeli.php — ayni ek yontem, bu kez govdesiyle bir soyut sinifa require_once 'yayilim.php'; olc('soyut-govde', [ 'odenebilir.php' => <<<'PHP' <?php abstract class Odenebilir { abstract public function tutar(): int; } PHP, 'main.php' => <<<'PHP' <?php require 'odenebilir.php'; class Fatura extends Odenebilir { public function tutar(): int { return 120; } } echo (new Fatura())->tutar(), "\n"; PHP, ], [ 'odenebilir.php' => <<<'PHP' <?php abstract class Odenebilir { abstract public function tutar(): int; public function paraBirimi(): string { return 'TL'; } } PHP, ]); foreach (['once', 'sonra'] as $asama) { [$kod, $cikti] = kostur("olcum/soyut-govde/$asama"); echo $asama, ' cikis=', $kod, ' cikti=', $cikti, "\n"; }
soyut-govde 1 dosya cikis=0 cikti ayni -> yerinde once cikis=0 cikti=120 sonra cikis=0 cikti=120
NM12. Aynı yöntem, aynı ad, aynı kullanan; bu kez sınıf yerinde kaldı: çıkış 0, çıktı 120. Eklenen yöntemin gövdesi olduğu için Fatura’nın bir şey yazması gerekmedi, yöntem devralındı. Demek ki kırılmayı arayüz sözcüğü değil gövdesiz sözleşme doğurdu. Bu çözümün bir bedeli var: bir sınıf yalnız bir sınıfı genişletebilir. Aşağıdaki betik iki üst sınıfı virgülle sayan bir bildirim yazar ve çıkış kodunu basar.
# tekli.sh — bir sinif iki sinifi genisletemez cat > Cift.php <<'PHP' <?php abstract class Odenebilir {} abstract class Yazdirilabilir {} class Fatura extends Odenebilir, Yazdirilabilir {} PHP php -d display_errors=0 Cift.php; echo "cikis=$?"
cikis=255
Bir sınıf istediği kadar arayüzü gerçekleştirebilir ama yalnız bir sınıfı genişletebilir. Gövdeli soyut sınıf değişikliği yerinde tutar, ama sınıfın tek kalıtım hakkını harcar. Arayüz bu hakkı harcamaz, bunun karşılığında eklenen her yöntemi gerçekleştirenlere dayatır.
Özet
- Arayüz yalnız imza taşır, bir sınıf istediği kadar arayüzü
implementsile gerçekleştirebilir ve arayüz parametre tipi olarak kullanılır;instanceofhangi arayüzün gerçekleştirildiğini söyler. - Arayüz imzayı denetler, anlamı denetlemez.
- Arayüze
paraBirimi()imzası ekleninceFaturabildirilemedi,120yerine çıkış 255; bu dersin ölçü sınıfı kırandır. - Aynı yöntem gövdesiyle bir soyut sınıfa eklenince sınıf yerinde kaldı, çıktı
120olarak sürdü. - Gövdeli soyut sınıfın bedeli tek kalıtımdır: bir sınıf iki sınıfı genişletemez, çıkış 255.
Sonraki Adım
Soyut sınıf gövde taşıyabiliyordu ama sınıfa yalnız bir kez kalıtım hakkı bırakıyordu. Trait’ler dersi gövdeyi paylaşmanın başka bir yolunu soruyor: iki trait aynı adda bir yöntem getirirse kullanan sınıf ne yaşıyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.