D Programlama Dili – Programlama dersleri ve D referansı
Ali Çehreli

alt sınıf: [subclass], başka sınıftan türetilen sınıf
arayüz: [interface], yapının, sınıfın, veya modülün sunduğu işlevler
bildirim: [declare], tanımını vermeden belirtmek
gerçekleştirme: [implementation], kodun oluşturulması
sıradüzen: [hierarchy], sınıfların türeyerek oluşturdukları aile ağacı
soyut: [abstract], somut gerçekleştirmesi verilmemiş olan
üst sınıf: [super class], kendisinden sınıf türetilen sınıf
... bütün sözlük



İngilizce Kaynaklar


Diğer




Arayüzler

Sınıf sıradüzenlerinde arayüz tanımlamak için class anahtar sözcüğü yerine interface kullanılır. interface, bazı olanakları kısıtlanmış soyut sınıf gibidir:

Bu kısıtlamalarına rağmen arayüzlerin getirdikleri önemli bir yarar vardır: Her sınıf en fazla bir class'tan türeyebildiği halde interface'ten türemenin sınırı yoktur.

Tanımlanması

class yerine interface yazılarak tanımlanır:

interface SesliAlet {
    // ...
}

interface o arayüzün gerektirdiği işlevleri bildirir ama tanımlarını vermez:

interface SesliAlet {
    string ses();     // Yalnızca bildirilir (tanımı verilmez)
}

O arayüz ile kullanılabilmeleri için, interface'ten türeyen sınıfların interface'in bildirdiği işlevleri tanımlamaları gerekir.

Arayüz işlevlerinin in ve out blokları bulunabilir:

interface I {
    int işlev(int i)
    in {
        /* Bu işlevi çağıranların uyması gereken en ağır
         * koşullar. (Alt arayüzler ve sınıflar bu koşulları
         * hafifletebilirler.) */

    } out {  // ((sonuç) parametresi de bulunabilir)
        /* Bu işlevin gerçekleştirmelerinin vermeleri gereken
         * garantiler. (Alt arayüzler ve sınıflar ek
         * garantiler de verebilirler.) */
    }
}

Sözleşmeli programlamanın türemedeki kullanımını daha sonra Yapı ve Sınıflarda Sözleşmeli Programlama bölümünde göreceğiz.

interface'ten türetme

Türeme söz dizimi class'tan farklı değildir:

class Keman : SesliAlet {
    string ses() {
        return "♩♪♪";
    }
}

class Çan : SesliAlet {
    string ses() {
        return "çın";
    }
}

Üst sınıflarda da olduğu gibi, parametre olarak interface alan işlevler onları asıl türlerini bilmeden kullanabilirler. Örneğin, işlemleri sırasında bir SesliAlet kullanan aşağıdaki işlev hangi tür bir sesli alet olduğunu bilmeden onun ses işlevinden yararlanabilir:

void sesliAletKullan(SesliAlet alet) {
    // ... bazı işlemler ...
    writeln(alet.ses());
    // ... başka işlemler ...
}

Sınıflarda da olduğu gibi, o işlev SesliAlet arayüzünden türeyen her sınıf ile çağrılabilir:

    sesliAletKullan(new Keman);
    sesliAletKullan(new Çan);

Her aletin kendi asıl türünün tanımladığı ses işlevi çağrılır ve sonuçta sırasıyla Keman.ses ve Çan.ses üye işlevlerinin çıktıları görülür:

♩♪♪
çın
Birden fazla interface'ten türetme

Bir sınıf ancak tek bir class'tan türetilebilir. interface'ten türemede ise böyle bir kısıtlama yoktur.

Örnek olarak, haberleşme aletlerini temsil eden aşağıdaki arayüzü ele alalım:

interface HaberleşmeAleti {
    void konuş(string mesaj);
    string dinle();
}

Telefon sınıfını hem bir sesli alet, hem de bir haberleşme aleti olarak kullanabilmek için onu bu iki arayüzden birden türeterek tanımlayabiliriz:

class Telefon : SesliAlet, HaberleşmeAleti {
    // ...
}

O tanım şu iki ilişkiyi birden sağlar: "telefon bir sesli alettir" ve "telefon bir haberleşme aletidir".

Telefon sınıfının nesnelerinin oluşturulabilmesi için bu iki arayüzün gerektirdiği bütün işlevleri tanımlamış olması gerekir:

class Telefon : SesliAlet, HaberleşmeAleti {
    string ses() {                     // SesliAlet için
        return "zırrr zırrr";
    }

    void konuş(string mesaj) {         // HaberleşmeAleti için
        // ... mesajı hatta ilet ...
    }

    string dinle() {                   // HaberleşmeAleti için
        string hattaDuyulanSes;
        // ... sesi hattan oku ...
        return hattaDuyulanSes;
    }
}

Programın gerekleri doğrultusunda sınırsız sayıda interface'ten türetilebilir.

interface'ten ve class'tan türetme

Bir sınıf, bir veya daha fazla interface'ten türetilmenin yanında, bir adet olduğu sürece aynı zamanda bir sınıftan da türetilebilir:

class Saat {
    // ... kendi gerçekleştirmesi ...
}

class ÇalarSaat : Saat, SesliAlet {
    string ses() {
        return "bi bi biip";
    }
}

ÇalarSaat, Saat'in bütün üyelerini ve üye işlevlerini türeme yoluyla edinmektedir. Bunun yanında, SesliAlet arayüzünün gerektirdiği ses işlevini tanımlamak zorundadır.

interface'ten interface türetme

Arayüzden türetilen bir arayüz, alt sınıfların tanımlamaları gereken işlevlerin sayısını arttırmış olur:

interface MüzikAleti : SesliAlet {
    void akortEt();
}

Yukarıdaki tanıma göre, bir MüzikAleti olabilmek için hem SesliAlet'in gerektirdiği ses işlevinin, hem de MüzikAleti'nin gerektirdiği akortEt işlevinin tanımlanması gerekir.

Örneğin, yukarıdaki Keman sınıfı doğrudan SesliAlet arayüzünden türetilmek yerine aradaki MüzikAleti'nden türetilse, akortEt işlevini de tanımlaması gerekir:

class Keman : MüzikAleti {
    string ses() {            // SesliAlet için
        return "♩♪♪";
    }

    void akortEt() {          // MüzikAleti için
        // ... akort işlemleri ...
    }
}
static üye işlevler

static üye işlevler yapılar, sınıflar, ve arayüzler için tanımlanabilir. Önceki bölümleri gereğinden fazla karmaşıklaştırmamak için bu olanağı bu bölüme bıraktım.

Hatırlayacağınız gibi, normal üye işlevler her zaman için bir nesne üzerinde çağrılırlar. Üye işlev içinde kullanılan üyeler hep o nesnenin üyeleridir:

struct Yapı {
    int i;

    void değiştir(int değer) {
        i = değer;
    }
}

void main() {
    auto nesne0 = Yapı();
    auto nesne1 = Yapı();

    nesne0.değiştir(10);    // nesne0.i değişir
    nesne1.değiştir(10);    // nesne1.i değişir
}

Ek olarak, üyeler üye işlev içindeyken "bu nesne" anlamına gelen this ile de belirtilebilirler:

    void değiştir(int değer) {
        this.i = değer;    // üsttekinin eşdeğeri
    }

static üye işlevler ise hiçbir nesne üzerinde işlemezler; üye işlev içindeyken this anahtar sözcüğünün karşılık geldiği bir nesne yoktur. O yüzden, static üye işlev içindeyken hiçbir normal üye değişken geçerli değildir:

struct Yapı {
    int i;

    static void ortakİşlev(int değer) {
        i = değer;         // ← derleme HATASI
        this.i = değer;    // ← derleme HATASI
    }
}

static işlevler ancak türlerin ortak üyeleri olan static üyeleri kullanabilirler.

Yapılar bölümünde gördüğümüz Nokta türünü static üye işlevi olacak biçimde tekrar tanımlayalım. Hatırlarsanız, Nokta türünün her nesnesine farklı bir numara veriliyordu. Numaralar bu sefer static bir üye işlev tarafından belirleniyor:

import std.stdio;

struct Nokta {
    size_t numara;    // Nesnenin kendi numarası
    int satır;
    int sütun;

    // Bundan sonra oluşturulacak olan nesnenin numarası
    static size_t sonrakiNumara;

    this(int satır, int sütun) {
        this.satır = satır;
        this.sütun = sütun;
        this.numara = yeniNumaraBelirle();
    }

    static size_t yeniNumaraBelirle() {
        immutable yeniNumara = sonrakiNumara;
        ++sonrakiNumara;
        return yeniNumara;
    }
}

