22 Ağustos 2011 Pazartesi

Türlerin Evrimi ya da Kalıtlama

Bir şirketin rekabet gücü, hangi alanda faaliyet gösteriyor olursa olsun, şirket içinde tanımlanmış süreçlerin verimli yönetilmesiyle artar. Rekabetin üst düzeyde olduğu ve zamanın çok hızlı aktığı bir sektörde yer alan yazılım şirketleri için bu, alandaki bir problemin düşük hata oranlı, ucuz, randımanlı ve nitelikli bir programa dönüştürülmesinin daha hızlı yapılması demektir. Böylesine zorlu bir amaca erişmek ise daha önceden harcanmış çabaları yinelemekten kaçınarak olanaklı hale gelebilir. Yeniden kullanım—yani, daha önceden geliştirilmiş bileşenlerin kullanımı—aşağıdaki yazılım ölçülerini olumlu bir şekilde etkileyecektir.

  • Hata oranı: Önceden geliştirilmiş bileşenler daha yoğun kullanılıp sınanmış olacağından, bu bileşenleri içeren son ürün daha düşük bir hata oranına sahip olacaktır.
  • Maliyet: Önceden geliştirilmiş bileşenlerin yeniden geliştirilmesine kaynak harcanmayacağı, bu bileşenlerin sadece kullanımı söz konusu olacağı için üretim maliyeti düşecektir.
  • İşlevsel nitelik: Önceden geliştirilmiş bileşenleri yeniden geliştirmekte harcanacak zamanın bir bölümü var olan bileşenlere yeni özellikler eklemekte kullanılabileceği için son ürün daha nitelikli olacaktır.
  • Geliştirme zamanı:1 Önceden geliştirilmiş bir bileşeni kullanmak aynı bileşeni ikinci bir defa geliştirmekten daha hızlı yapılacağı için son ürün daha hızlı bir şekilde üretilecektir.
  • Kaynak verimliliği: Önceden geliştirilmiş bileşenler daha yoğun eniyilenmiş olacakları için bu tür bileşenlerden oluşturulan son ürün daha hızlı ve/veya daha az bellek tutacak şekilde geliştirilecektir.

Dolayısıyla, yazılım geliştirme sürecinin tüm aşamalarındaki ürünlerin olabildiğince yeniden kullanılan bileşenlerle geliştirilmeye çalışılması bir gerekliliktir. Bu, yazımızın konusu olan kalıtlamanın nesne yönelimlilik bağlamında neden öne çıkarıldığının başlıca sebebidir: kalıtlama, kodun2 yeniden kullanımını sağlayan araçlardan biridir. Burada, çok önemli bir noktaya vurgu yapmakta yarar var: kalıtlama bir araçtır, amaç değil; amaç kodun yeniden kullanılması yoluyla yazılımın yukarıda anılan ölçüleri olumlu etkileyecek bir biçimde geliştirilmesidir. Aklınızda olsun!

Yazımızın başlığındaki anıştırmayı açarak başlayalım. Ortaçağ'da takılmayanlarınız bilir; birey olarak sahip olduğumuz biyolojik özellikler, türümüzün kollektif DNA'sındaki genlerin milyarlarca yıldır sürdürdükleri bir ölüm kalım savaşının sonrasında ebeveynlerimizin söz konusu genleri bize aktarmasıyla ortaya çıkar. Kimi zaman, bireyin oluşması3 sürecindeki mutasyonlar, onu (ve onun gen haritasından üretilecek bireyleri) türün geri kalanından farklı kılar. Nadiren de olsa, biriken mutasyonlar, çevresel etkenlerce "ödüllendirilerek" yeni bir türün doğmasına neden olabilir. Belki de, başka bir yerde başka bir zamanda, temel türün geri kalan bireylerini etkileyen mutasyonlar da olacaktır. Artık, birbirine benzeyen iki tür vardır ve bu benzerlik türlerin tarihçelerini anlatmakta kulanılma gibi bir soyut varoluşa indirgenen temel türde gizlidir; Tabiat Ana, ekonomik davranmış ve yeni türleri oluştururken temel türü yeniden kullanmıştır.

