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 thatObjects.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’sremove
method. Note that this implementation throws anUnsupportedOperationException
if theIterator
returned by this collection’siterator
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. (IftoIndex == fromIndex
, this operation has no effect.)
This method is called by theclear
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 theclear
operation on this list and its sublists.
Implementation Requirements: This implementation gets a list iterator positioned beforefromIndex
and repeatedly callsListIterator.next
followed byListIterator.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.