Press "Enter" to skip to content

Effective Java Madde 15: Sınıfların ve Üyelerinin Erişilebilirliğini Kısıtlayın

Last updated on April 5, 2020

İyi tasarlanmış bir yazılım modülünü kötü tasarlanmış bir modülden ayıran en önemli faktör, içindeki verileri ve gerçekleştirim (implementation) detaylarını diğer modüllerden ne kadar iyi saklayabildiğidir. İyi tasarlanmış bir modül, gerçekleştirim detaylarını net bir biçimde diğer modüllerden gizler. Modüller birbirleriyle tanımladıkları API (Uygulama Programlama Arabirimi, UPA) üzerinden konuşurlar ve birbirlerinin iç dünyasından habersiz olmaları gerekir. Bu konsept, çok temel bir yazılım tasarım ilkesidir ve bilgi saklama yada kapsülleme (encapsulation) olarak bilinir.

Bilgi saklama konsepti birçok açıdan önemlidir. En önemli faydası ise yazılım modüllerini birbirinden ayrıştırarak (decoupling) birbirinden bağımsız bir şekilde geliştirilebilmesini, test edilebilmesini ve kullanılabilmesini sağlamaktır. Bu durum, bir sistemin geliştirilmesini hızlandırır çünkü farklı modüller paralel bir biçimde aynı anda geliştirilebilir. Ayrıca bakım yapmayı ve modülün anlaşılmasını kolaylaştırır, bir problem olduğunda diğer modülleri bozma riski olmadan hata araştırılıp, çeşitli çözümler denenebilir. Bilgi saklama kendi başına bir sistemin yüksek performansla çalışmasını sağlamasa da, birbirinden bağımsız modüllerin diğerlerini etkilemeden optimize edilebilmesini sağlar. Dolayısıyla sistemi test ederek hangi modülün yavaş çalıştığını tespit edebilir, sistemin diğer parçalarına zarar vermeden yavaş çalışan modülü optimize edebilirsiniz. Birbirinden bağımsız modüller aynı zamanda tekrar kullanılabilirliği (reusability) yüksek olan modüllerdir. Bilgi saklama tekniği büyük sistemler geliştirmenin riskini önemli ölçüde azaltır, çünkü sistem bir bütün olarak doğru çalışmasa da bağımsız modüllerin doğru çalıştığı kanıtlanabilir.

Java, bilgi saklamada yazılımcıya destek olan birçok özellik sunar. Erişim kontrolü (access control) mekanizması sınıfların, arayüzlerin ve sınıf üyelerinin erişilebilirliğini belirler. Bir elemanın erişilebilirliği, tanımlandığı yer ve tanımlanırken kullanılan erişim belirteçleri (private, protected, public) ile anlaşılır. Bu belirteçlerin uygun kullanımı bilgi saklaması yapılırken çok önemli bir hal alır.

Temel kural her zaman şudur: sınıfların ve üyelerinin erişilebilirliğini mümkün olduğunca kısıtlayın. Diğer bir deyişle, yazdığınız yazılımın düzgün çalışmasını engellemeyen en düşük erişim seviyesini kullanın.

Dahili olmayan sınıflar (non-nested class) ve arayüzler için sadece iki erişim seviyesi vardır: package private (pakete özel) ve public (herkese açık). Böyle bir sınıfı veya arayüzü public erişim belirteci ile tanımlarsanız herkese açık, hiçbir erişim belirteci kullanmazsanız pakete özel olur. Eğer bir sınıf veya arayüz pakete özel yapılabiliyorsa yapılmalıdır çünkü bu daha iyi bir kapsülleme sağlar, böylece istemcilerinizi etkilemeden değişiklik yapabilirsiniz. Herkese açık erişim verilen bir sınıfı ise, ileriki versiyonlarda uyumluluğu bozmamak için sonsuza kadar o şekilde desteklemeniz gerekir.

