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:
- Bildirdiği ama tanımını vermediği bütün üye işlevleri soyuttur;
abstract
anahtar sözcüğü bile gerekmez. - Tanımını da verdiği üye işlevler içeriyorsa o işlevlerin
static
veyafinal
olmaları şarttır. (static
vefinal
işlevleri aşağıda açıklayacağım.) - Eğer varsa, üye değişkenleri ancak
static
olabilirler. - Arayüzler ancak başka arayüzlerlerden türeyebilirler.
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 yalnızca sınıflarda ve arayüzlerde anlamlıdır çünkü yapılarda türeme yoktur. Ö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
interface
bir arayüz tanımlar; bütün işlevleri soyut olan bir sınıf gibidir. Gerçekleştirme olarak yalnızcastatic
üye değişkenleri vestatic
vefinal
üye işlevleri olabilir.- Bir sınıfın nesnelerinin oluşturulabilmesi için, türetildiği bütün arayüzlerin bütün işlevlerinin tanımlanmış olmaları gerekir.
- Tek
class
'tan türetebilme kısıtlamasıinterface
'lerde yoktur; sınıflar ve arayüzler sınırsız sayıdainterface
'ten türetilebilirler. - Sık karşılaşılan bir sıradüzen, üstte bir arayüz (
interface
) ve alttaki gerçekleştirmeleridir (class
).