Evrim dersimizi burada keserek, Java programlamada kalıtlama ne şekilde ifade edilebilir, ona bir bakalım. Ancak, önce anlatımımızda yararlanacağımız pedagojik senaryoyu ortaya koyalım. Mekânımız, öğrenci ve çalışanlarla dolu bir ilköğretim okulu koridoru; görevimiz, sürdürmekte olduğumuz bir bilimsel çalışma için koridordan geçmekte olan kişilere soru sorarak veri toplamak; amacımız ise, işimizi en kısa sürede bitirmek. Amacımıza doğru kişilere doğru soruları yöneltirsek daha kolay erişebiliriz. Örneğin, herkese "Adınız ne?" sorusunu yöneltmek mümkünken, görünüşü öğrenci olduğunu haykıran birine "Kaç çocuğunuz var?" sorusunu yöneltmek pek de akıl kârı olmayacaktır. Kimi zaman, farklı gruptan kişilere aynı soruyu yöneltebilmemize karşın, yanıtın elde ediliş biçimi muhatabın ait olduğu gruba göre değişebilir. Örneğin, aylık gelir sorulduğunda, yanıt müstahdem için maaş ve ekstraların toplanmasıyla bulunurken, öğretmen için verilen özel derslerin ücretinin de eklenmesi gerekecektir.

  • Kişi: Ad ...
    • Çalışan: Çocuk sayısı, maaş, ekstra gelir, toplam aylık gelir ...
      • Öğretmen: Verilen dersler, özel ders geliri ...
      • Müstahdem: Kömür yardımı, ...
    • Öğrenci: Sınıf ...

O zaman, gelin yukarıda verilen özeti Java'da nasıl ifade edeceğimize bakalım. Bunu yaparken de, işimize evrim saatini işletmeye başlayan ve koridordan geçen herkesin ortak noktalarını tanımlayan kişi kavramını gerçekleştirmekle girişelim.

Kişi.java
public abstract class Kişi {
  ...
  public final String ad() { return _ad; }
  protected final void adDeğiştir(String yeniAd) {
    _ad = yeniAd;
  } // void adDeğiştir(String) sonu
  ...
  private String _ad;
} // Kişi sınıfının sonu

Kişi muhatap = new Kişi(...); // Derleme hatası!

Sağlanan kod parçasına bakıldığında göze ilk çarpan, Kişi sınıfının, nesnesinin yaratılamayacağını belirtecek şekilde abstract nitelenmek suretiyle, soyut sınıf olarak tanımlanmış olmasıdır. Bu çeşit sınıflar, kendisinden kalıtlamak suretiyle türetilecek sınıfların gerçekleştirilmesi bağlamında çerçeveyi çizmek amacıyla tanımlanırlar. Örneğimizde, koridordan geçmekte olan kişinin hangi grupta olmasına bağlı olmaksızın geçerli olan ad bilgisinin sorgulanması ve güncellenmesi işlemleri gerçekleştirilmiş ve böylece söz konusu işlemin diğer sınıflarda yinelenmesinin önüne geçilmiştir. Sağlanan yeniden kullanım sayesinde, değişiklik gereksiniminin belirmesi durumunda veya ortaya çıkan bir hatanın giderilmesi istendiğinde, bakılması gerekecek yer sayısı bire indirilmiş ve böylece kodun bakımı kolaylaştırılmıştır.

Göze çarpan ikinci nokta, adDeğiştir metodunun tanımında kullanılan niteleyici: protected. Bu erişim niteleyicisi, ilişkin öğeye, ait olduğu sınıfın paketindeki ve aynı sınıfın kökü olduğu kalıtım sıradüzenindeki sınıflar tarafından erişim izni verildiğini söylüyor. Buna göre, adDeğiştir metodu Kişi ve dolaylı veya dolaysız Kişi'den kalıtlayan sınıflara ve Kişi ile aynı paketteki sınıflara public gibi gözükürken, diğer sınıflara private gibi gözükecektir.