Eğer pakete özel dahili olmayan bir sınıf (veya arayüz) sadece bir sınıf tarafından kullanılıyorsa, bu sınıfı gizli bir dahili sınıf (private nested class) olarak kullanıcı sınıf içerisinde tanımlayabilirsiniz. Bu değişiklik sınıfın erişilebilirliğini pakete özel olmaktan çıkarıp sadece tanımlandığı diğer sınıf içerisinden erişilebilmesini sağlar, böylece erişimi daha da kısıtlamış oluruz. Ancak, herkese açık (public) bir sınıfın erişilebilirliğini düşürmek, pakete özel (package private) bir sınıfın erişilebilirliğini düşürmekten çok daha önemlidir. Çünkü herkese açık bir sınıf içinde bulunduğu paketin programlama arayüzünün (API) bir parçasıdır, pakete özel bir sınıf ise zaten bir gerçekleştirim detayı olarak paketin içerisinde kısıtlanmıştır.

Sınıf üyeleri için (alanlar, metotlar, dahili sınıflar ve dahili arayüzler) mümkün olan 4 tane erişim belirteci vardır:

  • private (gizli): Bu anahtar kelimeyle tanımlanan sınıf üyesi sadece tanımladığı sınıftan erişilebilir.
  • package-private (pakete özel): Hiçbir anahtar kelime kullanmadan tanımladığınız üyeler için geçerli olan erişim seviyesidir. Üye, aynı pakette bulunan bütün sınıflardan erişilebilir.
  • protected (korumalı): Üye, içinde tanımlandığı sınıfı kalıtan çocuk sınıflar içerisinden (JLS 6.6.2’de tanımlı bazı sınırlamalar hariç) ve aynı pakette bulunan diğer sınıflardan erişilebilir.
  • public (açık): Üye bir sınır olmaksızın her yerden erişilebilir.

Sınıfınızın public arayüzünü (API) dikkatlice tasarladıktan sonra, refleks olarak geri kalan bütün sınıf üyelerini private yapmalısınız. Sadece sınıf üyesinin aynı paket içerisindeki başka bir sınıftan erişilebilmesi gerektiği durumlarda private erişim belirtecini kaldırmalısınız, bu üyeyi package private (pakete özel) yapacaktır. Ancak bunu sık sık yapmak zorunda kalıyorsanız, tasarımınızı gözden geçirip bu iki sınıf arasındaki bağımlılığı azaltacak bir yol bulmaya çalışmalısınız. Hem private hem de package private üyeler sınıf gerçekleştiriminin bir parçasıdır ve dışa açık olan API’a dahil değildirler. Ancak sınıf Serializable arayüzünü uyguluyorsa o zaman bu alanlar API ile birlikte dışarı sızabilir. (Madde 86 ve 87)

Sınıf eğer public tanımlanmışsa, package private erişim seviyesinden protected seviyesine geçiş üyelerin erişilebilirliğini çok büyük oranda artıracaktır. protected tanımlanmış bir üye API’ın bir parçasıdır ve daima desteklenmeye devam edilmelidir. protected bir üye ayrıca bir gerçekleştirim detayına açık bir bağlılığı temsil etmektedir (Madde 19). Bu tür üyelere olan ihtiyaç genellikle düşüktür.

Metotların erişim seviyesini azaltma noktasında bizleri kısıtlayan bir kural vardır. Eğer bir metot ata sınıftan bir metodu geçersiz kılıyorsa (override), ata sınıfta tanımlanmış olan erişim seviyesini düşüremez. Bu, ata sınıfın kullanılabildiği her yerde çocuk sınıfın da kullanılabilmesini sağlamak için gereklidir. Bu kuralı çiğnediğiniz zaman derleyici hata verecek ve çocuk sınıf derlenmeyecektir. Bu kural arayüz kullanırken de geçerlidir. Arayüz içerisinde tanımlanmış bütün metotlar public olduğu için, bu arayüzü uygulayan sınıflardaki arayüz metotları da public olmak zorundadır.

Test yapmayı kolaylaştırmak için sınıfların, arayüzlerin ve sınıf üyelerinin erişilebilirlik seviyesini artırmak aklınıza gelebilir. Bu bir noktaya kadar kabul edilebilir. Mesela, public bir sınıfın private bir üyesini test edebilmek için package private yapabilirsiniz. ancak erişim seviyesini bundan daha fazla yükseltmek doğru değildir. Başka bir deyişle, bir sınıfı, arayüzü veya sınıf üyesini, sadece test yapmayı kolaylaştırmak için paketin API’ına dahil etmek kabul edilemez. Zaten buna gerek de yoktur, çünkü testler de paketin bir parçası olarak yazılabilir ve dolayısıyla package private erişim seviyesi kullanıldığında paketin bütün içeriğine erişebilir.

