İçeriğe geç / Skip to content / Zum Inhalt

Dart'ta Interface ve Implements: Sınıflara Ekstra Özellik

Ahmet Balaman

Son güncelleme:

6 dk okuma

FlutterDartInterfaceAbstractOOP
Dart'ta Interface ve Implements: Sınıflara Ekstra Özellik

Kalıtım ve polymorphism yazısında extends ile bir sınıfın tek bir üst sınıftan türediğini gördük. Gerçek nesnelerin ise çoğu zaman birden fazla yeteneği vardır: bir elma hem yenebilir hem sıkılabilir, bir ödeme servisi hem kayıt tutar hem de test sırasında sahtesiyle değiştirilebilmelidir. Dart bu ihtiyaçlar için üç ayrı araç sunar: abstract class, interface ve mixin.

Derslerde en çok karıştırılan nokta, bu üçünün aynı işi yaptığının sanılmasıdır. Kısaca ayıralım:

  • Abstract class: Kısmi uygulama ve sözleşme bir arada. "Bir türüdür" ilişkisi kurar.
  • Interface: Yalnızca sözleşme. "Şunu yapabilir" der, kodu uygulayan sınıf yazar.
  • Mixin: Hazır davranış paketi. Kodu verir, ama "bir türüdür" ilişkisi kurmaz.

Bu yazıda üçünü de Dart 3'ün sınıf niteleyicileriyle birlikte ele alıyoruz.

Canlı Demo: Abstract Class ve Implements

Abstract class ve implements kullanımlarını interaktif olarak öğrenin:

Dart'ta Her Sınıf Bir Interface'tir

Java veya C#'tan gelenler interface anahtar kelimesiyle ayrı bir yapı tanımlamaya alışkındır. Dart'ta her sınıf örtük olarak kendi interface'ini de tanımlar. Yani herhangi bir sınıfı implements edebilirsiniz:

class Selamlayici {
  final String ad;
  Selamlayici(this.ad);

  String selamla(String kime) => 'Merhaba $kime, ben $ad.';
}

class SahteSelamlayici implements Selamlayici {
  @override
  String get ad => 'Test';

  @override
  String selamla(String kime) => 'Selam $kime';
}

implements yalnızca sözleşmeyi alır; Selamlayici'nın gövdelerinden hiçbiri gelmez. Dikkat edin, ad alanı bile yeniden yazılmak zorunda kaldı. Alanlar da interface'in parçasıdır ve getter olarak (değiştirilebilir alanlar için setter olarak da) uygulanmalıdır.

Dart 3: abstract interface class

Somut bir sınıfı implements etmek mümkün olsa da niyeti belirsiz bırakır. Dart 3 ile gelen interface niteleyicisi, "bu sınıf bir sözleşmedir" demenin açık yoludur. Yalnızca sözleşme içeren saf bir interface için abstract interface class yazılır:

abstract interface class Yenebilir {
  void nasilYenir();
}

abstract interface class Sikilabilir {
  void nasilSikilir();
}

class Elma implements Yenebilir, Sikilabilir {
  @override
  void nasilYenir() => print('Isırıp çiğneyerek');

  @override
  void nasilSikilir() => print('Meyve sıkacağına koyup sıkarak');
}

void main() {
  final elma = Elma();

  print('Elma nasıl yenir?');
  elma.nasilYenir();

  print('\nElma nasıl sıkılır?');
  elma.nasilSikilir();
}

Çıktı:

Elma nasıl yenir?
Isırıp çiğneyerek

Elma nasıl sıkılır?
Meyve sıkacağına koyup sıkarak

Bu yazının eski sürümünde interface'ler void howToSqueeze() {} şeklinde, boş süslü parantezlerle tanımlanmıştı. Küçük görünen bu fark önemlidir: {} metoda boş bir gövde verir, yani metot artık soyut değildir. Böyle bir sınıfı extends eden biri metodu yazmayı unutursa derleyici bir şey demez ve hiçbir şey yapmayan metot sessizce çalışır. Sözleşme kuruyorsanız metodu gövdesiz, noktalı virgülle bitirin.

Neden İki Ayrı Interface?

Çünkü her nesne her yeteneğe sahip değildir. Taş yenmez ama kırılabilir; ekmek yenir ama sıkılmaz. Yetenekleri küçük interface'lere bölmek, her sınıfın yalnızca gerçekten yapabildiği sözleşmeleri üstlenmesini sağlar:

abstract interface class Kirilabilir {
  void nasilKirilir();
}

class Tas implements Kirilabilir {
  @override
  void nasilKirilir() => print('Çekiçle');
}

Tersini yapıp tek bir büyük "Meyve" interface'ine yenme, sıkma, soyma, kurutma metotlarını koyarsanız, onu uygulayan her sınıf işine yaramayan metotları boş gövdelerle doldurmak zorunda kalır. Boş bırakılmış @override metotları, interface'in fazla büyük olduğunun işaretidir.

Extends ile Implements Arasındaki Fark

extends ile alt sınıf metot gövdesini hazır alır, implements ile sınıf yalnızca sözleşmeyi alır ve gövdeyi kendi yazmak zorundadır