void main() {
    auto üstteki = Nokta(7, 0);
    auto ortadaki = Nokta(8, 0);
    auto alttaki =  Nokta(9, 0);

    writeln(üstteki.numara);
    writeln(ortadaki.numara);
    writeln(alttaki.numara);
}

static yeniNumaraBelirle işlevi, türün ortak değişkeni olan sonrakiNumara'yı kullanabilir. Sonuçta her nesnenin farklı bir numarası olur:

0
1
2

Yukarıda bir yapı üzerinde gösterdiğim static üye işlevler sınıflarla ve arayüzlerle de kullanılabilir.

final üye işlevler

final üye işlevler sınıflar ve arayüzler için tanımlanabilir. (Yapılarda türeme olmadığı için yapılarla ilgili bir olanak değildir.) Önceki bölümleri gereğinden fazla karmaşıklaştırmamak için bu olanağı bu bölüme bıraktım.

"Son" anlamına gelen final anahtar sözcüğü bir üye işlevin tanımının daha alttaki sınıflar tarafından değiştirilemeyeceğini bildirir; bir anlamda, algoritmanın son tanımı bu sınıf veya arayüz tarafından verilmektedir. Bir algoritmanın ana hatlarının üst sınıf veya arayüz tarafından belirlendiği ve ayrıntılarının alt sınıflara bırakıldığı durumlarda yararlıdır.

Bunun örneğini bir Oyun arayüzünde görelim. Bir oyunun nasıl oynatıldığının ana hatları bu arayüzün oynat işlevi tarafından belirlenmektedir:

interface Oyun {
    final void oynat() {
        string isim = oyunİsmi();
        writefln("%s oyunu başlıyor", isim);

        oyuncularıTanı();
        hazırlan();
        başlat();
        sonlandır();

        writefln("%s oyunu bitti", isim);
    }

    string oyunİsmi();
    void oyuncularıTanı();
    void hazırlan();
    void başlat();
    void sonlandır();
}

final işlevin tanımladığı adımların alt sınıflar tarafından değiştirilmesi mümkün değildir. Alt sınıflar ancak aynı arayüz tarafından şart koşulmuş olan beş işlevi tanımlayabilirler ve böylece algoritmayı tamamlamış olurlar:

import std.stdio;
import std.string;
import std.random;
import std.conv;

class ZarToplamıOyunu : Oyun {
    string oyuncu;
    size_t adet;
    size_t toplam;

    string oyunİsmi() {
        return "Zar Toplamı";
    }

    void oyuncularıTanı() {
        write("İsminiz nedir? ");
        oyuncu = strip(readln());
    }

    void hazırlan() {
        write("Kaç kere zar atılsın? ");
        readf(" %s", &adet);
        toplam = 0;
    }

    void başlat() {
        foreach (i; 0 .. adet) {
            immutable zar = uniform(1, 7);
            writefln("%s: %s", i, zar);
            toplam += zar;
        }
    }

    void sonlandır() {
        writefln("Oyuncu: %s, Zar toplamı: %s, Ortalama: %s",
                 oyuncu, toplam, to!double(toplam) / adet);
    }
}

void kullan(Oyun oyun) {
    oyun.oynat();
}

void main() {
    kullan(new ZarToplamıOyunu());
}

Yukarıda bir arayüz üzerinde gösterdiğim final üye işlevler sınıflarla da kullanılabilir.

Nasıl kullanmalı

interface çok kullanılan bir olanaktır. Hemen hemen bütün sıradüzenlerin en üstünde bir veya daha fazla interface bulunur. En sık karşılaşılan sıradüzenlerden birisi, tek bir interface'ten türeyen basit gerçekleştirme sınıflarından oluşan sıradüzendir:

                  MüzikAleti
                 (interface)
                /   |   \     \
          Kemençe  Saz  Kaval  ...

Çok daha karmaşık sıradüzenlerle de karşılaşılır ama bu basit yapı çoğu programın ihtiyacı için yeterlidir.

Bazı alt sınıfların ortak gerçekleştirmelerinin bir ara sınıfta tanımlandığı durumlarla da sık karşılaşılır. Alt sınıflar bu ortak sınıftan türerler. Aşağıdaki sıradüzende TelliMüzikAleti ve NefesliMüzikAleti sınıfları kendi alt türlerinin ortak üyelerini içeriyor olabilirler:

                 MüzikAleti
                (interface)
                /         \
     TelliMüzikAleti    NefesliMüzikAleti
       /   |  \          /    |   \
 Kemençe  Saz  ...    Kaval  Ney   ...

