Effective Java Madde 20: Arayüzleri Soyut Sınıflara Tercih Edin

Java dilinde birden fazla gerçekleştirime (implementation) izin veren türleri tanımlamak için iki mekanizma bulunmaktadır: arayüzler ve soyut sınıflar (abstract class). Java 8 öncesinde arayüzlerde sadece metot imzalarını tanımlayabilirken, Java 8 ile eklenen varsayılan metotlar (default method) sayesinde, arayüzler içerisinde de soyut sınıflarda olduğu gibi somut metotlar yazabilir hale geldik. Ancak bu ikisi arasında önemli bir fark var, soyut sınıfı bir ”tür” olarak kullanmak istediğinizde onu bir çocuk sınıf yaratıp kalıtmanız gerekir. Java’da sadece tekli kalıtıma izin verilmesi (single inheritance), soyut sınıfların ”tür” tanımı olarak kullanılması noktasında ciddi kısıtlara yol açar. Bir arayüzü uygulamak içinse, sınıfın arayüzdeki metotları tanımlaması ve arayüzün sözleşmesine uyması yeterlidir. Burada arayüzü uygulayan sınıfın, sınıf hiyerarşisinde nerede bulunduğunun bir önemi yoktur.

Mevcut sınıflara uyarlama yapılarak yeni bir arayüzü uygulaması kolaylıkla sağlanabilir. Bütün yapmanız gereken implements anahtar kelimesi ile sınıf tanımında hangi arayüzü uygulamak istediğinizi bildirmek ve zaten ekli değilse gerekli metotları sınıfa eklemektir. Örneğin, Java kütüphanelerindeki birçok sınıf sonradan Comparable, Iterable ve AutoCloseable gibi arayüzleri uygulayacak şekilde uyarlanmıştır. Bunun tersine, var olan sınıfları fazladan bir soyut sınıfı daha kalıtacak şekilde değiştiremezsiniz.

Not: Aşağıdaki paragrafta kullanılan mixin ifadesinin Türkçesi olmadığı için olduğu şekliyle kullanmak zorunda kaldım. Ne anlama geldiğini aktarmaya çalıştım ama anlaşılmazsa buradan bakabilirsiniz.

Arayüzler ”mixin” tanımlamak için idealdir. Basitçe söylemek gerekirse, mixin bir sınıfın kendi türüne ek olarak uygulayabildiği, opsiyonel bir davranış belirten türlere verilen isimdir. Örneğin, Comparable mixin bir arayüzdür. Bunun böyle adlandırılmasının sebebi, bu arayüzü uygulayan sınıfların arayüzde tanımlı ek işlevleri kendi ana işlevlerinin arasına katmasından dolayıdır. (Buradaki ”katmak” fiili önemlidir çünkü mixin terimi İngilizce’de katmak/karışmak anlamına gelen ”mix in” fiilinden türetilmiştir.) Bunu tekli kalıtımın getirdiği kısıtlar dolayısıyla soyut sınıflarla yapamayız çünkü mevcut bir sınıfı ikinci bir soyut sınıfı kalıtacak şekilde değiştiremeyiz.

Arayüzler tür hiyerarşisi olmayan sistemlerin inşa edilmesine de olanak sağlarlar. Tür hiyerarşileri bir takım şeyleri organize etmek için ideal olsa da birçok sınıf bu kategoriye girmez. Varsayalım ki, bir şarkıcıyı (Singer) ve bir söz yazarını (Songwriter) temsil eden iki tane arayüzümüz olsun:

public interface Singer {
    AudioClip sing(Song s);
}

public interface Songwriter {
    Song compose(int chartPosition);
}

Gerçek hayatta, bazı şarkıcılar aynı zamanda söz yazarıdırlar. Bu türleri tanımlamak için soyut sınıflar yerine arayüzler kullandığımız için, bir sınıfın hem Singer hem de Songwriter arayüzlerini uygulaması gayet mümkündür. Hatta, hem Singer hem de Songwriter arayüzlerini kullanan üçüncü bir arayüz tanımlayarak, içerisinde başka metotlar da ekleyebiliriz.