Nesne alanları (instance field) çok nadir durumlarda public olabilir. (Madde 16) Eğer bir alan final değilse, veya değiştirilebilir (mutable) bir nesneyi gösteren final bir referans ise, bu alanı public yaparak dışarıdan değiştirilebilmesine olanak sağlamış olursunuz, dolayısıyla içerisinde saklanabilecek değerleri kısıtlama kabiliyetini kaybetmiş olursunuz. Bu sebeple, public ve değiştirilebilir bir alana sahip bir sınıfın thread güvenliği (thread safety) yoktur. Böyle bir alan final olsa ve değiştirilemez (immutable) bir nesneye referans olsa bile, bu alanı public yaparak kontratın bir parçası haline getirmiş olursunuz ve ileride yapacağınız kod değişikliklerinde bu alanı ihtiyacınız kalmasa bile silmeniz mümkün olmaz.

Aynı tavsiye tek bir istisna haricinde static alanlar için de geçerlidir. Gerektiğinde sabit değerler public static final anahtar kelimeleriyle dış dünyaya açılabilir. Adet olduğu üzere, bu tür alanlar büyük harf kullanılarak ve kelimeler arasına alt çizgi (underscore) konularak tanımladır. (Madde 68) Bu alanların ilkel değerler (primitive values) taşıması veya değiştirilemez nesnelere referans olması çok önemli bir gerekliliktir. (Madde 17) Değiştirilebilir bir nesneyi gösteren final bir referans, final olmayan bir alanın sahip olduğu bütün dezavantajları taşır. Her ne kadar referans final olduğu için değiştirilemese de, gösterdiği nesne değiştirilebileceği için çok kötü sonuçlar doğurabilir.

Şuna da dikkati çekmek gerekir ki, sıfırdan fazla elemanı olan bir dizi her koşulda değiştirilebilir, bu yüzden bir sınıfın public static final bir dizi tanımlaması, veya böyle bir dizi döndüren bir erişim metodu içermesi yanlıştır. Bu yapıldığı taktirde, sınıfın istemcileri dizinin içeriğini kolayca değiştirebilirler. Bu, çok yaygın görülen bir güvenlik açığıdır:

// Potansiyel güvenlik açığı! 
public static final Thing[] VALUES = { ... };

Birçok IDE private tanımlanmış diziler için public erişim metodları tanımlayarak bu probleme yol açmaktadırlar. Bu sebeple dikkatli olun! Bu sorunu çözmek için iki yol vardır. Yukarıdaki public diziyi private tanımlayıp, yanında bir de public ama değiştirilemez bir liste tanımlayabilirsiniz:

private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES =
       Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

Alternatif olarak, yine diziyi private tanımlayıp public erişim metodu içerisinde dizinin bir kopyasını döndürebilirsiniz:

private static final Thing[] PRIVATE_VALUES = { ... }; 
public static final Thing[] values() { 
    return PRIVATE_VALUES.clone();
}

Arada seçim yaparken istemcinizin dönüş değerini nasıl kullanacağını düşünebilirsiniz. Hangi dönüş türü daha kullanışlı olacaktır? Hangisi daha iyi performans verecektir?

Özetleyecek olursak, erişilebilirliği her zaman mümkün olduğunca kısıtlamalısınız. Dikkatli bir biçimde public API tasarladıktan sonra, bunun dışında kalan sınıfları, arayüzleri ve sınıf üyelerini API dışında tutmalısınız. public static final alanlar dışında public sınıfların public alanları olmamalıdır. Ancak public static final alanların da değiştirilemez olduğundan emin olmalısınız.

Share

3 Comments

  1. […] Enum sabitleriyle ilişkilendirmek istediğiniz davranışların bir kısmına dışarıdan erişim gerekmeyebilir. Böyle metotların private veya package-private yazılması en doğrusu olur. Aynen sınıflarda olduğu gibi bir enum metodunu dışarı açmak için mantıklı bir sebep yoksa private veya bunu yapamıyorsanız package-private tanımlayın. (Madde 15) […]

Leave a Reply

%d bloggers like this: