PHP'de Kalıtım, Arayüz ve Trait: Kod Paylaşmanın Üç Yolu

PHP'de Kalıtım, Arayüz ve Trait: Kod Paylaşmanın Üç Yolu

5 dakika okuma

Kalıtım, arayüz ve trait sık sık birbirinin yerine kullanılır. Üçü de kod tekrarını azaltır, ancak çözdükleri sorun aynı değildir. Kalıtım bir tür ilişkisi kurar, arayüz bir sözleşme tanımlar, trait ise yalnızca kod kopyalamanın dille desteklenmiş biçimidir. Yanlış mekanizma seçildiğinde ortaya çıkan bağımlılık, ilerleyen aylarda değişimi engelleyen bir yapıya dönüşür.

Bu bölümde üç aracın sözdizimi, davranış farkları ve seçim ölçütleri ele alınıyor.

Kalıtım

Kalıtım, alt sınıfın üst sınıfın tüm görünür üyelerini devralmasını sağlar. Üst sınıfın metodu alt sınıfta yeniden tanımlanabilir; asıl davranışa parent anahtar sözcüğüyle ulaşılır.

PHP20 satır
abstract class Bildirim
{
    public function __construct(protected readonly string $alici) {}

    abstract public function gonder(string $mesaj): void;    // alt sınıf yazmak zorunda

    protected function kayitTut(string $mesaj): void
    {
        error_log(sprintf('[%s] %s', $this->alici, $mesaj));
    }
}

final class EpostaBildirimi extends Bildirim
{
    public function gonder(string $mesaj): void
    {
        mail($this->alici, 'Bildirim', $mesaj);
        $this->kayitTut($mesaj);
    }
}

Soyut sınıf doğrudan örneklenemez ve içinde gövdesi olmayan metotlar bulundurabilir. Ortak davranışın bir bölümü paylaşılacak, geri kalanı alt sınıfa bırakılacaksa uygun araçtır. Genişletilmesi istenmeyen sınıflar final ile işaretlenir; bu, ileride yapılacak değişikliklerde beklenmedik alt sınıf davranışlarıyla uğraşmayı önler.

Arayüz

Arayüz yalnızca sözleşmeyi tanımlar: hangi metotların hangi imzayla bulunacağını söyler, nasıl çalışacağını söylemez. Bir sınıf birden fazla arayüz uygulayabilir. Bu yüzden bağımlılıklar somut sınıflara değil arayüzlere verildiğinde, uygulama değiştirmek çağrı noktalarına dokunmadan mümkün olur.

PHP22 satır
interface OdemeSaglayici
{
    public function tahsilEt(int $kurus, string $kart): string;   // işlem numarası döner
}

final class SanalPos implements OdemeSaglayici
{
    public function tahsilEt(int $kurus, string $kart): string { /* ... */ }
}

final class TestSaglayici implements OdemeSaglayici
{
    public function tahsilEt(int $kurus, string $kart): string
    {
        return 'TEST-' . bin2hex(random_bytes(4));
    }
}

final class SiparisServisi
{
    public function __construct(private readonly OdemeSaglayici $odeme) {}
}

Yukarıdaki yapıda sipariş servisi hangi sağlayıcının kullanıldığını bilmez. Testlerde gerçek sağlayıcı yerine sahte bir uygulama verilebilir; üretimde sağlayıcı değiştiğinde servis kodu aynı kalır. Arayüzler sabit değerler de içerebilir, ancak özellik tanımlayamaz.

Trait

Trait, birden fazla sınıfa aynı metotları eklemenin yoludur. Kalıtımdan farkı, bir tür ilişkisi kurmamasıdır: trait kullanan sınıflar birbirinin yerine geçemez. Derleme aşamasında metotlar sınıfın içine kopyalanır.

PHP22 satır
trait ZamanDamgasi
{
    private ?DateTimeImmutable $olusturma = null;

    public function damgala(): void
    {
        $this->olusturma ??= new DateTimeImmutable('now', new DateTimeZone('UTC'));
    }
}

trait Gunlukleyici
{
    public function yaz(string $mesaj): void
    {
        error_log(static::class . ': ' . $mesaj);
    }
}

final class Fatura
{
    use ZamanDamgasi, Gunlukleyici;
}

İki trait aynı adda metot getirirse çakışma oluşur ve bu durum açıkça çözülmelidir. Çözüm sözdizimi işe yarar, ancak sık ihtiyaç duyuluyorsa tasarımın gözden geçirilmesi gerekir.

PHP7 satır
final class Rapor
{
    use Gunlukleyici, DosyaYazici {
        Gunlukleyici::yaz insteadof DosyaYazici;   // çakışmada bu kazanır
        DosyaYazici::yaz as dosyayaYaz;            // diğeri yeni adla kullanılır
    }
}

Trait durum taşıdığında sorun büyür: sınıfın hangi özelliklere sahip olduğu tek bir dosyaya bakarak anlaşılamaz. Trait kullanımı, davranışın gerçekten birbirinden bağımsız sınıflarda tekrarlandığı ve ortak bir üst sınıfın anlamsız olduğu durumlarla sınırlı tutulmalıdır.

Geç statik bağlama

self, yazıldığı sınıfı gösterir; static ise çağrının yapıldığı sınıfı. Fark, üst sınıfta yazılan bir üretici metodun alt sınıftan çağrılmasında ortaya çıkar.

PHP12 satır
abstract class Model
{
    public static function olustur(array $veri): static
    {
        return new static($veri);   // alt sınıfın örneği üretilir
    }

    public static function yanlis(array $veri): self
    {
        return new self($veri);     // her zaman Model üretmeye çalışır
    }
}

Soyut sınıf mı arayüz mü