Açıklamaya muhtaç bir diğer nokta, metotlarımızı nitelemekte kullandığımız final. İliştirildiği programlama öğesinin tanımında aldığı değerin daha sonra değişmeyeceğini ilan eden bu niteleyicinin kullanımı, söz konusu öğenin veri olması durumunda sabitliğe işaret ederken, örneğimizde olduğu gibi bir davranışsal öğeyi nitelemesi durumunda bu öğeye içinde bulunulan sınıftan türetilecek altsınıflarda ezilmek suretiyle yeni bir gerçekleştirim sağlanamayacağını belirtir.

Peki ama Java'da kalıtlama nasıl ifade edilir? Yani, bir kavramın bir diğeri gibi olduğuna dair gözlemimizi Java kaynak koduna nasıl aktarabiliriz? Yanıt oldukça basit: türetilmekte olan sınıfın tanımında sınıf adını takiben kullanılan extends ile sınıfı üstsınıfa bağlamak işimizi görür.

Öğrenci.java
public class Öğrenci extends Kişi {
  ...
  public String sınıfı() { return _sınıf; }
  ...
  private int _sınıf;
} // Öğrenci sınıfının sonu

Yukarıdaki tanıma göre, Öğrenci sınıfının Kişi'den türetildiğini gören derleyici, Öğrenci nesnelerinin Kişi nesneleri gibi olduğunu ve bunun sonucu olarak Kişi'de tanımlanan iletileri de alabileceğini bilir. Bundan dolayıdır ki, aşağıdaki çıktı komutu, gönderilen ileti Öğrenci sınıfında bulunmamasına rağmen bekleneni yapacak ve öğr ile temsil edilen öğrencinin Kişi'den kalıtladığı ad özelliğini çıktı ortamına basacaktır.
Öğrenci öğr = new Öğrenci(...);
System.out.println(öğr.ad());
Bu noktada, C++ ile nesne yönelimli programlama yapanlara iki hatırlatma yapmakta yarar var. Öncelikle, Java'da sınıf kalıtlaması teklidir; bir sınıf sadece bir sınıftan türetilebilir.4 Ayrıca, sınıf tanımı başlığında belirtilmemiş olsa bile, her sınıf bir diğerinden kalıtlar; üstsınıf bilgisinin eksik olduğu tanımlarda, sınıfın Object sınıfından kalıtladığı varsayılır. Dolayısıyla, şu ana kadar ki kısmi sınıf sıradüzeni şöyle oluşacaktır.

  • Object
    • Kişi
      • Öğrenci

Buna göre, Öğrenci nesneleri, geçişli olan gibi olmak ilişkisi nedeniyle, Öğrenci ve Kişi sınıflarındaki iletilerin yanısıra, Object sınıfındaki equals, toString gibi iletilere de yanıt verecektir.

Gelelim, koridorda rastlayacağımız çalışanların ortak yönlerini temsil etmek için sağlayacağımız sınıfa. Aşağıdaki kod parçasından da görülebileceği gibi, müstahdem ve öğretmenlerin paylaştığı fakat öğrencilerin sahip olmadıkları özellikleri soyutlayan bu sınıf, Kişi sınıfı gibi soyut tanımlanmış. Sınıfın soyut olmasına ek olarak, metotlardan biri de (sızlan) soyut ilan edilmiş. Bu, içinde bulunulan sınıfta söz konusu iletiye dair bir metot gövdesi sağlanmayacağı anlamına gelir.

Çalışan.java
public abstract class Çalışan extends Kişi {
  ...
  public int aylıkGelir() { return _ekstralar + _maaş; }
  public final int çocukSayısı() { return _çckSayısı; }
  public abstract void sızlan();
  ...
  private int _çckSayısı, _ekstralar, _maaş;
} // Çalışan sınıfının sonu

abstract ilan edilen bir metot, içinde bulunduğu sınıfı da otomatikman soyut kılar. Çünkü, metot gövdesinin bulunmaması ilişkin iletinin nesneye gönderilmesi durumunda çağrılacak bir metodun olmaması anlamına gelir ki. bu, arayüzde listelenen bir işlevin yerine getirilmediği anlamını taşır. [Müzik setinizdeki ses düğmesinin (ileti) donanımdaki devrelere (metot) bağlı olmadığı için işlev görmemesinin pek de mantıklı olmadığını takdir edersiniz.] Çözüm, bu tür aykırı durumların önüne geçmek için ortaya çıkmalarını engellemekten geçer. Dolayısıyla, sınıfın da abstract ilan edilmesi gereklidir.

