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

Bir Cevap Yazın