İkisi arasındaki seçim, paylaşılacak şeyin ne olduğuna bakılarak yapılır. Paylaşılan yalnızca metot adları ve imzalarsa arayüz yeterlidir. Paylaşılan bir de ortak davranışsa soyut sınıf düşünülür, ancak bu tercih alt sınıfların tek bir üst sınıfa bağlanması anlamına gelir.

Pratikte yaygın olan düzen, arayüzü sözleşme olarak tanımlayıp ortak davranışı ayrı bir nesneye taşımaktır. Böylece hem sözleşme belirgin kalır hem de kalıtım zinciri büyümez. Soyut sınıf, ortak davranışın gerçekten alt sınıfın iç işleyişine ait olduğu dar durumlarda kullanılır.

protected görünürlüğü de dikkatli kullanılmalıdır. Bu düzey, üyeyi alt sınıflara açtığı için kalıtım zincirinin tamamıyla bir sözleşme kurar; ileride değiştirilmesi, dışarı açık bir metodu değiştirmek kadar zordur.

Metot imzalarında uyumluluk

Alt sınıf, üst sınıfın metodunu yeniden tanımlarken imzayı daraltamaz. Kural şudur: parametreler genişleyebilir, dönüş tipi daralabilir. Bunun nedeni, alt sınıf örneğinin üst sınıf beklenen her yerde kullanılabilmesi gerektiğidir.

PHP12 satır
abstract class Depo
{
    abstract public function bul(int $id): ?Model;
}

final class UrunDeposu extends Depo
{
    public function bul(int $id): ?Urun    // dönüş tipi daraldı, geçerli
    {
        // ...
    }
}

Bu kural yalnızca dilin denetlediği bir sözdizimi meselesi değildir. Alt sınıf, üst sınıfın vaat ettiğinden daha katı koşullar öne sürerse, çağıran kod beklenmedik biçimde hata alır. Örneğin üst sınıfın metodu boş liste için çalışıyorken alt sınıfın istisna fırlatması, imza aynı kalsa bile sözleşmenin ihlalidir.

Hangisi ne zaman

  • Alt tür gerçekten üst türün bir çeşidiyse ve her yerde onun yerine geçebiliyorsa kalıtım uygundur.
  • Yalnızca ortak bir sözleşme gerekiyorsa arayüz kullanılır; uygulamalar birbirinden tamamen bağımsız kalır.
  • Aynı yardımcı davranış, aralarında tür ilişkisi olmayan sınıflarda tekrar ediyorsa trait düşünülebilir.
  • Davranış çalışma zamanında değişebiliyorsa hiçbiri uygun değildir; bağımlılık nesne olarak içeri verilir.

Kalıtımın en sık yapılan yanlış kullanımı, kod paylaşmak için tür ilişkisi uydurmaktır. Bir sınıf yalnızca üst sınıftaki iki metodu kullanmak için genişletiliyorsa, o iki metot ayrı bir nesneye taşınıp bileşimle kullanılmalıdır.

PHP15 satır
// kalıtım yerine bileşim
final class SiparisRaporu
{
    public function __construct(
        private readonly Bicimleyici $bicim,
        private readonly Gunluk $gunluk,
    ) {}

    public function uret(array $satirlar): string
    {
        $this->gunluk->bilgi('Rapor üretiliyor');

        return $this->bicim->tabloya($satirlar);
    }
}

Bileşimde bağımlılıklar dışarıdan verildiği için değiştirilmesi kolaydır ve sınıfın neye ihtiyaç duyduğu yapıcı metoda bakılarak görülür. Kalıtımda ise bağımlılık üst sınıfın içinde gizlidir.

Kontrol listesi

  • Kalıtım yalnızca gerçek bir tür ilişkisi varsa kullanılıyor.
  • Genişletilmesi planlanmayan sınıflar final ile işaretli.
  • Bağımlılıklar somut sınıflara değil arayüzlere veriliyor.
  • Trait kullanımı durum taşımayan yardımcı davranışlarla sınırlı.
  • Üretici metotlarda self yerine static tercih ediliyor.
  • Kalıtım zinciri iki düzeyi geçmiyor.

İlgili yazılar

PHP eğitim serisi

Bu yazı, temel sözdiziminden üretim ortamına kadar ilerleyen 25 bölümlük serinin bir parçası. Bölümler sırayla okunacak biçimde hazırlandı, ancak her biri tek başına da kullanılabilir.

Temeller

  1. Kurulum ve temel sözdizimi
  2. Döngüler ve akış kontrolü
  3. Diziler
  4. Fonksiyonlar
  5. String fonksiyonları
  6. Süper globaller
  7. Form işleme
  8. Dosya işlemleri

Dilin ayrıntıları

  1. Tip sistemi ve strict_types
  2. Hata ve istisna yönetimi
  3. Düzenli ifadeler
  4. Tarih ve saat işlemleri

Nesne yönelimli PHP

  1. Sınıf ve nesne
  2. Kalıtım, arayüz ve trait (bu yazı)
  3. Statik üyeler, sabitler ve enum
  4. Namespace, Composer ve autoload

Veri ve güvenlik

  1. PDO ile veritabanı işlemleri
  2. Oturum yönetimi ve kimlik doğrulama
  3. Güvenli çıktı ve XSS
  4. JSON ile veri değişimi

Dış dünya ve üretim

  1. cURL ile dış API çağırma
  2. Kendi REST API'ni yazmak
  3. PHPUnit ile birim test
  4. Hata ayıklama ve performans ölçümü
  5. Üretime alma ve güvenlik

Yorumlar

Bu form reCAPTCHA ile korunur; Google Gizlilik Politikası ve Hizmet Şartları geçerlidir.

İlk yorumu sen yap.

İlgili Yazılar