public interface SingerSongwriter extends Singer, Songwriter {
    AudioClip strum();
    void actSensitive();
}

Her zaman bu tarz bir esnekliğe gerek duymayabilirsiniz, ancak lazım olduğunda arayüzler hayat kurtarır. Bu sistemi soyut sınıflarla kurmak isterseniz, her bir kombinasyon için tür hiyerarşinize yeni sınıflar eklemek zorunda kalırsınız. Bu da tahmin edeceğiniz üzere hem işinizi zorlaştıracak hem hata yapmanıza sebebiyet verecektir.

Arayüzler sarmalayan sınıflar (wrapper class) yazmanıza izin verirler ve güvenli ve esnek bir biçimde var olan işlevselliği geliştirebilmenizi sağlarlar. (Madde 18) Soyut sınıflar kullanıldığında ise, programcıların yazılıma ek işlevsellik kazandırabilmek için kalıtım yapmaktan başka çaresi olmaz. Sonuçta ortaya çıkan da sarmalayan sınıflardan daha kırılgan ve zayıf çocuk sınıflar olur.

Eğer bir arayüzdeki bir metodun çok bariz bir uygulama şekli varsa, arayüzü uygulayacak programcılara yardımcı olmak için varsayılan metot (default method) yazabilirsiniz. Bunun bir örneği olarak aşağıdaki removeIf metoduna bakabiliriz. Eğer bu şekilde bir varsayılan metot sağlıyorsanız @implSpec Javadoc etiketini kullanarak belgelemeyi unutmayın. (Madde 19)

// Java 8'de Collection arayüzüne eklenen varsayılan metot
default boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    boolean result = false;
    for (Iterator<E> it = iterator(); it.hasNext(); ) {
        if (filter.test(it.next())) {
            it.remove();
            result = true;
        }
    }
    return result;
}

Varsayılan metotlar kullanarak arayüzü kullanacak programcılara ne kadar destek olabileceğiniz konusunda çeşitli sınırlamalar vardır. Her ne kadar birçok arayüz kendi türü için equals ve hashCode gibi Object metotlarının nasıl davranması gerektiğini belgelese de, bunları varsayılan metot olarak tanımlamak yasaktır. Ayrıca arayüzler nesne alanları (instance field) ve private static metotlar haricinde public olmayan statik üyeler barındıramazlar. Son olarak, arayüz koduna erişiminiz yoksa varsayılan metot yazmanız da mümkün olmaz.

Ancak, arayüzlerin ve soyut sınıfların avantajlarını beraber kullanabilmek için arayüzün yanında soyut bir iskelet gerçekleştirim sınıfı (skeletal implementation class) sağlayabilirsiniz. Arayüzü tür tanımlaması yapmak için kullanabilir ve içerisine mümkün olduğu taktirde varsayılan metotlar ekleyebilirsiniz. İskelet sınıf ise geriye kalan temel olmayan arayüz metotlarını gerçekleştirmek için kullanılır. (Burada arayüzün temel metotlarından kasıt, arayüzde ve iskelet sınıfta soyut olarak bırakılıp, gerçekleştirimin son kullanıcıya bırakılması gereken metotlardır) Arayüzü direk uygulamak yerine iskelet sınıfı kalıtmak, sizi işin büyük bir kısmını yapmaktan kurtaracaktır. Bu da aslında Şablon (Template) tasarım desenidir.

İskelet gerçekleştirim sınıfları genellikle AbstractArayüz şeklinde isimlendirilir. Burada Arayüz kısmı iskelet sınıfın uyguladığı arayüzün adını alır. Örneğin, Collections çatısı (framework), her bir Collection arayüzü için bu şekilde iskelet sınıflar sağlamaktadır: AbstractCollection, AbstractSet, AbstractList, AbstractMap gibi. Aslında bu sınıfları başında Abstract yerine Skeletal olacak şekilde isimlendirmek daha mantıklı olabilirdi ama bu şekliyle genel kabul gördüğü için buna uymak daha doğru olacaktır. Doğru tasarlandığında, iskelet sınıflar (ister ayrı bir soyut sınıf olsun isterse de sadece arayüz içerisindeki varsayılan metotlardan oluşmuş olsun), programcıların arayüzleri uygulamasına çok büyük kolaylık sağlarlar. Örneğin, aşağıda tamamen işlevsel olan, esasında AbstractList kalıtılarak yazılmış bir List gerçekleştirimini döndüren static fabrika metodunu görüyoruz:

// İskelet sınıf üzerine kurulmuş somut gerçekleştirim 
static List<Integer> intArrayAsList(int[] a) {
    Objects.requireNonNull(a);

    // Java 9'dan önceki bir versiyonu kullanıyorsanız burada 
    // <Integer> ile tür belirtmelisiniz
    return new AbstractList<>() {
        @Override 
        public Integer get(int i) {
            return a[i];  // Autoboxing (Madde 6)
        }
    
        @Override 
        public Integer set(int i, Integer val) {
            int oldVal = a[i];
            a[i] = val;     // Auto-unboxing
            return oldVal;  // Autoboxing
        }
    
        @Override 
        public int size() {
            return a.length;
        } 
    };
}

Burada geri döndürülen List gerçekleştiriminin neler yapabildiğini düşündüğünüzde, bu örneğin iskelet sınıfların gücünü gösteren ne kadar çarpıcı bir örnek olduğu da ortaya çıkmaktadır. Aslında bu örnek, int türünde bir diziyi Integer nesneleri tutan bir liste görüntüsüne dönüştürerek Adapter tasarım desenini uygulamaktadır. Ancak şunu da belirtelim ki int ilkel türü ve Integer nesnelerinin sürekli birbirine dönüştürülmesinden ötürü (boxing ve unboxing) performansı çok iyi olmayacaktır. Dikkat ederseniz, buradaki List gerçekleştirimi anonim sınıf (anonymous class) biçiminde yazılmıştır. (Madde 24)

İskelet sınıfların güzel tarafı, soyut sınıflar oldukları için arayüzü uygulamak isteyen programcılara ciddi bir kolaylık sağlarken, bunlar tür olarak kullanıldığında ortaya çıkan kısıtları bize yansıtmamasıdır. Yukarıdaki örnek üzerinden gidecek olursak, programcının asıl hedefi en nihayetinde List türünde bir nesne yaratmaktır. Ancak bunu yaparken List arayüzünü uygulayıp birçok işlevi sıfırdan yazmak yerine AbstractList iskelet sınıfını kullanarak sadece değiştirmek istediği kısımları geçersiz kılmaktadır.

Bu şekilde iskelet sınıfı olan arayüzleri gerçekleştirmek isteyen hemen herkes, sağlanan iskelet sınıfı kalıtarak bu işi yapar. Ancak bu tamamen size bağlıdır, isterseniz iskelet sınıfı pas geçerek arayüzü direk de uygulayabilirsiniz. Bu durumda da yine arayüzde tanımlıysa varsayılan metotlardan faydalanabilirsiniz. Eğer iskelet sınıfı taban alarak bir gerçekleştirim yapamıyorsanız, iskelet sınıfı kalıtan bir private iç sınıf kullanarak arayüz metotları çağrıldığında bu private sınıfın metotlarına iletim yapabilirsiniz. Bu, Madde 18‘de anlatılan sarmalayan sınıf (wrapper class) tekniğine benzemektedir.

İskelet sınıflar yazmak çok zor bir iş olmasa da biraz zahmetlidir. Öncelikle uygulayacağınız arayüzdeki hangi metotları dolduracağınızı ve hangi metotları boş bırakacağınızı (yukarıda bahsedildiği gibi bunlara aynı zamanda arayüzün temel metotları da denebilir) belirlemek gerekmektedir. Boş bırakmak istediğiniz metotlar iskelet sınıfta yine abstract olarak kalacaktır. Sonra, boş bıraktığınız temel metotları kullanarak yazabildiğiniz varsayılan metotları arayüze ekleyin. Eğer sadece temel ve varsayılan metotlarla bütün arayüzü tamamlayabilirseniz işiniz bitti demektir ve ayrı bir iskelet sınıfa gerek yoktur. Aksi taktirde, arayüzü uygulayan soyut bir sınıf yaratarak geriye kalan bütün metotları geçersiz kılacak şekilde bir gerçekleştirim yapın. Bu sınıf, gerektiği taktirde public olmayan başka metot ve alanlar da içerebilir.

