Türeme
Daha genel bir türün daha özel bir alt türünü tanımlamaya türetme denir. Türetilen alt tür; genel türün üyelerini edinir, onun gibi davranır, ve onun yerine geçebilir.
D'de türeme yalnızca sınıflar arasında geçerlidir. Yeni bir sınıf, mevcut başka bir sınıftan türetilerek tanımlanabilir. Bir sınıfın türetildiği türe üst sınıf, ondan türetilen yeni sınıfa da alt sınıf adı verilir. Üst sınıfın özelliklerinin alt sınıf tarafından edinilmesine kalıtım denir.
D'de iki tür türeme vardır. Bu bölümde gerçekleştirme türemesi olan class
'tan türemeyi göstereceğim; arayüz türemesi olan interface
'ten türemeyi ise daha sonraki bir bölüme bırakacağım.
Sınıfın hangi sınıftan türetildiği, tanımlanırken isminden sonra yazılan :
karakterinden sonra belirtilir:
class AltSınıf : ÜstSınıf { // ... }
Masa saati kavramını temsil eden bir sınıf olduğunu varsayalım:
class Saat { int saat; int dakika; int saniye; void ayarla(int saat, int dakika, int saniye = 0) { this.saat = saat; this.dakika = dakika; this.saniye = saniye; } }
Bu sınıfın üyelerinin, nesne oluşturulduğu an özel değerler almalarının şart olmadığını varsayalım. O yüzden bu sınıfın kurucu işlevine gerek yok. Saat, daha sonraki bir zamanda ayarla
üye işlevi ile ayarlanabiliyor; ve varsayılan değeri belirtilmiş olduğu için de saniye değerini vermek isteğe bağlı:
auto masaSaati = new Saat; masaSaati.ayarla(20, 30); writefln( "%02s:%02s:%02s", masaSaati.saat, masaSaati.dakika, masaSaati.saniye);
Not: Zaman bilgisini toString
üye işlevi ile yazdırmak çok daha uygun olurdu. O işlevi biraz aşağıda override
anahtar sözcüğünü tanırken ekleyeceğiz.
Yukarıdaki kodun çıktısı:
20:30:00
Bu kadarına bakarak Saat
sınıfının bir yapı olarak da tanımlanabileceğini düşünebiliriz. Bu üç üyeyi bir yapı olarak da bir araya getirebilirdik ve o yapı için de üye işlevler tanımlayabilirdik.
Saat
'in sınıf olması, ondan türetilen yeni türler tanımlama olanağı sunar.
Örneğin, temelde bu Saat
sınıfının olanaklarını olduğu gibi içeren, ve ek olarak alarm bilgisi de taşıyan bir ÇalarSaat
sınıfı düşünebiliriz. Bu sınıfı tek başına tanımlamak istesek; Saat
'in mevcut üç üyesinin aynılarına ek olarak iki tane de alarm üyesi, ve saati ayarlamak için kullanılan ayarla
işlevinin yanında da bir alarmıKur
işlevi gerekirdi.
Bu sınıf, bu anlatıma uygun olarak şöyle gerçekleştirilebilir:
class ÇalarSaat { int saat; int dakika; int saniye; int alarmSaati; int alarmDakikası; void ayarla(int saat, int dakika, int saniye = 0) { this.saat = saat; this.dakika = dakika; this.saniye = saniye; } void alarmıKur(int saat, int dakika) { alarmSaati = saat; alarmDakikası = dakika; } }
Saat
sınıfında da bulunan üyelerini işaretlenmiş olarak gösterdim. Görüldüğü gibi; Saat
ve ÇalarSaat
sınıflarını aynı program içinde bu şekilde ayrı ayrı tanımlamak oldukça fazla kod tekrarına neden olur.
class
'tan türetmek, bir sınıfın üyelerinin başka bir sınıf tarafından oldukları gibi edinilmelerini sağlar. ÇalarSaat
'i Saat
'ten türeterek tanımlamak, yeni sınıfı büyük ölçüde kolaylaştırır ve kod tekrarını ortadan kaldırır:
class ÇalarSaat : Saat { int alarmSaati; int alarmDakikası; void alarmıKur(int saat, int dakika) { alarmSaati = saat; alarmDakikası = dakika; } }
ÇalarSaat
'in Saat
'ten türetildiği bu tanım öncekinin eşdeğeridir. Bu tanımdaki işaretlenmiş olan bölüm, bir önceki tanımdaki işaretlenmiş olarak gösterilen bölümün yerine geçer.
ÇalarSaat
, Saat
'in bütün üye değişkenlerini ve işlevlerini kalıtım yoluyla edindiği için bir Saat
gibi de kullanılabilir:
auto başucuSaati = new ÇalarSaat; başucuSaati.ayarla(20, 30); başucuSaati.alarmıKur(7, 0);
Yeni türün Saat
'ten kalıtım yoluyla edindiği üyeleri de kendi üyeleri haline gelir, ve istendiğinde dışarıdan erişilebilir:
writefln("%02s:%02s:%02s ♫%02s:%02s", başucuSaati.saat, başucuSaati.dakika, başucuSaati.saniye, başucuSaati.alarmSaati, başucuSaati.alarmDakikası);
Yukarıdaki kodun çıktısı:
20:30:00 ♫07:00
Not: Onun yerine biraz aşağıda gösterilecek olan ÇalarSaat.toString
işlevini kullanmak çok daha doğru olur.
Bu örnekte görüldüğü gibi, üye veya üye işlev edinmek amacıyla yapılan türemeye gerçekleştirme türemesi denir.
Saat
'ten kalıtım yoluyla edinilen üyeler de ÇalarSaat
'in parçaları haline gelirler. Her ÇalarSaat
nesnesinin artık hem kendi tanımladığı alarmla ilgili üyeleri, hem de kalıtımla edindiği saatle ilgili üyeleri vardır.
Belleği bu sefer aşağıya doğru ilerleyen bir şerit olarak hayal edersek, ÇalarSaat
nesnelerinin bellekte aşağıdakine benzer biçimde durduklarını düşünebiliriz:
│ . │ │ . │ nesnenin adresi → ├───────────────┤ │(başka veriler)│ │ saat │ │ dakika │ │ saniye │ │ alarmSaati │ │ alarmDakikası │ ├───────────────┤ │ . │ │ . │
Yukarıdaki şekli yalnızca bir fikir vermesi için gösteriyorum. Üyelerin bellekte tam olarak nasıl durdukları derleyicinin kodu derlerken aldığı kararlara bağlıdır. Örneğin, başka veriler diye işaretlenmiş olan bölümde o türün sanal işlev tablosunu gösteren bir gösterge bulunur. (Nesnelerin belleğe tam olarak nasıl yerleştirildikleri bu kitabın kapsamı dışındadır.)
Uyarı: "o türden" ise türetin
Gerçekleştirme türemesinin üye edinme ile ilgili olduğunu gördük. Bu amaçla türetmeyi ancak türler arasında "bu özel tür, o genel türdendir" gibi bir ilişki (is-a) kurabiliyorsanız düşünün. Yukarıdaki örnek için böyle bir ilişkinin var olduğunu söyleyebiliriz, çünkü "çalar saat bir saattir."
Bazı türler arasında ise böyle bir ilişki yoktur. Çoğu durumda türler arasında bir içerme ilişkisi (has-a) vardır. Örneğin Saat
sınıfına Pil
de eklemek istediğimizi düşünelim. Pil
üyesini türeme yoluyla edinmek uygun olmaz, çünkü "saat bir pildir" ifadesi doğru değildir:
class Saat : Pil { // ← YANLIŞ TASARIM // ... }
Bunun nedeni saatin bir pil olmaması ama bir pil içermesidir. Türler arasında böyle bir içerme ilişkisi bulunduğunda doğru olan içeren türün diğerini üye olarak tanımlamasıdır:
class Saat { Pil pil; // ← Doğru tasarım // ... }
En fazla bir class
'tan türetilebilir
Sınıflar birden çok class
'tan türetilemezler.
Örneğin "çalar saat sesli bir alettir" ilişkisini gerçekleştirmek için ÇalarSaat
'i bir de SesliAlet
sınıfından türetmek istesek, derleme hatası ile karşılaşırız:
class SesliAlet { // ... } class ÇalarSaat : Saat, SesliAlet { // ← derleme HATASI // ... }
interface
'lerden ise istenildiği kadar sayıda türetilebilir. Bunu da daha sonra göreceğiz.
Öte yandan, sınıfların ne kadar derinlemesine türetildiklerinin bir sınırı yoktur:
class Çalgı { // ... } class TelliÇalgı : Çalgı { // ... } class Kemençe : TelliÇalgı { // ... }
Yukarıdaki kodda Kemençe
TelliÇalgı
'dan, TelliÇalgı
da Çalgı
'dan türetilmiştir. Bu tanımda Kemençe
, TelliÇalgı
ve Çalgı
özelden genele doğru bir sıradüzen oluştururlar.
Sıradüzenin gösterimi
Aralarında türeme ilişkisi bulunan türlerin hepsine birden sıradüzen ismi verilir.
Nesne yönelimli programlamada sıradüzenin geleneksel bir gösterimi vardır: üst sınıflar yukarıda ve alt sınıflar aşağıda olacak şekilde gösterilirler. Sınıflar arasındaki türeme ilişkisi de alt sınıftan üst sınıfa doğru bir okla belirtilir.
Örneğin yukarıdaki sınıf ilişkisini de içeren bir sıradüzen şöyle gösterilir:
Çalgı ↗ ↖ TelliÇalgı NefesliÇalgı ↗ ↖ ↗ ↖ Kemençe Saz Kaval Ney
Üst sınıf üyelerine erişmek için super
anahtar sözcüğü
Alt sınıf içinden üst sınıfın üyelerine erişilmek istendiğinde, üst sınıfı temsil etmek için super
anahtar sözcüğü kullanılır.
Örneğin ÇalarSaat
sınıfının üye işlevlerinin içindeyken, Saat
'ten edindiği bir üyeye super.dakika
diye erişilebilir:
class ÇalarSaat : Saat { // ... void birÜyeİşlev() { super.dakika = 10; // Saat'ten edindiği dakika değişir dakika = 10; // ... aynı şey } }
Yukarıdaki koddan da anlaşıldığı gibi, super
anahtar sözcüğü her zaman gerekli değildir çünkü bu durumda yalnızca dakika
yazıldığında da üst sınıftaki dakika
anlaşılır. super
'in bu kullanımı, hem üst sınıfta hem de alt sınıfta aynı isimde üyeler bulunduğu durumlardaki karışıklıkları gidermek için yararlıdır. Bunu biraz aşağıdaki super.sıfırla()
ve super.toString()
kullanımlarında göreceğiz.
Sıradüzendeki iki sınıfın aynı isimde üyeleri varsa isim karışıklıkları üyelerin tam isimleri belirtilerek giderilir:
class Alet { string üretici; } class Saat : Alet { string üretici; } class ÇalarSaat : Saat { // ... void foo() { Alet.üretici = "Öz Saatçilik"; Saat.üretici = "En Öz Saatçilik"; } }
Üst sınıf üyelerini kurmak için super
anahtar sözcüğü
super
anahtar sözcüğü, üst sınıfın kurucusu anlamına da gelir. Alt sınıfın kurucusundan üst sınıfın kurucusunu çağırmak için kullanılır. Bu kullanımda; this
nasıl bu sınıfın kurucusu ise, super
de üst sınıfın kurucusudur.
Üst sınıfın kurucusunun açıkça çağrılması gerekmez. Eğer alt sınıfın kurucusu üst sınıfın herhangi bir kurucusunu açıkça çağırıyorsa, üst sınıfın kurucusu çağrıldığı noktada işletilir. Öte yandan (ve eğer üst sınıfın varsayılan kurucusu varsa), üst sınıfın varsayılan kurucusu henüz alt sınıf kurucusuna girilmeden otomatik olarak işletilir.
Yukarıdaki Saat
ve ÇalarSaat
sınıflarının kurucularını tanımlamamıştık. Bu yüzden her ikisinin üyeleri de kendi .init
değerleri ile ilklenirler. Hatırlarsanız, o değer int
için sıfırdır.
Saat
'in aşağıdaki gibi bir kurucusu olduğunu varsayalım:
class Saat { this(int saat, int dakika, int saniye) { this.saat = saat; this.dakika = dakika; this.saniye = saniye; } // ... }
Kullanıcıların Saat
nesnelerini bu kurucu ile kurmaları gerektiğini biliyoruz:
auto saat = new Saat(17, 15, 0);
Bir Saat
nesnesinin öyle tek başına kurulması doğaldır.
Ancak, kullanıcıların bir ÇalarSaat
kurdukları durumda, onun türemeyle edindiği Saat
parçasını açıkça kurmaları olanaksızdır. Hatta kullanıcılar bazı durumlarda ÇalarSaat
'in bir Saat
'ten türediğini bile bilmek zorunda değillerdir.
Kullanıcının tek amacı, yalnızca alt sınıftan bir nesne kurmak ve onu kullanmak olabilir:
auto başucuSaati = new ÇalarSaat(/* ... */); // ... bir ÇalarSaat olarak kullan ...
Bu yüzden, kalıtımla edindiği üst sınıf parçasını kurmak alt sınıfın görevidir. Üst sınıfın kurucusu super
ismiyle çağrılır:
class ÇalarSaat : Saat { this(int saat, int dakika, int saniye, // Saat için int alarmSaati, int alarmDakikası) { // ÇalarSaat için super(saat, dakika, saniye); this.alarmSaati = alarmSaati; this.alarmDakikası = alarmDakikası; } // ... }
ÇalarSaat
'in kurucusu, hem kendisi için gereken alarmla ilgili bilgileri hem de üst sınıf için gereken saat bilgilerini parametre olarak almakta ve Saat
'in üyelerini super
'i çağırarak kurmaktadır.
Üye işlevleri override
ile özel olarak tanımlamak
Türemenin önemli bir yararı, üst sınıfta bulunan işlevlerin alt sınıf tarafından özel olarak yeniden tanımlanabilmesidir. override
, bu kullanımda "hükümsüz kılmak, bastırmak" anlamına gelir. Alt sınıf, üst sınıfın işlevini kendisine uygun olacak şekilde yeniden tanımlayabilir.
Alt sınıfta yeniden tanımlanabilen işlevlere sanal işlev denir. Derleyiciler sanal işlevleri sanal işlev gösterge tabloları (virtual function pointer table (vtbl)) ve vtbl göstergeleri ile gerçekleştirirler. Bu konu bu kitabın kapsamı dışında olsa da, sanal işlev çağrılarının normal işlev çağrılarından biraz daha yavaş olduklarını bilmeniz gerekir. D'de bütün sınıf işlevleri sanal varsayılırlar. O yüzden, üst sınıfın işlevinin yeniden tanımlanmasının gerekmediği bir durumda o işlevin sanal olmaması için final
olarak işaretlenmesi uygun olur. final
anahtar sözcüğünü daha sonra Arayüzler bölümünde göreceğiz.
Saat
'in sıfırla
isminde bir üye işlevi olduğunu düşünelim. Bu işlev bütün üyelerin değerlerini sıfırlıyor olsun:
class Saat { void sıfırla() { saat = 0; dakika = 0; saniye = 0; } // ... }
Hatırlayacağınız gibi, aynı işlev kalıtım yoluyla ÇalarSaat
tarafından da edinilir ve onun nesneleri ile de kullanılabilir:
auto başucuSaati = new ÇalarSaat(20, 30, 0, 7, 0); // ... başucuSaati.sıfırla();
Ancak, Saat
'in bu sıfırla
işlevinin alarmla ilgili üyelerden haberi yoktur; o, yalnızca kendi sınıfının üyeleri ile ilgili olabilir. Bu yüzden, alt sınıfın üyelerinin de sıfırlanabilmeleri için; üst sınıftaki sıfırla
işlevinin bastırılması, ve alt sınıfta yeniden tanımlanması gerekir:
class ÇalarSaat : Saat { override void sıfırla() { super.sıfırla(); alarmSaati = 0; alarmDakikası = 0; } // ... }
Alt sınıfın yalnızca kendi üyelerini sıfırladığına, ve üst sınıfın işini super.sıfırla()
çağrısı yoluyla üst sınıfa havale ettiğine dikkat edin.
Yukarıdaki kodda super.sıfırla()
yerine yalnızca sıfırla()
yazamadığımıza da ayrıca dikkat edin. Eğer yazsaydık, ÇalarSaat
sınıfı içinde bulunduğumuz için öncelikle onun işlevi anlaşılırdı, ve içinde bulunduğumuz bu işlev tekrar tekrar kendisini çağırırdı. Sonuçta da program sonsuz döngüye girer, bir bellek sorunu yaşar, ve çökerek sonlanırdı.
toString
'in tanımını bu noktaya kadar geciktirmemin nedeni, her sınıfın bir sonraki bölümde anlatacağım Object
isminde bir sınıftan otomatik olarak türemiş olması ve Object
'in zaten bir toString
işlevi tanımlamış olmasıdır.
Bu yüzden, bir sınıfın toString
işlevinin tanımlanabilmesi için override
anahtar sözcüğünün de kullanılması gerekir:
import std.string; class Saat { override string toString() const { return format("%02s:%02s:%02s", saat, dakika, saniye); } // ... } class ÇalarSaat : Saat { override string toString() const { return format("%s ♫%02s:%02s", super.toString(), alarmSaati, alarmDakikası); } // ... }
ÇalarSaat
'in işlevinin Saat
'in işlevini super.toString()
diye çağırdığına dikkat edin.
Artık ÇalarSaat
nesnelerini de dizgi olarak ifade edebiliriz:
void main() { auto masaSaatim = new ÇalarSaat(10, 15, 0, 6, 45); writeln(masaSaatim); }
Çıktısı:
10:15:00 ♫06:45
Alt sınıf nesnesi, üst sınıf nesnesi yerine geçebilir
Üst sınıf daha genel, ve alt sınıf daha özel olduğu için; alt sınıf nesneleri üst sınıf nesneleri yerine geçebilirler. Buna çok şekillilik denir.
Bu genellik ve özellik ilişkisini "bu tür o türdendir" gibi ifadelerde görebiliriz: "çalar saat bir saattir", "öğrenci bir insandır", "kedi bir omurgalı hayvandır", vs. Bu ifadelere uygun olarak; saatin gerektiği yerde çalar saat, insanın gerektiği yerde öğrenci, omurgalı hayvanın gerektiği yerde de kedi kullanılabilir.
Üst sınıfın yerine kullanılan alt sınıf nesneleri kendi türlerini kaybetmezler. Nasıl normal hayatta bir çalar saatin bir saat olarak kullanılması onun aslında bir çalar saat olduğu gerçeğini değiştirmiyorsa, türemede de değiştirmez. Alt sınıf kendisi gibi davranmaya devam eder.
Elimizde Saat
nesneleri ile çalışabilen bir işlev olsun. Bu işlev kendi işlemleri sırasında bu verilen saati de sıfırlıyor olsun:
void kullan(Saat saat) { // ... saat.sıfırla(); // ... }
Çok şekilliliğin yararı böyle durumlarda ortaya çıkar. Yukarıdaki işlev Saat
türünden bir parametre beklediği halde, onu bir ÇalarSaat
nesnesi ile de çağırabiliriz:
auto masaSaatim = new ÇalarSaat(10, 15, 0, 6, 45); writeln("önce : ", masaSaatim); kullan(masaSaatim); writeln("sonra: ", masaSaatim);
kullan
işlevi, masaSaatim
nesnesini bir ÇalarSaat
olmasına rağmen kabul eder, ve bir Saat
gibi kullanır. Bu, aralarındaki türemenin "çalar saat bir saattir" ilişkisini oluşturmuş olmasındandır. Sonuçta, masaSaatim
nesnesi sıfırlanmıştır:
önce : 10:15:00 ♫06:45
sonra: 00:00:00 ♫00:00
Burada dikkatinizi çekmek istediğim önemli nokta, yalnızca saat bilgilerinin değil, alarm bilgilerinin de sıfırlanmış olmasıdır.
kullan
işlevinde bir Saat
'in sıfırla
işlevinin çağrılıyor olmasına karşın; asıl nesne bir ÇalarSaat
olduğu için, o türün özel sıfırla
işlevi çağrılır ve onun tanımı gereği hem saatle ilgili üyeleri, hem de alarmla ilgili üyeleri sıfırlanır.
masaSaatim
'in kullan
işlevine bir Saat
'miş gibi gönderilebilmesi, onun asıl türünde bir değişiklik yapmaz. Görüldüğü gibi, kullan
işlevi bir Saat
nesnesi kullandığını düşündüğü halde, elindeki nesnenin asıl türüne uygun olan sıfırla
işlevi çağrılmıştır.
Saat
sıradüzenine bir sınıf daha ekleyelim. Sıfırlanmaya çalışıldığında üyelerinin rasgele değerler aldığı BozukSaat
sınıfı:
import std.random; class BozukSaat : Saat { this() { super(0, 0, 0); } override void sıfırla() { saat = uniform(0, 24); dakika = uniform(0, 60); saniye = uniform(0, 60); } }
O türün parametre kullanmadan kurulabilmesi için parametresiz bir kurucu işlev tanımladığına da dikkat edin. O kurucunun tek yaptığı, kendi sorumluğunda bulunan üst sınıfını kurmaktır.
kullan
işlevine bu türden bir nesne gönderdiğimiz durumda da bu türün özel sıfırla
işlevi çağrılır. Çünkü bu durumda da kullan
içindeki saat
parametresinin asıl türü BozukSaat
'tir:
auto raftakiSaat = new BozukSaat; kullan(raftakiSaat); writeln(raftakiSaat);
BozukSaat
'in kullan
içinde sıfırlanması sonucunda oluşan rasgele saat değerleri:
22:46:37
Türeme geçişlidir
Sınıfların birbirlerinin yerine geçmeleri yalnızca türeyen iki sınıfla sınırlı değildir. Alt sınıflar, üst sınıflarının türedikleri sınıfların da yerine geçerler.
Yukarıdaki Çalgı
sıradüzenini hatırlayalım:
class Çalgı { // ... } class TelliÇalgı : Çalgı { // ... } class Kemençe : TelliÇalgı { // ... }
Oradaki türemeler şu iki ifadeyi gerçekleştirirler: "telli çalgı bir çalgıdır" ve "kemençe bir telli çalgıdır". Dolayısıyla, "kemençe bir çalgıdır" da doğru bir ifadedir. Bu yüzden, Çalgı
beklenen yerde Kemençe
de kullanılabilir.
Gerekli türlerin ve üye işlevlerin tanımlanmış olduklarını varsayarsak:
void güzelÇal(Çalgı çalgı, Parça parça) { çalgı.akortEt(); çalgı.çal(parça); } // ... auto kemençem = new Kemençe; güzelÇal(kemençem, doğaçlama);
güzelÇal
işlevi bir Çalgı
beklediği halde, onu bir Kemençe
ile çağırabiliyoruz; çünkü geçişli olarak "kemençe bir çalgıdır".
Türeme yalnızca iki sınıfla sınırlı değildir. Eldeki probleme bağlı olarak, ve her sınıfın tek bir class
'tan türeyebileceği kuralına uyulduğu sürece, sıradüzen gerektiği kadar kapsamlı olabilir.
Soyut üye işlevler ve soyut sınıflar
Bazen bir sınıfta bulunmasının doğal olduğu, ama o sınıfın kendisinin tanımlayamadığı işlevlerle karşılaşılabilir. Somut bir gerçekleştirmesi bulunmayan bu işleve bu sınıfın bir soyut işlevi denir. En az bir soyut işlevi bulunan sınıflara da soyut sınıf ismi verilir.
Örneğin satranç taşlarını ifade eden bir sıradüzende SatrançTaşı
sınıfının taşın hamlesinin yasal olup olmadığını sorgulamaya yarayan yasal_mı
isminde bir işlevi olduğunu varsayalım. Böyle bir sıradüzende bu üst sınıf, taşın hangi karelere ilerletilebileceğini bilemiyor olabilir; her taşın hareketi, onunla ilgili olan alt sınıf tarafından biliniyordur: piyonun hareketini Piyon
sınıfı biliyordur, şahın hareketini Şah
sınıfı, vs.
abstract
anahtar sözcüğü, o üye işlevin bu sınıfta gerçekleştirilmediğini, ve alt sınıflardan birisinde gerçekleştirilmesinin şart olduğunu bildirir:
class SatrançTaşı { abstract bool yasal_mı(Kare nereden, Kare nereye); }
Görüldüğü gibi; o işlev o sınıfta tanımlanmamış, yalnızca abstract
olarak bildirilmiştir.
Soyut sınıf türlerinin nesneleri oluşturulamaz:
auto taş = new SatrançTaşı; // ← derleme HATASI
Bunun nedeni, eksik işlevi yüzünden bu sınıfın kullanılamaz durumda bulunmasıdır. Çünkü; eğer oluşturulabilse, taş.yasal_mı(buKare, şuKare)
gibi bir çağrının sonucunda ne yapılacağı bu sınıf tarafından bilinemez.
Öte yandan, bu işlevin tanımını veren alt sınıfların nesneleri oluşturulabilir; çünkü alt sınıf bu işlevi kendisine göre tanımlamıştır ve işlev çağrısı sonucunda ne yapılacağı böylece bilinir:
class Piyon : SatrançTaşı { override bool yasal_mı(Kare nereden, Kare nereye) { // ... işlevin piyon tarafından gerçekleştirilmesi ... return karar; } }
Bu işlevin tanımını da sunduğu için bu alt sınıftan nesneler oluşturulabilir:
auto taş = new Piyon; // derlenir
Soyut işlevlerin de tanımları bulunabilir. (Alt sınıf yine de kendi tanımını vermek zorundadır.) Örneğin, SatrançTaşı
türünün yasal_mı
işlevi genel denetimler içerebilir:
class SatrançTaşı { abstract bool yasal_mı(Kare nereden, Kare nereye) { // 'nereden' karesinin 'nereye' karesinden farklı // olduğunu denetliyoruz return nereden != nereye; } } class Piyon : SatrançTaşı{ override bool yasal_mı(Kare nereden, Kare nereye) { // Öncelikle hamlenin herhangi bir SatrançTaşı için // yasal olduğundan emin oluyoruz if (!super.yasal_mı(nereden, nereye)) { return false; } // ... sonra Piyon için özel karar veriyoruz ... return karar; } }
SatrançTaşı
sınıfı yasal_mı
işlevi tanımlanmış olduğu halde yine de sanaldır. Piyon
sınıfının ise nesneleri oluşturulabilir.
Örnek
Bir örnek olarak demir yolunda ilerleyen araçlarla ilgili bir sıradüzene bakalım. Bu örnekte sonuçta şu sıradüzeni gerçekleştirmeye çalışacağız:
DemirYoluAracı / | \ Lokomotif Tren Vagon { bindir()?, indir()? } / \ YolcuVagonu YükVagonu
Vagon
sınıfının soyut olarak bıraktığı işlevleri soru işaretleriyle belirttim.
Burada amacım yalnızca sınıf ve sıradüzen tasarımlarını göstermek olduğu için fazla ayrıntıya girmeyeceğim ve yalnızca gerektiği kadar kod yazacağım. O yüzden aşağıdaki işlevlerde gerçek işler yapmak yerine yalnızca çıkışa mesaj yazdırmakla yetineceğim.
Yukarıdaki tasarımdaki en genel araç olan DemirYoluAracı
yalnızca ilerleme işiyle ilgilenecek şekilde tasarlanmış olsun. Genel olarak bir demir yolu aracı olarak kabul edilebilmek için bu tasarımda bundan daha fazlası da gerekmiyor. O sınıfı şöyle tanımlayabiliriz:
class DemirYoluAracı { void ilerle(size_t kilometre) { writefln("Araç %s kilometre ilerliyor", kilometre); } }
DemirYoluAracı
'ndan türemiş olan bir tür Lokomotif
. Bu sınıfın henüz bir özelliği bulunmuyor:
class Lokomotif : DemirYoluAracı {
}
Problemler bölümünde DemirYoluAracı
'na soyut sesÇıkart
işlevini eklediğimiz zaman Lokomotif
türü de sesÇıkart
işlevinin tanımını vermek zorunda kalacak.
Benzer biçimde, Vagon
da bir DemirYoluAracı
'dır. Ancak, eğer vagonların programda yük ve yolcu vagonu olarak ikiye ayrılmaları gerekiyorsa indirme ve bindirme işlemlerinin farklı olarak tanımlanmaları gerekebilir. Kullanım amacına uygun olarak her vagon mal veya yolcu taşır. Bu genel tanıma uygun olarak bu sınıfa iki işlev ekleyelim:
class Vagon : DemirYoluAracı { abstract void bindir(); abstract void indir(); }
Görüldüğü gibi, bu işlevlerin Vagon
arayüzünde soyut olarak tanımlanmaları gerekiyor çünkü vagonun indirme ve bindirme işlemleri sırasında tam olarak ne olacağı o vagonun türüne bağlıdır. Bu işlemler Vagon
düzeyinde bilinemez. Yolcu vagonlarının indirme işlemi vagon kapılarının açılması ve yolcuların trenden çıkmalarını beklemek kadar basit olabilir. Yük vagonlarında ise yük taşıyan görevlilere ve belki de vinç gibi bazı araçlara gerek duyulabilir. Bu yüzden indir
ve bindir
işlevlerinin sıradüzenin bu aşamasında soyut olmaları gerekir.
Soyut Vagon
sınıfının soyut işlevlerini gerçekleştirmek, ondan türeyen somut iki sınıfın görevidir:
class YolcuVagonu : Vagon { override void bindir() { writeln("Yolcular biniyor"); } override void indir() { writeln("Yolcular iniyor"); } } class YükVagonu : Vagon { override void bindir() { writeln("Mal yükleniyor"); } override void indir() { writeln("Mal boşalıyor"); } }
Soyut bir sınıf olması Vagon
'un kullanılamayacağı anlamına gelmez. Vagon
sınıfının kendisinden nesne oluşturulamasa da Vagon
sınıfı bir arayüz olarak kullanılabilir. Yukarıdaki türetmeler "yük vagonu bir vagondur" ve "yolcu vagonu bir vagondur" ilişkilerini gerçekleştirdikleri için bu iki sınıfı Vagon
yerine kullanabiliriz. Bunu biraz aşağıda Tren
sınıfı içinde göreceğiz.
Treni temsil eden sınıfı bir lokomotif ve bir vagon dizisi içerecek biçimde tanımlayabiliriz:
class Tren : DemirYoluAracı { Lokomotif lokomotif; Vagon[] vagonlar; // ... }
Burada çok önemli bir konuya tekrar dikkatinizi çekmek istiyorum. Her ne kadar Lokomotif
ve Vagon
demir yolu araçları olsalar da, trenin onlardan türetilmesi doğru olmaz. Yukarıda değindiğimiz kuralı hatırlayalım: sınıfların türemeleri için, "bu özel tür, o genel türdendir" gibi bir ilişki bulunması gerekir. Oysa tren ne bir lokomotiftir, ne de vagondur. Tren onları içerir. Bu yüzden lokomotif ve vagon kavramlarını trenin üyeleri olarak tanımladık.
Bir trenin her zaman için lokomotifinin olması gerektiğini kabul edersek geçerli bir Lokomotif
nesnesini şart koşan bir kurucu tanımlamamız gerekir. Vagonlar seçime bağlı iseler onlar da vagon eklemeye yarayan bir işlevle eklenebilirler:
import std.exception; // ... class Tren : DemirYoluAracı { // ... this(Lokomotif lokomotif) { enforce(lokomotif !is null, "Lokomotif null olamaz"); this.lokomotif = lokomotif; } void vagonEkle(Vagon[] vagonlar...) { this.vagonlar ~= vagonlar; } // ... }
Kurucuya benzer biçimde, vagonEkle
işlevi de vagon nesnelerinin null
olup olmadıklarına bakabilirdi. Bu konuyu gözardı ediyorum.
Trenle ilgili bir durum daha düşünebiliriz. Trenin istasyondan ayrılma ve istasyona gelme işlemlerinin de desteklenmesinin gerektiğini varsayalım:
class Tren : DemirYoluAracı { // ... void istasyondanAyrıl(string istasyon) { foreach (vagon; vagonlar) { vagon.bindir(); } writefln("%s garından ayrılıyoruz", istasyon); } void istasyonaGel(string istasyon) { writefln("%s garına geldik", istasyon); foreach (vagon; vagonlar) { vagon.indir(); } } }
Bu programda geriye kalan, std.stdio
modülünün eklenmesi ve bu sınıfları kullanan bir main
işlevinin yazılmasıdır:
import std.stdio; // ... void main() { auto lokomotif = new Lokomotif; auto tren = new Tren(lokomotif); tren.vagonEkle(new YolcuVagonu, new YükVagonu); tren.istasyondanAyrıl("Ankara"); tren.ilerle(500); tren.istasyonaGel("Haydarpaşa"); }
Programda trene farklı türden iki vagon eklenmektedir: YolcuVagonu
ve YükVagonu
. Ek olarak, Tren
sınıfı programda iki farklı arayüzün sunduğu işlevlerle kullanılmaktadır:
ilerle
işlevi çağrıldığındatren
nesnesi birDemirYoluAracı
olarak kullanılmaktadır çünkü o işlevDemirYoluAracı
düzeyinde bildirilmiştir veTren
tarafından türeme yoluyla edinilmiştir.istasyondanAyrıl
veistasyonaGel
işlevleri çağrıldığında isetren
nesnesi birTren
olarak kullanılmaktadır çünkü o işlevlerTren
düzeyinde bildirilmişlerdir.
Programın çıktısı indir
ve bindir
işlevlerinin vagonların türüne bağlı olarak uygulandığını gösteriyor:
Yolcular biniyor ← Mal yükleniyor ← Ankara garından ayrılıyoruz Araç 500 kilometre ilerliyor Haydarpaşa garına geldik Yolcular iniyor ← Mal boşalıyor ←
Özet
- Türeme, "bu tür o türdendir" ilişkisi içindir.
- Her sınıf en fazla bir
class
'tan türetilebilir. super
'in iki kullanımı vardır: üst sınıfın kurucusunu çağırmak ve üst sınıfın üyelerine erişmek.override
, üst sınıfın bir işlevini bu sınıf için özel olarak tanımlar.abstract
, soyut işlevin alt sınıflardan birisinde tanımlanmasını şart koşar.
Problemler
- Yukarıdaki sıradüzenin en üst sınıfı olan
DemirYoluAracı
'nı değiştirelim. Kaç kilometre ilerlediğini bildirmenin yanında her yüz kilometre için bir de ses çıkartsın:class DemirYoluAracı { void ilerle(size_t kilometre) { writefln("Araç %s kilometre ilerliyor:", kilometre); foreach (i; 0 .. kilometre / 100) { writefln(" %s", sesÇıkart()); } } }
Ancak,
sesÇıkart
işleviDemirYoluAracı
sınıfında tanımlanamasın çünkü her aracın kendi özel sesi olsun:Lokomotif
için "çuf çuf"Vagon
için "takıtak tukutak"
Not:
Tren.sesÇıkart
işlevini şimdilik bir sonraki soruya bırakın.Her aracın farklı sesi olduğu için
sesÇıkart
'ın genel bir tanımı verilemez. O yüzden üst sınıfta soyut olarak bildirilmesi gerekir:class DemirYoluAracı { // ... abstract string sesÇıkart(); }
sesÇıkart
işlevini alt sınıflar için gerçekleştirin ve şumain
ile deneyin:void main() { auto vagon1 = new YolcuVagonu; vagon1.ilerle(100); auto vagon2 = new YükVagonu; vagon2.ilerle(200); auto lokomotif = new Lokomotif; lokomotif.ilerle(300); }
Bu programın aşağıdaki çıktıyı vermesini sağlayın:
Araç 100 kilometre ilerliyor: takıtak tukutak Araç 200 kilometre ilerliyor: takıtak tukutak takıtak tukutak Araç 300 kilometre ilerliyor: çuf çuf çuf çuf çuf çuf
Dikkat ederseniz,
YolcuVagonu
ileYükVagonu
aynı sesi çıkartıyorlar. O yüzden onların sesi, ortak üst sınıfları olanVagon
tarafından sağlanabilir. sesÇıkart
işleviniTren
sınıfı için nasıl tanımlayabileceğinizi düşünün.Bir fikir:
Tren
'in sesini, içerdiği lokomotifin ve vagonların seslerinin birleşimi olarak oluşturabilirsiniz.