O ortak sınıflardan türeyen alt sınıflar da kendi daha özel tanımlarını içerebilirler.

Soyutlama

Arayüzler programların alt bölümlerini birbirlerinden bağımsızlaştırmaya yararlar. Buna soyutlama denir. Örneğin, müzik aletleri kullanan bir programın büyük bir bölümü yalnızca MüzikAleti arayüzünden haberi olacak biçimde ve yalnızca onu kullanarak yazılabilir.

Müzisyen gibi bir sınıf asıl türünü bilmeden bir MüzikAleti içerebilir:

class Müzisyen {
    MüzikAleti alet;
    // ...
}

Birden fazla müzik aletini bir araya getiren türler o aletlerin asıl türlerini bilmek zorunda değillerdir:

    MüzikAleti[] orkestradakiAletler;

Programın çoğu işlevi yalnızca bu arayüzü kullanarak yazılabilir:

bool akortGerekiyor_mu(MüzikAleti alet) {
    bool karar;
    // ...
    return karar;
}

void güzelÇal(MüzikAleti alet) {
    if (akortGerekiyor_mu(alet)) {
        alet.akortEt();
    }

    writeln(alet.ses());
}

Bu şekilde bir soyutlama kullanarak programın bölümlerinin birbirlerinden bağımsız hale getirilmeleri, alt sınıflarda ileride gerekebilecek kod düzenlemelerinin serbestçe yapılabilmesini sağlar. Alt sınıfların gerçekleştirmeleri bu arayüzün arkasında oldukları için, bu arayüzü kullanan kodlar o değişikliklerden etkilenmemiş olurlar.

Örnek

SesliAlet, MüzikAleti, ve HaberleşmeAleti arayüzlerini içeren bir program şöyle yazılabilir:

import std.stdio;

/* Bu arayüz 'ses' işlevini gerektirir. */
interface SesliAlet {
    string ses();
}

/* Bu sınıfın yalnızca 'ses' işlevini tanımlaması gerekir. */
class Çan : SesliAlet {
    string ses() {
        return "çın";
    }
}

/* Bu arayüz 'ses' işlevine ek olarak 'akortEt' işlevini de
 * gerektirir. */
interface MüzikAleti : SesliAlet {
    void akortEt();
}

/* Bu sınıfın 'ses' ve 'akortEt' işlevlerini tanımlaması
 * gerekir. */
class Keman : MüzikAleti {
    string ses() {
        return "♩♪♪";
    }

    void akortEt() {
        // ... kemanın akort işlemleri ...
    }
}

/* Bu arayüz 'konuş' ve 'dinle' işlevlerini gerektirir. */
interface HaberleşmeAleti {
    void konuş(string mesaj);
    string dinle();
}

/* Bu sınıfın 'ses', 'konuş', ve 'dinle' işlevlerini
 * tanımlaması gerekir. */
class Telefon : SesliAlet, HaberleşmeAleti {
    string ses() {
        return "zırrr zırrr";
    }

    void konuş(string mesaj) {
        // ... mesajın hatta iletilmesi ...
    }

    string dinle() {
        string hattaDuyulanSes;
        // ... sesin hattan okunması ...
        return hattaDuyulanSes;
    }
}

class Saat {
    // ... Saat'in gerçekleştirilmesi ...
}

/* Bu sınıfın yalnızca 'ses' işlevini tanımlaması gerekir. */
class ÇalarSaat : Saat, SesliAlet {
    string ses() {
        return "bi bi biip";
    }

    // ... ÇalarSaat'in gerçekleştirilmesi ...
}

void main() {
    SesliAlet[] aletler;

    aletler ~= new Çan;
    aletler ~= new Keman;
    aletler ~= new Telefon;
    aletler ~= new ÇalarSaat;

    foreach (alet; aletler) {
        writeln(alet.ses());
    }
}

main'in içindeki aletler bir SesliAlet dizisi olduğu için, o diziye SesliAlet'ten türeyen her tür eklenebiliyor. Sonuçta programın çıktısı bütün aletlerin ürettikleri sesleri içerir:

çın
♩♪♪
zırrr zırrr
bi bi biip
Özet