Basit bir örnek olması açısından, Map.Entry arayüzüne bakabiliriz. Burada temel metotlar getKey, getValue ve bir ihtimal setValue olarak belirlenebilir. Arayüz, bu türdeki nesneler için equals ve hashCode için doğru davranışın nasıl olması gerektiğini belgelemektedir ve toString için temel metotların ne şekilde kullanılabileceği açıktır. Ancak, Object sınıfından gelen bu metotları varsayılan metot olarak tanımlamak mümkün olmadığı için bütün bunlar iskelet sınıfın içerisine yazılmıştır:

// Map.Entry arayüzü için iskelet sınıf
public abstract class AbstractMapEntry<K,V> 
    implements Map.Entry<K,V> {
       
    // Bu metot mutlaka geçersiz kılınmalıdır!
    @Override 
    public V setValue(V value) {
       throw new UnsupportedOperationException();
    }

    // Map.Entry.equals ile belirtilen sözleşmeye uyulmaktadır
    @Override 
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry) o;
        return Objects.equals(e.getKey(), getKey())
               &amp;&amp; Objects.equals(e.getValue(), getValue()) 
    }

    // Map.Entry.hashCode ile belirtilen sözleşmeye uyulmaktadır   
    @Override 
    public int hashCode() {
        return Objects.hashCode(getKey())
               ^ Objects.hashCode(getValue());
    }
   
    @Override 
    public String toString() {
        return getKey() + "=" + getValue();
    }
}

Tekrar hatırlatalım, bu iskelet sınıf Map.Entry arayüzünde varsayılan metotlar ile yazılamazdı çünkü varsayılan metotlar equals, hashCode, toString gibi Object metotlarını geçersiz kılamazlar.

İskelet sınıflar kalıtılmak üzere tasarlandığı için, Madde 19’da anlatılan bütün tasarım ve belgeleme esaslarına uymalısınız. Önceki örnekte, biz asıl anlatılmak isteneni vurgulamak için dokümantasyonu çıkardık, ancak iskelet sınıflar için iyi dokümantasyon çok önemlidir.

Bazı arayüzler için iskelet sınıfların yerine AbstractMap.SimpleEntry ile yapıldığı gibi çok basit bir gerçekleştirim de sağlayabilirsiniz. İskelet sınıf gibi bir arayüzü uygulayan bu basit sınıflar, kalıtım için tasarlanmış olmalarına rağmen abstract değillerdir. Arayüz için yazılabilecek en basit gerçekleştirimi temsil ederler. Bu sınıfları olduğu gibi kullanabilir veya kalıtıp değişiklik yapabilirsiniz.

Özetle, arayüzler birden fazla gerçekleştirimi yazılabilecek türleri tanımlamanın en iyi yoludur. Basit olmayan bir arayüzünüz varsa, arayüzün yanında bir de iskelet sınıf sağlamaya çalışın. Bunu yaparken de arayüzünüze mümkün olduğunca çok sayıda varsayılan metot ekleyin ki arayüzü direk uygulayan herkes bundan faydalanabilsin. Ancak bu her zaman mümkün olmadığı için birçok durumda abstract sınıf biçiminde iskelet sınıflar da yazmanız gerekebilir.

Share

Effective Java Madde 19: Kalıtımı ya Tasarım ve Dokümantasyon ile Destekleyin ya da Yasaklayın

Madde 18’de, kalıtılmak için tasarlanmamış ve belgelenmemiş (dokümantasyon) yabancı sınıfları kalıtmanın tehlikeli olduğunu söylemiştik. Peki ama bir sınıfın kalıtım için tasarlanması ve belgelenmesi ne anlama gelmektedir?

Birincisi, sınıf içerisinde bulunan her bir metot için, geçersiz kılındığında ne gibi etkiler olacağı net olarak belgelenmelidir. Başka bir deyişle, sınıf geçersiz kılınabilen (override edilebilen) bütün metotlarını çok iyi belgelemelidir. Bütün public ve protected metotlar için, bu metotların başka hangi geçersiz kılınabilen metotları hangi sırada çağırdığı ve bu çağrıların sonucunda işletimin nasıl etkileneceği belgelenmelidir. (burada geçersiz kılınabilen metot derken final olmayan, public veya protected metotlar kastedilmektedir) Daha genel bir ifadeyle, bir sınıf geçersiz kılınabilen metotları çağırdığı bütün durumları belgelemelidir. Mesela, bu çağrılar arka plandaki bir thread veya statik ilklendiricilerden (static initializer) gelebilir.

Geçersiz kılınabilen başka metotları işleten bir metot, yaptığı bu çağrıları @implSpec Javadoc etiketini kullanarak ”Implementation Requirements” başlığı altında belgeler. Bu bölümde metodun iç çalışma mekanizması anlatılır. java.util.AbstractCollection sınıfında bunun bir örneğini bulabiliriz:

public boolean remove(Object o)

Removes a single instance of the specified element from this collection, if it is present (optional operation). More formally, removes an element e such that Objects.equals(o, e), if this collection contains one or more such elements. Returns true if this collection contained the specified element (or equivalently, if this collection changed as a result of the call).

Implementation Requirements: This implementation iterates over the collection looking for the specified element. If it finds the element, it removes the element from the collection using the iterator’s remove method. Note that this implementation throws an UnsupportedOperationException if the Iterator returned by this collection’s iterator method does not implement the remove method and this collection contains the specified object.

Not: Yukarıdaki alıntı orijinal Javadoc dokümantasyonu olduğu için birebir çevirmeyi gerekli bulmadım. Özetle, ikinci paragrafta remove metodunun çalışma mantığını anlatıyor ve aynı sınıftaki Iterator referansının remove metodunu kullanarak (Iterator.remove) veri yapısından eleman sildiğini söylüyor. Bu Iterator nesnesini elde etmek için de iterator() metodunu çağırıyor. Eğer burada geri döndürülenIterator nesnesi remove metodunu gerçekleştirmemişse UnsupportedOperationException hatası fırlatılıyor.

Bu belgelemeden anlaşıldığı üzere, iterator metodunu geçersiz kılmak remove metodunun çalışmasını etkileyecektir çünkü bu metot iterator metodunu kendi içerisinde çağırmaktadır. Hatta iterator metodu tarafından döndürülen Iterator nesnesinin davranışının, remove üzerindeki etkisini de belirtmektedir. (istisna fırlatma durumu) Hatırlarsanız Madde 18’de HashSet sınıfını kalıtan bir programcı add metodunu geçersiz kıldığında addAll metodunun davranışını etkileyip etkilemeyeceğini bilemiyordu.

Ama bu durum iyi yazılmış API belgelerinde metotların ne yaptığını güzelce açıklayıp nasıl yapıldığını gizli tutması gerektiği görüşüne ters düşmüyor mu? Aynen öyle! Bu maalesef kalıtımın sarmalama (encapsulation) ilkesini ihlal etmesi sebebiyle oluşan talihsiz bir sonuç. Bir sınıfın güvenli bir şekilde kalıtılabilmesi için, istemcilerin normal şartlarda bilmemesi gereken gerçekleştirim detaylarını belgelemek gerekmektedir.

@implSpec Javadoc etiketi Java 8 ile eklendi ve Java 9’la beraber sıkça kullanılmaya başlandı. Bu etiketin varsayılan olarak (default) aktif olması gerekirken, Java 9 itibariyle komut satırından -tag "apiNote:a:API Note:" parametresi geçilmesi gerekmektedir. Aksi taktirde Javadoc bu etiketi görmezden gelmektedir.

Kalıtım için tasarım yapmak sadece belgeleme yapmaktan ibaret değildir. Programcılara acı çektirmeden verimli çocuk sınıflar yazmalarına izin vermek için, sınıfın iç çalışma mekanizmasına müdahale edebilmeleri için akıllıca seçilmiş protected metotlar, nadir bazı durumlarda ise protected alanlar eklemek gerekebilir. Örneğin java.util.AbstractList sınıfındaki removeRange metoduna bakalım:

protected void removeRange(int fromIndex, int toIndex)

Removes from this list all of the elements whose index is between fromIndex, inclusive, and toIndex, exclusive. Shifts any succeeding elements to the left (reduces their index). This call shortens the list by (toIndex - fromIndex) elements. (If toIndex == fromIndex, this operation has no effect.)

This method is called by the clear operation on this list and its sublists. Overriding this method to take advantage of the internals of the list implementation can substantially improve the performance of the clear operation on this list and its sublists.

Implementation Requirements: This implementation gets a list iterator positioned before fromIndex and repeatedly calls ListIterator.next followed by ListIterator.remove, until the entire range has been removed. Note: If ListIterator.remove requires linear time, this implementation requires quadratic time.

Parameters:
fromIndex: index of first element to be removed.
toIndex: index after last element to be removed.

Not: Yine yukarıdaki gibi, Javadoc çevirisi yapmaktansa özetle ne dediğine bakalım. Önemli kısım olan ikinci paragrafta bu removeRange metodunun clear metodu tarafından çağrıldığını, bu metodu geçersiz kılarak çocuk sınıfların clear metodu işletildiğinde çok ciddi performans kazanabilecekleri belirtilmiş.

Bu metot List gerçekleştirimlerinden birini direk kullanan istemci sınıflar için aslında hiç de gerekli değildir. protected yapılarak API’a dahil edilmesinin tek sebebi, dokümantasyonda belirtildiği gibi AbstractList sınıfını kalıtan çocuk sınıflara hızlı çalışan bir clear metodu yazabilmelerini sağlamak içindir. Bu metot verilmeseydi, çocuk sınıflar ya clear metotları çağrıldığında ciddi performans kayıplarını kabul edecekler ya da alt liste (subList) oluşturma mekanizmasını sıfırdan yazmak zorunda kalacaklardı – bu da hiç kolay bir şey değil!

Peki bir sınıfı kalıtılabilmesi için tasarlıyorsak, hangi metotları veya alanları protected yaparak çocuk sınıflara açmamız gerektiğini nereden bileceğiz? Maalesef bunun sihirli bir formülü yok. Yapılabilecek en iyi şey çok iyi düşünmek ve çocuk sınıflar yazarak testler yapmaktır. Ancak mümkün olduğunca az sayıda metot ve alanı açmak yerinde olacaktır, çünkü bunların her biri ileride belirli gerçekleştirim detaylarına bağlı kalmanızı gerektirmektedir. Diğer taraftan, bunu kısıtlayayım derken gerekli bir metodu açmamak da sınıfı pratikte asla kalıtılamayacak duruma sokabilir.

Bu tür kalıtım için tasarlanmış sınıfların doğru tasarlanıp tasarlanmadığını anlamanın tek yolu çocuk sınıflar yazarak test etmektir. Eğer gerekli bir sınıf üyesini protected yapmayı gözden kaçırdıysanız, test esnasında yazacağının çocuk sınıflarda bu eksikliği farketmeniz çoğu zaman mümkündür. Tam tersine, birkaç tane çocuk sınıf yazmanıza rağmen hiç birisinde protected bir üyeyi kullanma ihtiyacı hissetmediyseniz, o zaman buna gerek yoktur ve private olarak değiştirebilirsiniz. Tecrübelere göre, bu şekilde 3 tane çocuk sınıf yazmak genellikle yeterlidir. Ancak bu çocuk sınıfların en az bir tanesi ata sınıfı yazan kişiden farklı biri tarafından yazılmalıdır.

Kalıtım için tasarladığınız sınıfın geniş kitlelere yayılma ihtimali var ise, şunu unutmayın ki belgelediğiniz ve protected sınıf üyeleri için uyguladığınız gerçekleştirim detaylarına sonsuza dek bağlı kalmak zorundasınız. Bu durum ileride performans iyileştirmeleri yapmak veya sınıfın işlevselliğini artırmak için yapacağınız değişiklikleri çok zorlaştırabilir hatta imkansız hale getirebilir. Bu sebeple, sınıfınızı mutlaka çocuk sınıflar yazarak test etmelisiniz.