extends implements with (mixin)
Kaç tane? Yalnızca bir Birden fazla Birden fazla
Gövdeler gelir mi? Evet Hayır Evet
Her üyeyi yazmak zorunlu mu? Hayır, yalnızca soyut olanları Evet, hepsini Hayır
Kurduğu ilişki "Bir türüdür" "Şunu yapabilir" "Şu davranışı kullanır"

Diyagramdaki örneğin kodu şöyle:

class Hayvan {
  void nefesAl() => print('Nefes alıyor');
}

class Kopek extends Hayvan {
  // nefesAl hazır geldi; istersek override ederiz
}

abstract interface class Yuruyebilir {
  void yuru();
}

class Robot implements Yuruyebilir {
  @override
  void yuru() => print('Robot yürüyor'); // Yazmak zorunlu
}

Bir sınıf ikisini aynı anda da kullanabilir: class Kedi extends Hayvan implements Yuruyebilir gibi.

Aynı Abstract Class, İki Farklı Kullanım

Öğrencilerin kafasını en çok karıştıran durum şudur: bir abstract class hem extends hem implements edilebilir ve sonuç farklıdır.

abstract class Sekil {
  void ciz() => print('Şekil çiziliyor');
  double alan();
}

class Kare extends Sekil {
  final double kenar;
  Kare(this.kenar);

  @override
  double alan() => kenar * kenar;
  // ciz() hazır geldi
}

class Daire implements Sekil {
  final double r;
  Daire(this.r);

  @override
  double alan() => 3.14159 * r * r;

  @override
  void ciz() => print('Daire çiziliyor'); // Gövdeli olsa da yazmak zorunlu
}

extends eden Kare yalnızca soyut alan metodunu yazar. implements eden Daire ise gövdesi hazır olan ciz dahil her şeyi yazmak zorundadır.

Dart 3 Niteleyicileri: Kim Ne Yapabilir?

Dart 3 ile bir sınıfın tanımlandığı kütüphanenin (genellikle dosyanın) dışından nasıl kullanılabileceğini belirleyebiliyorsunuz:

Bildirim Nesnesi oluşturulur mu? Dışarıdan extends Dışarıdan implements
class Evet Evet Evet
abstract class Hayır Evet Evet
interface class Evet Hayır Evet
abstract interface class Hayır Hayır Evet
base class Evet Evet Hayır
final class Evet Hayır Hayır

interface class'ın neden var olduğunu sorarsanız: bir sınıfın metotları birbirini çağırıyorsa, dışarıdan biri onu genişletip bir metodu ezdiğinde diğerleri beklenmedik biçimde bozulabilir. interface bu riski kapatır; sınıfı kullanmak ve uygulamak serbesttir, ama miras alınıp içi değiştirilemez. Bu kısıtlamalar paket yazarken önem kazanır; tek bir uygulamanın içinde en sık kullanacağınız biçim abstract interface class olacaktır.

Mixin: Sözleşme Değil, Hazır Davranış

Bazen ihtiyacınız sözleşme değil, birden fazla sınıfta tekrar eden bir kod parçasıdır. Loglama gibi bir davranışı her sınıfa kopyalamak yerine mixin kullanılır:

mixin Loglayici {
  void logla(String mesaj) => print('[$runtimeType] $mesaj');
}

class OdemeServisi with Loglayici {
  void ode(double tutar) {
    logla('$tutar TL ödeme başlatıldı');
  }
}

class SepetServisi with Loglayici {
  void temizle() => logla('Sepet temizlendi');
}

void main() {
  OdemeServisi().ode(250);   // [OdemeServisi] 250.0 TL ödeme başlatıldı
  SepetServisi().temizle();  // [SepetServisi] Sepet temizlendi
}

Bir mixin'in yalnızca belirli bir türle kullanılmasını istiyorsanız on ile kısıtlarsınız. Böylece mixin içinden o türün üyelerine erişebilirsiniz:

abstract class Hayvan {
  String get ad;
}

mixin Yuzebilir on Hayvan {
  void yuz() => print('$ad yüzüyor');
}

class Ordek extends Hayvan with Yuzebilir {
  @override
  String get ad => 'Ördek';
}

void main() {
  Ordek().yuz(); // Ördek yüzüyor
}

Eski eğitimlerde sık görülen bir kalıp Dart 3'te artık çalışmaz: sıradan bir class bildirimini with ile karıştırmak. Dart 3'te bir sınıfın mixin olarak kullanılabilmesi için mixin ya da mixin class olarak tanımlanması gerekir. mixin class hem sınıf hem mixin olarak kullanılabilir; Flutter'daki ChangeNotifier bu şekilde tanımlıdır, bu yüzden hem extends ChangeNotifier hem with ChangeNotifier yazabilirsiniz. Kendi kodunuzda ise Effective Dart'ın önerisine uyup ya saf bir mixin ya da saf bir class tanımlamak daha nettir.

Gerçek Hayatta Interface: Test Edilebilir Kod