Farkındayım, çok uzun oldu. Ama, işin en heyecanlı kısmına geldik. Onun için biraz daha sabredin de birlikte aşağıda verilen Öğretmen sınıfının gerçekleştirimine göz atalım. Somut—yani, nesnesi yaratılabilir— bir sınıf olarak tanımlanan Öğretmen sınıfında daha önceden görmediğimiz iki şey var: Override açımlaması ve super ayrılmış sözcüğü.

Öğretmen.java
import java.util.Vector;

public class Öğretmen extends Çalışan {
  ...
  @Override
  public int aylıkGelir() {
    return super.aylıkGelir() + _özelDersGeliri;
  } // int aylıkGelir() sonu

  @Override
  public void sızlan() {
    System.out.print("Ne olacak bu memleketin hali!!!");
  } // void sızlan() sonu

  public Vector<String> dersler() {
    return _verdiğiDersler;
  } // Vector<String> dersler() sonu
  ...
  private int _özelDersGeliri;
  private Vector<String> _verdiğiDersler;
} // Öğretmen sınıfının sonu

super, içinde bulunulan sınıfın üstsınıfındaki bir özelliğin kullanılmak istendiğini belirtir. Buna göre, öğretmenlerin gelirleri, Çalışan sınıfında sağlanan toplama özel derslerden alınan ücretin eklenmesi ile hesaplanacaktır. super'in konulmaması, metodun kendisini çağırarak sonsuz döngüye girmesine neden olacaktır.

@Override açımlaması, iliştirildiği metodun kalıtlanılan aynı imzalı bir metodu ezmekte olduğunu ilan eder. Bundan hareketle derleyici, üstsınıflarda—Object, Kişi ve Çalışan— bu imzaya sahip bir metodun varlığını denetler ve sonucun olumsuz olması durumunda hata vererek derlemeyi durdurur. Zorunlu olmayan bu açımlamanın kullanılması, metot imzasının yanlış yazılması durumunda ortaya çıkabilecek sinsi mantık hatalarının önüne geçilmesine yarar. Örnek olarak, yukarıdaki kod parçasında aylıkGelir yerine yanlışlıkla aylıkgelir yazdığınızı düşünün. @Override açımlaması olmadan yazıldığı takdirde, derleyici aylıkgelir ve aylıkGelir iletilerine karşılık, biri üstsınıftan kalıtlanan diğeri sınıf tarafından sağlanan, iki metot bulunduğunu düşünecektir. @Override açımlamasının kullanılması durumunda ise, metot adlarındaki küçük farklılık görülecek ve yazım hatası mantıksal hataya dönüşmeden derleme hatası olarak yakalanacaktır.

Peki ama o zaman, Öğretmen sınıfındaki sızlan metodu @Override ile açımlanırken neden aşağıda verilen Müstahdem sınıfındaki aynı imzalı metot açımlanmamıştır? İşin sırrı, sızlan metodunun üstsınıfta soyut tanımlanmış olmasında yatmaktadır. Bu, somut bir altsınıfın anılan metoda dair bir gerçekleştirim vermeden kullanılamayacağı anlamına gelir; altsınıfta sızlan metodunun sağlanmaması, derleyici tarafından yakalanacak ve gerçekleştirim sağlanmadıkça derleme hatası ortadan kalkmayacaktır. Yani, metodun üstsınıfta soyut ilan edilmesi zaten @Override açımlamasının görevini görmektedir; söz konusu metodun ayrıca açımlanması gereksizdir.

Müstahdem.java
public class Müstahdem extends Çalışan {
  ...
  public boolean kömürYardımı { return _kömürYardımı; }

  public void sızlan() {
    System.out.print("Allah devletimize milletimize zeval vermesin!!!");
  } // void sızlan() sonu
  ...
  private boolean _kömürYardımı;
} // Müstahdem sınıfının sonu

Evet, problemimizde geçen kavramlara karşılık gelen sınıfları gerçekleştirdik. Sıra, bu sınıfları ve bu sınıfların nesnelerini nasıl kullanabileceğimizi anlamaya geldi. Aşağıdaki kod parçasını takip ederek görelim.
Çalışan birisi;
if (Math.random() > 0.5)
  birisi = new Öğretmen(...);
  else birisi = new Müstahdem(...);
System.out.print(birisi.aylıkGelir());
System.out.print(birisi.dersler()); // Derleme hatası!
Kod parçasına göre, Çalışan türünde ilan edilen birisi adlı tutacak, üretilen rastgele sayıya göre kimi zaman bir Öğretmen nesnesini gösterirken kimi zaman bir Müstahdem nesnesini göterecektir. Bu bağlamda, iki nokta sizi kod parçamızda hata olabileceği düşüncesine sevkedebilir.

  1. Nasıl olur da, soyut tanımlanan Çalışan sınıfı birisi adlı değişkenin türünü tanımlamakta kullanılabilir? Yanıt: Bileşke türden değerlerin tutacak ve nesne olmak üzere iki kısımdan oluştuğunu unutmuşa benziyorsunuz; burada yaptığımız nesne yaratmak değil, nesne iliştirilmemiş bir tutacak tanımlamak. [Bileşke türlerin bu özelliği size yabancı geliyorsa, şu yazıyı🔎 okumanızı tavsiye ederim.]
  2. Nasıl olur da, Çalışan türündeki bir tutacak Öğretmen veya Müstahdem türünde bir nesne gösterebilir? Yanıt: her iki sınıfın nesneleri de Çalışan merceğinden görülebilir. Ne de olsa, öğretmen veya müstahdem, her ikisi de bir çalışandır. Ancak, nesne ne türden olursa olsun, birisi tutacağı yoluyla arkadaki nesneye Çalışan sınıfının arayüzünde bulunmayan iletiler gönderilemez.

Örneğimize derleyicinin gözlerinden bakarak ikinci maddeyi açalım. Birincil görevi koddaki tanımlayıcıların ilan edildiklerine uygun bir biçimde kullanıldığını denetlemek olan derleyici, birisi adlı değişkenin doğru kullanıldığını garanti etmek isteyecektir. Bu, birisi vasıtasıyla arkadaki nesnenin doğru kullanılmasının—yani, nesneye anlayabileceği iletilerin gönderilmesinin—her koşulda sağlanması demektir. Örneğimizi, durum durum ele alarak görelim.

  1. birisi'nin arkasındaki nesne Öğretmen türünde: Nesnemiz, Öğretmen sınıfının arayüzündeki iletilere yanıt verebilir. Bu iletiler, Object, Kişi, Çalışan ve Öğretmen sınıflarının arayüzlerindeki iletilerin bileşkesidir.
  2. birisi'nin arkasındaki nesne Müstahdem türünde: Nesnemiz, Müstahdem sınıfının arayüzündeki iletilere yanıt verebilir. Bu iletiler, Object, Kişi, Çalışan ve Müstahdem sınıflarının arayüzlerindeki iletilerin bileşkesidir.

Aynı kodun kullanılıyor olmasına karşın, programın çalışması sırasında rastgele sayı üretecinin döndürdüğü değere göre dinamik olarak belirlenen kullanım senaryosu, doğruluk denetimi esnasında elinde kaynak kod ve kaynak kodun derlenmiş hali olan sınıf dosyalarından başka bir şey bulunmayan derleyicinin nesnenin türünden yararlanarak karar veremeyeceği anlamına gelir. Programı çalıştırarak nesnenin hangi sınıfa ait olacağını görme lüksü olmayan derleyici, görevini koddan çıkarsayabileceği bilgilerle görmek zorundadır. Aradığımız bilgi, nesnelere erişimde aracılık eden tutacaktadır: Derleyici, nesnenin doğru kullanımını denetlemek için statik tür olarak da adlandırılan tutacak türünü kullanır. Yani, bir nesneye hangi iletilerin gönderilebileceğine nesneyi gösteren tutacağın türü kullanılarak karar verilir. Bundan dolayıdır ki, derleyici birisi'nin arkasındaki nesneye dersler iletisinin gönderilmesine izin vermeyecektir. Çünkü, Çalışan türündeki bir tutacak, Çalışan'ın kökü olduğu sınıf sıradüzeni içindeki somut sınıfların türünden olan nesneleri gösterebilir ve bu nesnelerin bazıları Öğretmen'e özel dersler iletisini anlamaz.

Peki, gönderilmesine izin verilen bir ileti, hangi metodun çağrılmasına yol açar? Buna, koridorda rastladığımız çalışanlara aylık gelirlerini sorduğumuz senaryoyu düşünerek yanıt vermeye çalışalım. Çalışanın müstahdem olması durumunda yanıt, maaş ve ek gelirlerin toplanması ile verilirken, muhatabımızın öğretmen olması durumunda maaş ve ek gelirlere özel derslerden alınan ücretlerin eklenmesi gerekecektir. İşin kısası, aynı soru muhatabımızın özelliklerine bağlı olarak farklı yöntemler kullanılarak yanıtlanacaktır. Vardığımız sonucu programlama sözlükçemiz ile ifade edecek olursak, aynı gönderinin (ileti gönderme) farklı metotların çağrılmasına neden olabileceğini söyleyebiliriz. Çokbiçimlilik olarak adlandırılan bu özelliğin örneği, aylıkGelir iletisinin gönderilmesinde görülebilir: birisi'nin süzgecinden geçen aylıkGelir iletisi, rastgele sayı üreticinin döndürdüğü değere bağlı olarak yaratılan nesnenin türüne—dinamik tür olarak da adlandırılır—göre ya Öğretmen sınıfındaki ya da Müstahdem sınıfındaki aynı imzalı metodun çağrılmasına neden olacaktır. Yani, bir nesneye gönderilen ileti sonucu hangi metodun çağrılacağına nesnenin türüne bakılarak karar verilir. Bir iletinin çağrılacak metoda çalışma anında bağlanmasına ise dinamik iletim denir.

Özetleyecek olursak;

  1. Öncelikle, derleme sırasında tutacak türü kullanılarak gönderinin doğruluk denetimi yapılır. Bu denetim sırasında, farklı imzalara sahip iletiler farklı addedilir.
  2. Programın çalışması sırasında, nesnenin türüne bakılarak hangi metodun çağrılacağına karar verilir.

Evet, kalıtlama ile ilgili ilk yazımız için bu kadar yeter sanırım. Aklınızdan çıkmaması gereken şu sloganı yineleyerek kapatalım: kalıtlama bir araçtır, amaç değil; amaç kodun yeniden kullanılması yoluyla yazılımın yukarıda anılan ölçüleri olumlu etkileyecek bir biçimde geliştirilmesidir.


  1. Altını çizmekte yarar var; en son okudukları programlama kitabı 16 ikillik mimarilerin teknoloji harikası olarak tanıtıldığı yıllarda basılmış kişilerin aksine, bir sonraki maddede atıfta bulunulan çalışma hızının artırılmasından bahsetmiyoruz. Bilgisayar donanımının görece değerinin azalıp, en değerli kaynak olarak yerini geliştiriciye bıraktığını vurgulamak için geliştirme zamanı diyoruz. Nedenimiz basit: aklı başında şirket yöneticileri, düşük maliyetli bir kaynaktan (işlemci, bellek, vb.) ziyade yüksek maliyetli kaynağın (insan) verimliliğini artırmaya öncelik verir. Ancak bu, düşük maliyetli kaynağın har vurup harman savrularak kullanılabileceği anlamına gelmemelidir.
  2. Kalıtlama, çözümleme ve tasarım aşamalarında da kullanılan bir araçtır. Ancak, yazımızda kalıtlamanın Java programı yazarken nasıl kullanılabileceğine değinmekle yetineceğiz.
  3. Aslında, yapılan araştırmalar, mutasyonların bireyin yaşamı sırasında maruz kaldığı yüksek radyasyon nedeniyle de olabileceğini gösteriyor.
  4. Eksiklik gibi gözüken bu durum, çoğu zaman sınıfların arayüz(ler) gerçekleştirmesi ile giderilebilir.