Ayrıca bu durumda yapmak zorunda olduğunuz özel belgelemenin, sınıfı kalıtmak yerine nesnesini oluşturup olağan bir şekilde kullanmak isteyen programcıların kullandığı normal dokümantasyonu kirlettiği de aşikardır. Bu yazı yazılırken, dokümantasyon araçlarının normal kullanıcılar ile sınıfı kalıtmak isteyen kullanıcılar arasında ayrım yapabilecek kadar gelişmiş olmadığını belirtelim.

Bir sınıfın kalıtıma izin vermeden önce uyması gereken birkaç tane daha kural vardır. Yapıcı metotlar geçersiz kılınabilen metotları doğrudan veya dolaylı olarak çağırmamalıdırlar. Bu kurala uymamak yazılımda hatalara yol açar. Çocuk sınıfın nesnesi yaratılırken ata sınıfın yapıcı metodu çocuk sınıfınkinden önce çalıştırılır. Eğer ata sınıfın yapıcı metodu geçersiz kılınabilen bir metodu çağırıyorsa, çocuk sınıfta geçersiz kılınan bu metot kendi yapıcı metodundan önce çalışacaktır. Eğer bu metot da çocuk sınıfın yapıcı metodunda yapılması gereken işlemlere (ilk değer ataması gibi) bağımlı ise, program istenilen sonucu vermeyecektir. Bunu örneklemek için aşağıdaki sınıfı inceleyelim:

public class Super {
    // Bozuk - yapıcı metot geçersiz kılınabilen metot çağırıyor
    public Super() {
        overrideMe();
    }
    
    public void overrideMe() {  }
}

Şimdi de bu sınıfı kalıtıp overrideMe metodunu geçersiz kılan çocuk sınıfa bakalım:

public final class Sub extends Super {
    // final alan yapıcı metot tarafından atanıyor
    private final Instant instant;
    
    Sub() {
        instant = Instant.now();
    }
    // ata sınıf tarafından çağrılan geçersiz kılınmış metot
    @Override public void overrideMe() {
        System.out.println(instant);
    }
    
    public static void main(String[] args) {
        Sub sub = new Sub();
        sub.overrideMe();
    } 
}

Bu programın instant değerini iki defa yazdırması gerektiğini umuyor olabilirsiniz ancak ilk seferinde null yazdıracaktır çünkü overrideMe metodu ilk seferde Super sınıfındaki yapıcı metot tarafından, instant değeri henüz atanmamışken işletilecektir. Bu programda final tanımlanan instant alanının farklı zamanlarda iki farklı değer ürettiğini de gözden kaçırmayın! Ayrıca, eğer overrideMe içerisinde instant üzerinden herhangi bir metot çağırsaydık, Super yapıcı metodundan gelen ilk çağrıda overrideMe metodu NullPointerException fırlatırdı. Şu anki haliyle NullPointerException fırlatmamasının tek sebebi println metodunun null parametreleri tolere etmesidir.

Ata sınıfın yapıcı metodundan private, final ve static metotları çağırmakta bir sorun yoktur çünkü bunların hiç birisi geçersiz kılınabilir değildir.

Cloneable ve Serializable arayüzleri kalıtım için tasarlanan sınıflar için daha büyük güçlükler doğurmaktadır. Genel olarak, kalıtım için tasarlanan sınıfların bu arayüzleri uygulaması iyi bir fikir değildir çünkü bu sınıfı kalıtacak programcılara ağır bir yük yüklenmiş olur. Ama yine de bunu yapmak istiyorsanız, Madde 13 ve Madde 86’da anlatıldığı şekilde kullanabileceğiniz özel yöntemler mevcuttur. Bu arayüzlerden birini uygulamaya karar verdiyseniz, yapıcı metotlardaki kısıtlamanın clone ve readObject metotları için de geçerli olduğunu unutmayın: clone ve readObject içerisinde geçersiz kılınabilen başka metotları doğrudan veya dolaylı olarak çağırmayın!

Şimdiye kadar anlatılanlardan, bir sınıfı kalıtıma izin verecek şekilde tasarlamanın çok zahmetli bir iş olduğunu ve sınıf üzerinde ciddi kısıtlamalara yol açtığını anlamışsınızdır. Bu sebeple bu kararı alırken ciddi kafa yormanız gerekmektedir. Soyut sınıflar (abstract class) gibi durumlarda bu açıkça yapılması gereken bir şey olsa da, değiştirilemez sınıflar (immutable class) söz konusuysa kalıtımı düşünerek tasarım yapmak anlamsızdır. (Madde 17)

Peki ama sıradan somut sınıflar için ne yapmalıyız? Bunlar genellikle ne final olarak tanımlanır ne de kalıtılmak üzere tasarım ve belgeleme yapılır, bu tehlikeli bir durumdur! Böyle bir sınıfa her değişiklik yapıldığında, bu sınıfı kalıtmış olan çocuk sınıflarda problemlere yol açmak olasıdır. Bu, teoride kalmış bir problemden ibaret değildir. Kalıtılmak için tasarlanmamış ve belgelenmemiş sınıflarda yapılan değişikliklerden sonra bu sınıfı kalıtan kişilerden hata raporları gelmesi son derece yaygın bir durumdur.

Bunun için en iyi çözüm, kalıtım için tasarlanmamış ve belgelenmemiş sınıfların kalıtılmasını yasaklamaktır. Bunun için de iki yol vardır. Kolay olanı sınıfı final olarak tanımlamaktır. İkinicisi ise bütün yapıcı metotları private veya package private tanımlayıp, public static fabrika metotları aracılığıyla nesne yaratılmasına izin vermektir. Bu alternatif Madde 17’de ele alınmıştır. Bu iki yöntemden herhangi birisinin kullanılmasında bir sakınca yoktur.

Bu öneri biraz çelişkili gelebilir çünkü birçok yazılımcı sıradan somut sınıfları kalıtarak eklemeler ve çıkarmalar yapmaya çok alışkındır. Madde 18’de anlatılan wrapper class yöntemi, işlevselliği artırmak için kalıtıma nazaran daha iyi bir alternatiftir.

Eğer somut bir sınıf standart bir arayüzü uygulamıyorsa, kalıtımı kısıtlamak suretiyle bazı programcıları zor durumda bırakabilirsiniz. Böyle bir sınıfta kendinizi kalıtıma izin vermek zorunda hissederseniz, en azından sınıfın geçersiz kılınabilen metotlarının hiç birisini çağırmadığından emin olun ve bu durumu belgeleyin. Bunu yaparak, kalıtılması makul derecede güvenli bir sınıf yazmış olursunuz, çünkü çocuk sınıfta geçersiz kılınan hiçbir metot başka metotların bozulmasına sebep olmayacaktır.

Sınıfın geçersiz kılınabilen metotlarını çağırmasını davranış değişikliğine sebep olmadan engellemenin bir yolu mevcuttur. Geçersiz kılınabilen her metodun içeriğini private bir yardımcı metoda (helper method) taşıyın ve geçersiz kılınabilen her metottan kendisine ait olan yardımcı metodu çağırın. Daha sonra sınıfın içerisinde geçersiz kılınabilen metotları çağıran kısımları değiştirip direk olarak onlara karşılık gelen private yardımcı metotları çağırın.

Özetle, bir sınıfı kalıtıma göre tasarlamak zor bir iştir. Sınıfın kendi içindeki kullanımları çok iyi belgelemelisiniz ve bunlara sınıfın ömrü boyunca riayet etmelisiniz. Bunu yapmazsanız, çocuk sınıflar ata sınıfın gerçekleştirim detaylarına bağımlı olurlar ve bu gerçekleştirim değiştiğinde hata vermeye başlarlar. Diğer programcılata verimli çocuk sınıflar yazmalarına izin vermek için, bazı metotları protected olarak dışarı açmanız gerekebilir. Sınıfınızdan çocuk sınıflar yaratılması konusunda ciddi bir gereklilik hissetmiyorsanız, kalıtımı yasaklamak daha faydalı olacaktır.

Share