Interface'lerin günlük Flutter geliştirmesindeki en değerli kullanımı, bir bağımlılığı değiştirilebilir kılmaktır. Kullanıcı verisini getiren bir katman düşünün. Uygulamada veriyi sunucudan alır, testte ise sunucuya hiç gitmeden sabit veri döndürmelidir:

class Kullanici {
  final int id;
  final String ad;
  const Kullanici(this.id, this.ad);
}

abstract interface class KullaniciDeposu {
  Future<Kullanici?> getir(int id);
}

class SahteKullaniciDeposu implements KullaniciDeposu {
  @override
  Future<Kullanici?> getir(int id) async =>
      id == 1 ? const Kullanici(1, 'Ayşe') : null;
}

class ProfilServisi {
  final KullaniciDeposu depo; // Somut sınıfa değil, sözleşmeye bağlı

  ProfilServisi(this.depo);

  Future<String> baslik(int id) async {
    final kullanici = await depo.getir(id);
    return kullanici?.ad ?? 'Bilinmeyen kullanıcı';
  }
}

Future<void> main() async {
  final servis = ProfilServisi(SahteKullaniciDeposu());
  print(await servis.baslik(1)); // Ayşe
  print(await servis.baslik(2)); // Bilinmeyen kullanıcı
}

ProfilServisi verinin nereden geldiğini bilmez; yalnızca KullaniciDeposu sözleşmesini tanır. Uygulamada aynı sözleşmeyi HTTP isteği atan bir ApiKullaniciDeposu sınıfı uygular, testte SahteKullaniciDeposu kullanılır. Burada composition ile interface birlikte çalışıyor: servis bir depoya sahiptir, deponun hangi sınıf olduğu ise dışarıdan verilir. async ve await ayrıntıları için try-catch ve async/await yazısına bakabilirsiniz.

Flutter'ın kendisi de bu fikirle çalışır. Örneğin ChangeNotifier, ValueNotifier ve animasyon controller'ları ortak Listenable sözleşmesini uygular; bu yüzden hepsi ListenableBuilder gibi aynı widget'a verilebilir. Dokunma olayları için kendi "tıklanabilir" interface'inizi yazmanıza da gerek yoktur; o işi GestureDetector üstlenir.

Editörün Yardımı

VS Code veya Android Studio'da bir sınıfa implements Yenebilir yazdığınızda, eksik metotlar yüzünden hata işareti çıkar. Hızlı düzeltme menüsündeki (ampul simgesi) eksik override'ları oluşturma seçeneği, tüm metot imzalarını sizin için ekler. Sonra yalnızca gövdeleri doldurursunuz.

Sık Yapılan Hatalar

  • Interface metotlarını {} ile tanımlamak; metot soyut olmaktan çıkar ve unutulan uygulamalar sessizce geçer.
  • Tek bir büyük interface tasarlayıp uygulayan sınıfları boş metotlarla doldurmak.
  • Yalnızca kod paylaşmak için implements kullanmak; hazır davranış gerekiyorsa mixin ya da composition daha uygundur.
  • Dart 3'te sıradan bir class'ı with ile kullanmaya çalışmak; mixin ya da mixin class gerekir.
  • Servisleri somut sınıflara bağlamak; sözleşmeye bağlanan kod test edilebilir kalır.

Sonraki Adım

Nesneleri tasarlamayı öğrendikten sonra onları bir arada tutmanın yollarına geçebiliriz: Dart koleksiyonları: List, Set ve Map. Aynı ayrım C# tarafında da var; oradaki karşılığını interface ve abstract class farkı yazısında bulabilirsiniz.

Kaynaklar


Benimle çalışmak isterseniz:

Sık Sorulan Sorular

Dart'ta ayrı bir interface anahtar kelimesi var mı?

Dart'ta her sınıf örtük olarak bir interface tanımlar, bu yüzden herhangi bir sınıfı implements edebilirsiniz. Dart 3 ile gelen interface niteleyicisi ise sınıfın kendi kütüphanesi dışından extends edilmesini engelleyip yalnızca implements'e izin verir. Saf bir sözleşme için abstract interface class yazılır.

Bir sınıf hem extends hem implements kullanabilir mi?

Evet. class Kedi extends Hayvan with Yuzebilir implements Yuruyebilir, Kosabilir gibi tek bir üst sınıftan türeyip mixin kullanabilir ve aynı anda birden fazla interface uygulayabilirsiniz. Sıralama her zaman extends, with, implements şeklindedir.

Abstract class içinde gövdesi olan metot tanımlayabilir miyim?

Evet, abstract class hem gövdeli hem gövdesiz (soyut) metot içerebilir. Onu extends eden sınıflar gövdeli metotları hazır alır; implements eden sınıflar ise gövdeli olsun olmasın hepsini yeniden yazmak zorundadır.

Mixin ile implements arasındaki fark nedir?

implements yalnızca sözleşmeyi alır ve tüm gövdeleri sizin yazmanızı ister. Mixin (with) ise gövdeleri hazır olarak sınıfa ekler. Bir davranışın kodunu paylaşmak istiyorsanız mixin, sınıfın belirli bir yeteneğe sahip olduğunu garanti etmek istiyorsanız interface kullanın.

Yorumlar